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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
Last name
<input
class="input"
type="text"
[formField]="form().lastName"
aria-required="true" />
<app-validation [fieldState]="form().lastName()" />
</label>
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
First name
<input
class="input"
type="text"
[formField]="form().firstName"
aria-required="true" />
<app-validation [fieldState]="form().firstName()" />
</label>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { FieldTree, FormField } from '@angular/forms/signals';
import { ValidationComponent } from '../validation/validation.component';
import { AccountData } from './account-form.model';

@Component({
selector: 'account-form',
templateUrl: './account-form.component.html',
imports: [FormField, ValidationComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountFormComponent {
public readonly form = input.required<FieldTree<AccountData>>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { signal } from '@angular/core';
import { required, SchemaPathTree } from '@angular/forms/signals';

export type AccountData = {
firstName: string;
lastName: string;
};

export const createAccountModel = () => {
return signal<AccountData>({
firstName: '',
lastName: '',
});
};

export const buildAccountSection = (
schemaPath: SchemaPathTree<AccountData>,
) => {
required(schemaPath.firstName, { message: 'First name is required' });
required(schemaPath.lastName, { message: 'Last name is required' });
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="grid gap-4 sm:grid-cols-2" data-testid="shipping-fields">
<label
class="flex flex-col gap-1 text-sm font-medium text-slate-700 sm:col-span-2">
Street
<input
class="input"
type="text"
[formField]="form().street"
aria-required="true" />
<app-validation [fieldState]="form().street()" />
</label>
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
ZIP code
<input
class="input"
type="text"
[formField]="form().zipcode"
aria-required="true" />
<app-validation [fieldState]="form().zipcode()" />
</label>
<label class="flex flex-col gap-1 text-sm font-medium text-slate-700">
City
<input
class="input"
type="text"
[formField]="form().city"
aria-required="true" />
<app-validation [fieldState]="form().city()" />
</label>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { FieldTree, FormField } from '@angular/forms/signals';
import { ValidationComponent } from '../validation/validation.component';
import { AddressData } from './address-form.model';

@Component({
selector: 'address-form',
templateUrl: './address-form.component.html',
imports: [FormField, ValidationComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressFormComponent {
public readonly form = input.required<FieldTree<AddressData>>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { signal } from '@angular/core';
import { required, SchemaPathTree } from '@angular/forms/signals';

export type AddressData = {
street: string;
zipcode: string;
city: string;
};

export const createAddressModel = () => {
return signal<AddressData>({
street: '',
zipcode: '',
city: '',
});
};

export const buildAddressSection = (
schemaPath: SchemaPathTree<AddressData>,
) => {
required(schemaPath.street, { message: 'Street is required' });
required(schemaPath.zipcode, { message: 'ZIP code is required' });
required(schemaPath.city, { message: 'City is required' });
};
62 changes: 62 additions & 0 deletions apps/forms/63-child-forms/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<main class="min-h-screen bg-slate-50 text-slate-900">
<div class="mx-auto max-w-4xl px-6 py-12">
<h1 class="mb-6 text-3xl font-semibold">Order</h1>

<form
[formRoot]="checkoutForm"
class="space-y-8 rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<section class="space-y-4">
<h2 class="text-xl font-semibold">Information</h2>
<account-form [form]="checkoutForm.account" />
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold">Shipping address</h2>
<address-form [form]="checkoutForm.shipping" />
</section>

<section class="space-y-4">
<div class="flex items-center justify-between gap-4">
<h2 class="text-xl font-semibold">Billing address</h2>
<label
class="flex items-center gap-2 text-sm font-medium text-slate-700">
<input
type="checkbox"
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
[formField]="checkoutForm.sameAsShipping" />
Billing address same as shipping
</label>
</div>

@if (!checkoutForm.sameAsShipping().value()) {
<address-form [form]="checkoutForm.billing" />
}
</section>

<div
class="flex items-center justify-between border-t border-slate-200 pt-4">
<div class="text-sm text-slate-600">
<span [class.text-rose-600]="checkoutForm().invalid()">
{{
checkoutForm().invalid() ? 'Form incomplete' : 'Ready to submit'
}}
</span>
</div>
<button
type="submit"
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:bg-slate-300">
Submit
</button>
</div>
</form>

<section
class="mt-6 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<h3 class="mb-2 text-lg font-semibold">Preview</h3>
<pre
class="overflow-x-auto rounded bg-slate-900 p-4 text-sm text-slate-100"
>{{ checkoutForm().value() | json }}</pre
>
</section>
</div>
</main>
Loading
Loading