From 0455e95ca851424ca9adc29a96d400b884f12954 Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 18 Feb 2026 13:55:59 +0100 Subject: [PATCH] feat(63): answer to challenge 63 custom sub form --- .../src/app/address-form.component.ts | 81 ++++++ .../63-custom-forms/src/app/app.component.ts | 264 ++++++------------ 2 files changed, 160 insertions(+), 185 deletions(-) create mode 100644 apps/forms/63-custom-forms/src/app/address-form.component.ts diff --git a/apps/forms/63-custom-forms/src/app/address-form.component.ts b/apps/forms/63-custom-forms/src/app/address-form.component.ts new file mode 100644 index 000000000..34db85170 --- /dev/null +++ b/apps/forms/63-custom-forms/src/app/address-form.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + FieldState, + FieldTree, + FormField, + required, + SchemaPathTree, +} from '@angular/forms/signals'; + +interface AddressGroup { + street: string; + zipcode: string; + city: string; +} + +export const defaultAddress: AddressGroup = { + street: '', + zipcode: '', + city: '', +}; + +export const addressSchema = (item: SchemaPathTree) => { + required(item.street); + required(item.zipcode); + required(item.city); +}; + +@Component({ + selector: 'app-address-form', + imports: [FormField], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ + + +
+ `, + styles: [ + ` + .input { + @apply w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm outline-none transition focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200; + } + .hint { + @apply text-xs text-rose-600; + } + `, + ], +}) +export class AddressFormComponent { + form = input.required>(); + + showError(field: FieldState): boolean { + return field.invalid() && (field.touched() || field.dirty()); + } +} diff --git a/apps/forms/63-custom-forms/src/app/app.component.ts b/apps/forms/63-custom-forms/src/app/app.component.ts index ff074abff..fba2941e2 100644 --- a/apps/forms/63-custom-forms/src/app/app.component.ts +++ b/apps/forms/63-custom-forms/src/app/app.component.ts @@ -1,38 +1,50 @@ -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { JsonPipe } from '@angular/common'; import { - FormControl, - FormGroup, - ReactiveFormsModule, - Validators, -} from '@angular/forms'; + ChangeDetectionStrategy, + Component, + effect, + signal, + untracked, +} from '@angular/core'; +import { + apply, + disabled, + FieldState, + form, + FormField, + required, + submit, +} from '@angular/forms/signals'; +import { + AddressFormComponent, + addressSchema, + defaultAddress, +} from './address-form.component'; -type AddressGroup = FormGroup<{ - street: FormControl; - zipcode: FormControl; - city: FormControl; -}>; +type AddressGroup = { + street: string; + zipcode: string; + city: string; +}; type CheckoutForm = { - firstName: FormControl; - lastName: FormControl; + firstName: string; + lastName: string; shipping: AddressGroup; billing: AddressGroup; - sameAsShipping: FormControl; + sameAsShipping: boolean; }; @Component({ selector: 'app-root', - standalone: true, - imports: [CommonModule, ReactiveFormsModule], + imports: [AddressFormComponent, FormField, JsonPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `

Order

Information

@@ -40,14 +52,9 @@ type CheckoutForm = {
- @if (!form.controls.sameAsShipping.value) { -
- - - -
+ @if (!form.sameAsShipping().value()) { + }
- - {{ form.invalid ? 'Form incomplete' : 'Ready to submit' }} + + {{ form().invalid() ? 'Form incomplete' : 'Ready to submit' }}
@@ -230,59 +133,50 @@ type CheckoutForm = { ], }) export class AppComponent { - readonly shipping: AddressGroup = this.createAddressGroup(); - readonly billing: AddressGroup = this.createAddressGroup(); + checkoutModel = signal({ + firstName: '', + lastName: '', + shipping: defaultAddress, + billing: defaultAddress, + sameAsShipping: false, + }); - readonly form = new FormGroup({ - firstName: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), - lastName: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), - shipping: this.shipping, - billing: this.billing, - sameAsShipping: new FormControl(false, { nonNullable: true }), + form = form(this.checkoutModel, (schema) => { + required(schema.firstName); + required(schema.lastName); + required(schema.billing, { + when: ({ valueOf }) => valueOf(schema.sameAsShipping), + }); + apply(schema.shipping, addressSchema); + apply(schema.billing, addressSchema); + disabled(schema.billing, ({ valueOf }) => valueOf(schema.sameAsShipping)); }); - toggleSameAsShipping(): void { - const checked = !this.form.controls.sameAsShipping.value; - this.form.controls.sameAsShipping.setValue(checked, { emitEvent: false }); - if (checked) { - this.billing.setValue(this.shipping.getRawValue(), { emitEvent: false }); - this.billing.disable({ emitEvent: false }); - } else { - this.billing.enable({ emitEvent: false }); - } - } + constructor() { + effect(() => { + const sameAsShipping = this.form.sameAsShipping().value(); - onSubmit(): void { - this.form.markAllAsTouched(); - if (this.form.invalid) { - return; - } + if (sameAsShipping) { + untracked(() => { + this.form.billing().value.set(this.form.shipping().value()); + }); + } + }); } - showError(control: FormControl): boolean { - return control.invalid && (control.touched || control.dirty); - } + async onSubmit(event: Event) { + event.preventDefault(); + + const toto = this.form.billing; - private createAddressGroup(): AddressGroup { - return new FormGroup({ - street: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), - zipcode: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), - city: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), + await submit(this.form, async () => { + alert( + 'Form submitted successfully! Check the console for the submitted value.', + ); }); } + + showError(field: FieldState): boolean { + return field.invalid() && (field.touched() || field.dirty()); + } }