Skip to content
Open
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
97 changes: 48 additions & 49 deletions apps/forms/61-simplest-signal-form/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { JsonPipe } from '@angular/common';
import { Component, signal, WritableSignal } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { Field, form, max, min, required } from '@angular/forms/signals';

interface IFormData {
name: string;
lastname: string;
age: number;
note: string;
}

@Component({
selector: 'app-root',
imports: [ReactiveFormsModule, JsonPipe],
imports: [Field, 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">
<form (submit)="onSubmit($event)" class="space-y-6">
<div>
<label
for="name"
Expand All @@ -26,14 +28,16 @@ import {
<input
id="name"
type="text"
formControlName="name"
[field]="form.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
form.name().invalid() && form.name().touched()
" />
@if (form.controls.name.invalid && !form.controls.name.untouched) {
<p class="mt-1 text-sm text-red-600">Name is required</p>
@if (form.name().errors().length > 0 && form.name().touched()) {
@for (error of form.name().errors(); track error) {
<p class="mt-1 text-sm text-red-600">{{ error.message }}</p>
}
}
</div>

Expand All @@ -46,7 +50,7 @@ import {
<input
id="lastname"
type="text"
formControlName="lastname"
[field]="form.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>
Expand All @@ -60,23 +64,16 @@ import {
<input
id="age"
type="number"
formControlName="age"
[field]="form.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
form.age().invalid() && form.age().touched()
" />
@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>
@if (form.age().errors().length > 0 && form.age().touched()) {
@for (error of form.age().errors(); track error) {
<p class="mt-1 text-sm text-red-600">{{ error.message }}</p>
}
}
</div>

Expand All @@ -89,15 +86,15 @@ import {
<input
id="note"
type="text"
formControlName="note"
[field]="form.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"
[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>
Expand Down Expand Up @@ -126,35 +123,37 @@ import {
`,
})
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 }),
formDefaults: IFormData = {
name: '',
lastname: '',
age: null!,
note: '',
};

formModel = signal<IFormData>(this.formDefaults);

form = form<IFormData>(this.formModel, (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' });
});

submittedData: WritableSignal<{
name: string;
lastname: string;
age: number | null;
note: string;
} | null> = signal(null);
submittedData: WritableSignal<IFormData | null> = signal(null);

onSubmit(): void {
if (this.form.valid) {
this.submittedData.set(this.form.getRawValue());
onSubmit(e: Event): void {
e.preventDefault();
if (this.form().valid()) {
this.submittedData.set({
...this.form().value(),
// Form returns age as string from the input for some reason, convert it to number
age: +this.form().value().age,
Copy link
Owner

Choose a reason for hiding this comment

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

That's interesting. I'll check that.

});
console.log('Form submitted:', this.submittedData);
}
}

onReset(): void {
this.form.reset();
this.form().reset(this.formDefaults);
this.submittedData.set(null);
}
}
Loading