|
| 1 | +<div class="example-container"> |
| 2 | +<div class="flex flex-wrap gap-2 items-center justify-start mt-4 mb-4"> |
| 3 | + <button mat-stroked-button color="primary" routerLink="/signal-forms">Go to List</button> |
| 4 | + <button mat-stroked-button color="primary" routerLink="/signal-forms/example4">Prev</button> |
| 5 | + <button mat-stroked-button color="primary" disabled>Next</button> |
| 6 | + </div> |
| 7 | + |
| 8 | + <mat-tab-group> |
| 9 | + <mat-tab label="Demo"> |
| 10 | + <div class="content-area"> |
| 11 | + <h2>Example 5: Advanced Nested Signal Form</h2> |
| 12 | + <p>This example demonstrates a deeply nested form with multiple fields and arrays, including dynamic tags for each contact.</p> |
| 13 | + </div> |
| 14 | + <div class="demo-section flex gap-4 flex-col"> |
| 15 | + <mat-card class="w-full"> |
| 16 | + <mat-card-content> |
| 17 | + <form (ngSubmit)="onSubmit()"> |
| 18 | + <!-- User fields --> |
| 19 | + <div> |
| 20 | + <input placeholder="Name" [control]="userForm.name" /> |
| 21 | + @if(userForm.name().touched() || userForm.name().dirty()) { |
| 22 | + @for (err of userForm.name().errors(); track err.kind) { |
| 23 | + <p style="color:red">{{ err.message }}</p> |
| 24 | + } |
| 25 | + } |
| 26 | + </div> |
| 27 | + <div> |
| 28 | + <input placeholder="Age" type="number" [control]="userForm.age" /> |
| 29 | + @if(userForm.age().touched() || userForm.age().dirty()) { |
| 30 | + @for (err of userForm.age().errors(); track err.kind) { |
| 31 | + <p style="color:red">{{ err.message }}</p> |
| 32 | + } |
| 33 | + } |
| 34 | + </div> |
| 35 | + <div> |
| 36 | + <input placeholder="Email" [control]="userForm.email" /> |
| 37 | + @if(userForm.email().touched() || userForm.email().dirty()) { |
| 38 | + @for (err of userForm.email().errors(); track err.kind) { |
| 39 | + <p style="color:red">{{ err.message }}</p> |
| 40 | + } |
| 41 | + } |
| 42 | + </div> |
| 43 | + |
| 44 | + <!-- Additional user fields --> |
| 45 | + <div> |
| 46 | + <label>Gender:</label> |
| 47 | + <div class="flex gap-2 items-center mb-2"> |
| 48 | + <label><input type="radio" [control]="userForm.gender" value="male" /> Male</label> |
| 49 | + <label><input type="radio" [control]="userForm.gender" value="female" /> Female</label> |
| 50 | + <label><input type="radio" [control]="userForm.gender" value="other" /> Other</label> |
| 51 | + </div> |
| 52 | + @if(userForm.gender().touched() || userForm.gender().dirty()) { |
| 53 | + @for (err of userForm.gender().errors(); track err.kind) { |
| 54 | + <p style="color:red">{{ err.message }}</p> |
| 55 | + } |
| 56 | + } |
| 57 | + </div> |
| 58 | + <div> |
| 59 | + <label> |
| 60 | + <input type="checkbox" [control]="userForm.subscribe" /> Subscribe to newsletter: |
| 61 | + </label> |
| 62 | + </div> |
| 63 | + |
| 64 | + <!-- Address fields --> |
| 65 | + <div> |
| 66 | + <h3>Address</h3> |
| 67 | + <div class="flex gap-2 mb-4"> |
| 68 | + <div> |
| 69 | + <input placeholder="Street" [control]="userForm.address.street" /> |
| 70 | + </div> |
| 71 | + <div> |
| 72 | + <input placeholder="City" [control]="userForm.address.city" /> |
| 73 | + </div> |
| 74 | + <div> |
| 75 | + <input placeholder="Zip" [control]="userForm.address.zip" /> |
| 76 | + </div> |
| 77 | + <div> |
| 78 | + <select [control]="userForm.address.country"> |
| 79 | + <option value="">Select Country</option> |
| 80 | + <option value="India">India</option> |
| 81 | + <option value="USA">USA</option> |
| 82 | + <option value="UK">UK</option> |
| 83 | + <option value="Other">Other</option> |
| 84 | + </select> |
| 85 | + @if(userForm.address.country().touched() || userForm.address.country().dirty()) { |
| 86 | + @for (err of userForm.address.country().errors(); track err.kind) { |
| 87 | + <p style="color:red">{{ err.message }}</p> |
| 88 | + } |
| 89 | + } |
| 90 | + </div> |
| 91 | + </div> |
| 92 | + </div> |
| 93 | + |
| 94 | + <!-- Contacts array --> |
| 95 | + <div> |
| 96 | + <h3>Contacts</h3> |
| 97 | + @for (contact of userForm.address.contacts; track contact; let i = $index) { |
| 98 | + <div class="contact-card"> |
| 99 | + <div class="flex gap-2 items-center mb-2"> |
| 100 | + <input [control]="contact.type" placeholder="Type (e.g. phone, email)" /> |
| 101 | + <input [control]="contact.value" placeholder="Value" /> |
| 102 | + <button mat-flat-button color="warn" type="button" (click)="removeContact(i)">Remove Contact</button> |
| 103 | + @if(contact.type().touched() || contact.type().dirty()) { |
| 104 | + @for (err of contact.type().errors(); track err.kind) { |
| 105 | + <p style="color:red">{{ err.message }}</p> |
| 106 | + } |
| 107 | + } |
| 108 | + @if(contact.value().touched() || contact.value().dirty()) { |
| 109 | + @for (err of contact.value().errors(); track err.kind) { |
| 110 | + <p style="color:red">{{ err.message }}</p> |
| 111 | + } |
| 112 | + } |
| 113 | + </div> |
| 114 | + <!-- Tags array for each contact --> |
| 115 | + <div class="ml-6"> |
| 116 | + <h4>Tags</h4> |
| 117 | + @for (tag of contact.tags; track tag; let j = $index) { |
| 118 | + <div class="flex gap-2 items-center mb-2"> |
| 119 | + <input [control]="tag" placeholder="Tag" /> |
| 120 | + <button mat-flat-button color="warn" type="button" (click)="removeTag(i, j)">Remove Tag</button> |
| 121 | + @if(tag().touched() || tag().dirty()) { |
| 122 | + @for (err of tag().errors(); track err.kind) { |
| 123 | + <p style="color:red">{{ err.message }}</p> |
| 124 | + } |
| 125 | + } |
| 126 | + </div> |
| 127 | + } |
| 128 | + @empty { |
| 129 | + <p class="text-gray-500 py-2">No tags added yet.</p> |
| 130 | + } |
| 131 | + <button mat-flat-button color="primary" type="button" (click)="addTag(i)">+ Add Tag</button> |
| 132 | + </div> |
| 133 | + </div> |
| 134 | + } |
| 135 | + @empty { |
| 136 | + <p class="text-gray-500 py-2">No contacts added yet.</p> |
| 137 | + } |
| 138 | + <button mat-flat-button color="primary" type="button" (click)="addContact()">+ Add Contact</button> |
| 139 | + </div> |
| 140 | + |
| 141 | + <div> |
| 142 | + <button mat-stroked-button color="primary" type="submit" [disabled]="!userForm().valid">Save</button> |
| 143 | + </div> |
| 144 | + </form> |
| 145 | + </mat-card-content> |
| 146 | + </mat-card> |
| 147 | + <mat-card class="w-full"> |
| 148 | + <mat-card-content> |
| 149 | + <pre>{{ user() | json }}</pre> |
| 150 | + <h3>Key Features</h3> |
| 151 | + <ul> |
| 152 | + <li>⚡ Deeply nested reactive form state powered by Angular signals</li> |
| 153 | + <li>✅ Schema-based validation for all fields (user, address, contacts, tags)</li> |
| 154 | + <li>🔄 Real-time error feedback and UI updates</li> |
| 155 | + <li>➕ Dynamic add/remove for contacts and tags</li> |
| 156 | + <li>🧩 Minimal, readable, and extensible form logic</li> |
| 157 | + <li>🛡️ Instant validation and error display for each field</li> |
| 158 | + </ul> |
| 159 | + <p>This example demonstrates efficient nested form state management and custom validation using Angular signals, with instant error feedback and a clean, extensible approach.</p> |
| 160 | + </mat-card-content> |
| 161 | + </mat-card> |
| 162 | + </div> |
| 163 | + </mat-tab> |
| 164 | + <mat-tab label="HTML"> |
| 165 | + <markdown clipboard [src]="'assets/examples/signal-forms/example5/example5.component.html.md'"></markdown> |
| 166 | + </mat-tab> |
| 167 | + <mat-tab label="TS"> |
| 168 | + <markdown clipboard [src]="'assets/examples/signal-forms/example5/example5.component.ts.md'"></markdown> |
| 169 | + </mat-tab> |
| 170 | + <mat-tab label="SCSS"> |
| 171 | + <markdown clipboard [src]="'assets/examples/signal-forms/example5/example5.component.scss.md'"></markdown> |
| 172 | + </mat-tab> |
| 173 | + </mat-tab-group> |
| 174 | +</div> |
0 commit comments