Skip to content

Commit f9777bd

Browse files
authored
Merge pull request #125 from sonusindhu/signal-forms
Signal forms - example 5
2 parents 940ea34 + 3a9e332 commit f9777bd

File tree

11 files changed

+920
-1
lines changed

11 files changed

+920
-1
lines changed

src/app/examples/signal-form/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This section contains examples for building reactive forms with Angular signals.
88
| 2 | **Signal Form Validations** | Signals, validation, error handling | Showcases validation, error handling, and extensible form logic using Angular signals. | [🔗 Demo](/signal-forms/example2) |
99
| 3 | **Signal Form Signup with Custom Validation** | Signals, custom validation, password match | Signup form with name, email, password, confirm password fields and custom password match validation using Angular signals. | [🔗 Demo](/signal-forms/example3) |
1010
| 4 | **Signal Form Signup with Dynamic Hobbies & Validation** | Signals, dynamic fields, schema validation, extensible logic | Signup form with name, email, and dynamic hobbies fields. Features schema-based validation, instant error feedback, and interactive add/remove for hobbies using Angular signals. | [🔗 Demo](/signal-forms/example4) |
11+
| 5 | **Advanced Nested Signal Form** | Signals, nested fields, dynamic arrays | Deeply nested form with dynamic contacts and tags using Angular signals. | [🔗 Demo](/signal-forms/example5) |
1112

1213
---
1314

src/app/examples/signal-form/example4/example4.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="flex flex-wrap gap-2 items-center justify-start mt-4 mb-4">
33
<button mat-stroked-button color="primary" routerLink="/signal-forms">Go to List</button>
44
<button mat-stroked-button color="primary" routerLink="/signal-forms/example3">Prev</button>
5-
<button mat-stroked-button color="primary" disabled>Next</button>
5+
<button mat-stroked-button color="primary" routerLink="/signal-forms/example5">Next</button>
66
</div>
77

88
<mat-tab-group>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
:host {
2+
display: contents;
3+
form {
4+
margin: auto;
5+
background: #fff;
6+
border-radius: 8px;
7+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
8+
padding: 2.5rem 2.5rem;
9+
display: flex;
10+
flex-direction: column;
11+
gap: 2rem;
12+
}
13+
label {
14+
font-weight: bold;
15+
margin-bottom: 0.3rem;
16+
display: block;
17+
}
18+
input {
19+
padding: 0.7rem 1.1rem;
20+
border: 1px solid #bdbdbd;
21+
border-radius: 6px;
22+
min-width: 200px;
23+
font-size: 1rem;
24+
background: #f9f9f9;
25+
transition: border-color 0.2s, box-shadow 0.2s;
26+
}
27+
input:focus {
28+
border-color: #1976d2;
29+
outline: none;
30+
background: #fff;
31+
box-shadow: 0 0 0 2px #1976d220;
32+
}
33+
button {
34+
padding: 0.75rem;
35+
border-radius: 4px;
36+
cursor: pointer;
37+
}
38+
button:disabled {
39+
cursor: not-allowed;
40+
}
41+
42+
h3,
43+
h4 {
44+
margin-top: 1.5rem;
45+
margin-bottom: 0.8rem;
46+
font-weight: 600;
47+
color: #1976d2;
48+
letter-spacing: 0.02em;
49+
}
50+
mat-card {
51+
background: #f5f7fa;
52+
border-radius: 12px;
53+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
54+
padding: 2rem 1.5rem;
55+
margin-bottom: 2rem;
56+
}
57+
.nested-section {
58+
background: #f0f4f8;
59+
border-radius: 8px;
60+
padding: 1.2rem 1.5rem;
61+
margin-bottom: 1.2rem;
62+
border-left: 4px solid #1976d2;
63+
}
64+
p[style*='color:red'] {
65+
margin: 0.3rem 0 0.7rem 0;
66+
font-size: 1.05rem;
67+
font-weight: 500;
68+
color: #e53935 !important;
69+
letter-spacing: 0.01em;
70+
}
71+
.contact-card {
72+
background: #fff;
73+
border-radius: 8px;
74+
box-shadow: 0 1px 6px rgba(25, 118, 210, 0.08);
75+
border: 1px solid #e3e8ee;
76+
padding: 1.2rem 1.2rem 1rem 1.2rem;
77+
margin-bottom: 1.2rem;
78+
position: relative;
79+
transition: box-shadow 0.2s;
80+
}
81+
.contact-card:hover {
82+
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.13);
83+
border-color: #1976d2;
84+
}
85+
/* Style for radio group */
86+
.radio-group {
87+
display: flex;
88+
gap: 1.5rem;
89+
margin-bottom: 0.7rem;
90+
align-items: center;
91+
}
92+
.radio-group label {
93+
font-weight: 500;
94+
color: #333;
95+
margin-right: 0.5rem;
96+
cursor: pointer;
97+
display: flex;
98+
align-items: center;
99+
gap: 0.3rem;
100+
}
101+
input[type="radio"] {
102+
accent-color: #1976d2;
103+
margin-right: 0.3rem;
104+
}
105+
/* Style for checkbox */
106+
.checkbox-group {
107+
display: flex;
108+
align-items: center;
109+
gap: 0.5rem;
110+
margin-bottom: 0.7rem;
111+
}
112+
input[type="checkbox"] {
113+
accent-color: #1976d2;
114+
width: 1.1rem;
115+
height: 1.1rem;
116+
margin-right: 0.3rem;
117+
}
118+
/* Style for select box */
119+
select {
120+
padding: 0.6rem 1rem;
121+
border: 1px solid #bdbdbd;
122+
border-radius: 6px;
123+
font-size: 1rem;
124+
background: #f9f9f9;
125+
margin-top: 0.3rem;
126+
margin-bottom: 0.7rem;
127+
min-width: 180px;
128+
transition: border-color 0.2s;
129+
}
130+
select:focus {
131+
border-color: #1976d2;
132+
outline: none;
133+
background: #fff;
134+
}
135+
@media (max-width: 700px) {
136+
form {
137+
padding: 1rem 0.5rem;
138+
max-width: 100%;
139+
}
140+
mat-card {
141+
padding: 1rem 0.5rem;
142+
}
143+
.nested-section {
144+
padding: 0.7rem 0.5rem;
145+
}
146+
input {
147+
min-width: 120px;
148+
}
149+
}
150+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { FormExample5Component } from './example5.component';
3+
4+
describe('FormExample5Component', () => {
5+
let component: FormExample5Component;
6+
let fixture: ComponentFixture<FormExample5Component>;
7+
8+
beforeEach(async () => {
9+
await TestBed.configureTestingModule({
10+
declarations: [ FormExample5Component ]
11+
})
12+
.compileComponents();
13+
14+
fixture = TestBed.createComponent(FormExample5Component);
15+
component = fixture.componentInstance;
16+
fixture.detectChanges();
17+
});
18+
19+
it('should create', () => {
20+
expect(component).toBeTruthy();
21+
});
22+
});

0 commit comments

Comments
 (0)