Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions apps/forms/61-simplest-signal-form/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<div class="min-h-screen bg-gray-100 px-4 py-12 sm:px-6 lg:px-8">
<div class="mx-auto max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 class="mb-6 text-3xl font-bold text-gray-900">Simple Form</h1>

<form class="space-y-6" [formRoot]="userForm">
<div>
<label for="name" class="mb-2 block text-sm font-medium text-gray-700">
Name
<span class="text-red-500">*</span>
</label>
<input
id="name"
type="text"
[formField]="userForm.name"
placeholder="Enter your name"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
userForm.name().invalid() && userForm.name().touched()
" />
@if (userForm.name().errors().length > 0 && userForm.name().touched()) {
@for (error of userForm.name().errors(); track error) {
<p class="mt-1 text-sm text-red-600">{{ error.message }}</p>
}
}
</div>

<div>
<label
for="lastname"
class="mb-2 block text-sm font-medium text-gray-700">
Last Name
</label>
<input
id="lastname"
type="text"
[formField]="userForm.lastname"
placeholder="Enter your last name"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
</div>

<div>
<label for="age" class="mb-2 block text-sm font-medium text-gray-700">
Age
</label>
<input
id="age"
type="number"
[formField]="userForm.age"
placeholder="Enter your age (1-99)"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
userForm.age().invalid() && userForm.age().touched()
" />
@if (userForm.age().errors().length > 0 && userForm.age().touched()) {
@for (error of userForm.age().errors(); track error) {
<p class="mt-1 text-sm text-red-600">{{ error.message }}</p>
}
}
</div>

<div>
<label for="note" class="mb-2 block text-sm font-medium text-gray-700">
Note
</label>
<input
id="note"
type="text"
[formField]="userForm.note"
placeholder="Enter a note"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
</div>

<div class="flex gap-4">
<button
type="submit"
[disabled]="userForm().invalid()"
class="flex-1 rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400">
Submit
</button>
<button
type="button"
(click)="onReset()"
class="flex-1 rounded-md bg-gray-600 px-4 py-2 font-medium text-white transition hover:bg-gray-700">
Reset
</button>
</div>
</form>

@if (submittedData()) {
<div class="mt-8 rounded-lg border border-green-200 bg-green-50 p-4">
<h2 class="mb-2 text-lg font-semibold text-green-900">
Submitted Data:
</h2>
<pre
class="overflow-x-auto rounded border border-green-200 bg-white p-4 text-sm"
>{{ submittedData() | json }}</pre
>
</div>
}
</div>
</div>
202 changes: 55 additions & 147 deletions apps/forms/61-simplest-signal-form/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,160 +1,68 @@
import { JsonPipe } from '@angular/common';
import { Component, signal, WritableSignal } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
ChangeDetectionStrategy,
Component,
signal,
WritableSignal,
} from '@angular/core';
import {
form,
FormField,
FormRoot,
max,
min,
required,
} from '@angular/forms/signals';

type UserData = {
name: string;
lastname: string;
age: number | null;
note: string;
};

@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
template: `
<div class="min-h-screen bg-gray-100 px-4 py-12 sm:px-6 lg:px-8">
<div class="mx-auto max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 class="mb-6 text-3xl font-bold text-gray-900">Simple Form</h1>

<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-6">
<div>
<label
for="name"
class="mb-2 block text-sm font-medium text-gray-700">
Name
<span class="text-red-500">*</span>
</label>
<input
id="name"
type="text"
formControlName="name"
placeholder="Enter your name"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.name.invalid && !form.controls.name.untouched
" />
@if (form.controls.name.invalid && !form.controls.name.untouched) {
<p class="mt-1 text-sm text-red-600">Name is required</p>
}
</div>

<div>
<label
for="lastname"
class="mb-2 block text-sm font-medium text-gray-700">
Last Name
</label>
<input
id="lastname"
type="text"
formControlName="lastname"
placeholder="Enter your last name"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
</div>

<div>
<label
for="age"
class="mb-2 block text-sm font-medium text-gray-700">
Age
</label>
<input
id="age"
type="number"
formControlName="age"
placeholder="Enter your age (1-99)"
min="1"
max="99"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.age.invalid && !form.controls.age.untouched
" />
@if (form.controls.age.invalid && !form.controls.age.untouched) {
<p class="mt-1 text-sm text-red-600">
@if (form.controls.age.hasError('min')) {
Age must be at least 1
}
@if (form.controls.age.hasError('max')) {
Age must be at most 99
}
</p>
}
</div>

<div>
<label
for="note"
class="mb-2 block text-sm font-medium text-gray-700">
Note
</label>
<input
id="note"
type="text"
formControlName="note"
placeholder="Enter a note"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
</div>

<div class="flex gap-4">
<button
type="submit"
[disabled]="form.invalid"
class="flex-1 rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400">
Submit
</button>
<button
type="button"
(click)="onReset()"
class="flex-1 rounded-md bg-gray-600 px-4 py-2 font-medium text-white transition hover:bg-gray-700">
Reset
</button>
</div>
</form>

@if (submittedData()) {
<div class="mt-8 rounded-lg border border-green-200 bg-green-50 p-4">
<h2 class="mb-2 text-lg font-semibold text-green-900">
Submitted Data:
</h2>
<pre
class="overflow-x-auto rounded border border-green-200 bg-white p-4 text-sm"
>{{ submittedData() | json }}</pre
>
</div>
}
</div>
</div>
`,
imports: [JsonPipe, FormField, FormRoot],
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
form = new FormGroup({
name: new FormControl('', {
validators: Validators.required,
nonNullable: true,
}),
lastname: new FormControl('', { nonNullable: true }),
age: new FormControl<number | null>(null, [
Validators.min(1),
Validators.max(99),
]),
note: new FormControl('', { nonNullable: true }),
});
private readonly _initialData: UserData = {
name: '',
lastname: '',
age: null,
note: '',
};
private _userModel = signal<UserData>(this._initialData);

submittedData: WritableSignal<{
name: string;
lastname: string;
age: number | null;
note: string;
} | null> = signal(null);
protected userForm = form(
this._userModel,
(schemaPath) => {
required(schemaPath.name, { message: 'Name is required' });
min(schemaPath.age, 1, { message: 'Age must be at least 1' });
max(schemaPath.age, 99, { message: 'Age must be at most 99' });
},
{
submission: {
action: async () => {
if (this.userForm().valid()) {
this.setSubmittedData();
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
},
},
);
protected submittedData: WritableSignal<UserData | null> = signal(null);

onSubmit(): void {
if (this.form.valid) {
this.submittedData.set(this.form.getRawValue());
console.log('Form submitted:', this.submittedData);
}
public onReset(): void {
this.userForm().reset(this._initialData);
this.setSubmittedData();
}

onReset(): void {
this.form.reset();
this.submittedData.set(null);
private setSubmittedData(): void {
const formData = this._userModel();
console.log('Form submitted:', formData);
this.submittedData.set(formData);
}
}