Skip to content

Commit ee7ea2e

Browse files
authored
Merge pull request #114 from sonusindhu/signal-forms
Signal forms - example 3
2 parents fa3810f + 9764f81 commit ee7ea2e

File tree

10 files changed

+357
-1
lines changed

10 files changed

+357
-1
lines changed

src/app/examples/signal-form/example2/example2.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/example1">Prev</button>
5-
<button mat-stroked-button color="primary" disabled="">Next</button>
5+
<button mat-stroked-button color="primary" routerLink="/signal-forms/example3">Next</button>
66
</div>
77

88
<mat-tab-group>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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/example2">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 3: Signal Form Signup with Custom Validation</h2>
12+
<p>This example demonstrates a signup form using Angular signals and custom password match validation.</p>
13+
</div>
14+
<div class="demo-section flex gap-4 flex-row md:flex-col">
15+
<mat-card class="w-full md:w-1/2">
16+
<mat-card-content>
17+
<form (ngSubmit)="onSubmit()">
18+
<div>
19+
<input type="text" placeholder="Name" [control]="userForm.name" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
20+
@if(userForm.name().touched() || userForm.name().dirty()){
21+
@for (item of userForm.name().errors(); track item) {
22+
<p class="text-red-500 p-0">{{ item.message }}</p>
23+
}
24+
}
25+
</div>
26+
<div>
27+
<input type="email" placeholder="Email" [control]="userForm.email" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
28+
@if(userForm.email().touched() || userForm.email().dirty()){
29+
@for (item of userForm.email().errors(); track $index) {
30+
<p class="text-red-500 p-0">{{ item.message }}</p>
31+
}
32+
}
33+
</div>
34+
<div>
35+
<input type="password" placeholder="Password" [control]="userForm.password" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
36+
@if(userForm.password().touched() || userForm.password().dirty()){
37+
@for (item of userForm.password().errors(); track $index) {
38+
<p class="text-red-500 p-0">{{ item.message }}</p>
39+
}
40+
}
41+
{{ userForm.password().errors() | json }}
42+
</div>
43+
<div>
44+
<input type="password" placeholder="Confirm Password" [control]="userForm.confirm_password" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
45+
@if(userForm.confirm_password().touched() || userForm.confirm_password().dirty()){
46+
@for (item of userForm.confirm_password().errors(); track $index) {
47+
<p class="text-red-500 p-0">{{ item.message }}</p>
48+
}
49+
}
50+
{{ userForm.confirm_password().errors() | json }}
51+
</div>
52+
<button mat-raised-button color="primary" type="submit" [disabled]="!userForm().valid">Sign Up</button>
53+
{{ userForm().errors()| json }}
54+
</form>
55+
</mat-card-content>
56+
</mat-card>
57+
<mat-card class="w-full md:w-1/2">
58+
<mat-card-content>
59+
<h3>Key Features</h3>
60+
<ul>
61+
<li>⚡ Reactive form state with Angular signals</li>
62+
<li>✅ Built-in and custom validation (password match)</li>
63+
<li>🔄 Real-time UI updates on input changes</li>
64+
<li>🧩 Minimal, readable form logic</li>
65+
<li>🛠️ Easily extensible for more fields and rules</li>
66+
</ul>
67+
<p>This example demonstrates efficient form state management and custom validation using Angular signals, with instant error feedback and a clean, extensible approach.</p>
68+
</mat-card-content>
69+
</mat-card>
70+
</div>
71+
</mat-tab>
72+
<mat-tab label="HTML">
73+
<markdown clipboard [src]="'assets/examples/signal-forms/example3/example3.component.html.md'"></markdown>
74+
</mat-tab>
75+
<mat-tab label="TS">
76+
<markdown clipboard [src]="'assets/examples/signal-forms/example3/example3.component.ts.md'"></markdown>
77+
</mat-tab>
78+
<mat-tab label="SCSS">
79+
<markdown clipboard [src]="'assets/examples/signal-forms/example3/example3.component.scss.md'"></markdown>
80+
</mat-tab>
81+
</mat-tab-group>
82+
</div>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
:host{
2+
display: contents;
3+
form {
4+
max-width: 400px;
5+
margin: auto;
6+
display: flex;
7+
flex-direction: column;
8+
gap: 1rem;
9+
}
10+
label {
11+
font-weight: bold;
12+
}
13+
input {
14+
padding: 0.5rem;
15+
border: 1px solid #ccc;
16+
border-radius: 4px;
17+
}
18+
button {
19+
background: #1976d2;
20+
color: white;
21+
border: none;
22+
padding: 0.75rem;
23+
border-radius: 4px;
24+
cursor: pointer;
25+
}
26+
button:disabled {
27+
background: #ccc;
28+
cursor: not-allowed;
29+
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { FormExample3Component } from './example3.component';
2+
describe('FormExample3Component', () => {
3+
it('should create', () => {
4+
const component = new FormExample3Component();
5+
expect(component).toBeTruthy();
6+
});
7+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Component, effect, signal } from '@angular/core';
2+
import { form, required, email, minLength, pattern, schema, Control, FieldPath, validate, customError } from '@angular/forms/signals';
3+
import { MatButtonModule } from '@angular/material/button';
4+
import { MatTabsModule } from '@angular/material/tabs';
5+
import { MatCardModule } from '@angular/material/card';
6+
import { RouterModule } from '@angular/router';
7+
import { MarkdownComponent } from 'ngx-markdown';
8+
import { JsonPipe } from '@angular/common';
9+
10+
function matchPassword(
11+
path: FieldPath<{ password: unknown, confirm_password: unknown }>
12+
) {
13+
14+
validate(path, (form) => {
15+
// get path value
16+
const formValue = form.value();
17+
18+
// if when condition is false, return null
19+
if(formValue.password !== formValue.confirm_password){
20+
return customError({ kind: 'notMatched', message: 'Passwords do not match' });
21+
}
22+
23+
return null;
24+
})
25+
26+
}
27+
28+
function matchConfirmPassword(path: FieldPath<string>) {
29+
30+
validate(path, (confirmPassword) => {
31+
const field: any = confirmPassword.field;
32+
33+
const password = field()?.['structure'].parent?.value()?.password;
34+
35+
// if when condition is false, return null
36+
if(password !== confirmPassword.value()){
37+
return customError({ kind: 'notMatched', message: 'Passwords do not match' });
38+
}
39+
40+
return null;
41+
})
42+
43+
}
44+
45+
46+
const signupSchema = schema<{ name: string; email: string; password: string; confirm_password: string }>((form) => {
47+
48+
required(form.name, { message: 'Name is required' });
49+
pattern(form.name, /^[a-zA-Z ]+$/, { message: 'Enter a valid name' });
50+
51+
required(form.email, { message: 'Email is required' });
52+
email(form.email, { message: 'Enter a valid email' });
53+
54+
required(form.password, { message: 'Password is required' });
55+
minLength(form.password, 6, { message: 'Password must be at least 6 characters' });
56+
57+
required(form.confirm_password, { message: 'Confirm password is required' });
58+
59+
// matchPassword(form); // it will inject in the form level error
60+
matchConfirmPassword(form.confirm_password); // it will inject in the confirm_password field level error
61+
});
62+
63+
@Component({
64+
selector: 'app-form-example3',
65+
standalone: true,
66+
imports: [
67+
MatButtonModule,
68+
MatTabsModule,
69+
MatCardModule,
70+
MarkdownComponent,
71+
RouterModule,
72+
Control,
73+
JsonPipe
74+
],
75+
templateUrl: './example3.component.html',
76+
styleUrls: ['./example3.component.scss']
77+
})
78+
export class FormExample3Component {
79+
public user = signal({ name: '', email: '', password: '', confirm_password: '' });
80+
public userForm = form(this.user, signupSchema);
81+
82+
public errors = effect(() => {
83+
return this.userForm().errors();
84+
});
85+
86+
onSubmit() {
87+
if (this.userForm().valid()) {
88+
alert('Signup successful!');
89+
}
90+
}
91+
}

src/app/examples/signal-form/signal-forms-routings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const ADVANCED_ROUTES: Route[] = [
1313
{
1414
path: 'example2',
1515
loadComponent: () => import('./example2/example2.component').then(x => x.FormExample2Component),
16+
},
17+
{
18+
path: 'example3',
19+
loadComponent: () => import('./example3/example3.component').then(x => x.FormExample3Component),
1620
}
1721
];
1822

src/app/examples/signal-form/signal-forms.const.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ export const SIGNAL_FORM_EXAMPLES: ExampleModel[] = [
1010
title: 'Example 2: Signal Form Validations',
1111
content: 'Showcases validation, error handling, and extensible form logic using Angular signals.',
1212
routerLink: '/signal-forms/example2'
13+
},
14+
{
15+
title: 'Example 3: Signal Form Signup with Custom Validation',
16+
content: 'Signup form with name, email, password, confirm password fields and custom password match validation using Angular signals.',
17+
routerLink: '/signal-forms/example3'
1318
}
1419
];
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
```html
2+
<div class="demo-section flex gap-4 flex-row md:flex-col">
3+
<mat-card class="w-full md:w-1/2">
4+
<mat-card-content>
5+
<form (ngSubmit)="onSubmit()">
6+
<div>
7+
<input type="text" placeholder="Name" [control]="userForm.name" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
8+
@if(userForm.name().touched() || userForm.name().dirty()){
9+
@for (item of userForm.name().errors(); track item) {
10+
<p class="text-red-500 p-0">{{ item.message }}</p>
11+
}
12+
}
13+
</div>
14+
<div>
15+
<input type="email" placeholder="Email" [control]="userForm.email" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
16+
@if(userForm.email().touched() || userForm.email().dirty()){
17+
@for (item of userForm.email().errors(); track $index) {
18+
<p class="text-red-500 p-0">{{ item.message }}</p>
19+
}
20+
}
21+
</div>
22+
<div>
23+
<input type="password" placeholder="Password" [control]="userForm.password" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
24+
@if(userForm.password().touched() || userForm.password().dirty()){
25+
@for (item of userForm.password().errors(); track $index) {
26+
<p class="text-red-500 p-0">{{ item.message }}</p>
27+
}
28+
}
29+
</div>
30+
<div>
31+
<input type="password" placeholder="Confirm Password" [control]="userForm.confirm_password" class="w-full p-2 border border-gray-300 rounded-md text-base mb-4" />
32+
@if(userForm.confirm_password().touched() || userForm.confirm_password().dirty()){
33+
@for (item of userForm.confirm_password().errors(); track $index) {
34+
<p class="text-red-500 p-0">{{ item.message }}</p>
35+
}
36+
}
37+
</div>
38+
<button mat-raised-button color="primary" type="submit" [disabled]="!userForm().valid">Sign Up</button>
39+
</form>
40+
</mat-card-content>
41+
</mat-card>
42+
<mat-card class="w-full md:w-1/2">
43+
<mat-card-content>
44+
<h3>Key Features</h3>
45+
<ul>
46+
<li>⚡ Reactive form state with Angular signals</li>
47+
<li>✅ Built-in and custom validation (password match)</li>
48+
<li>🔄 Real-time UI updates on input changes</li>
49+
<li>🧩 Minimal, readable form logic</li>
50+
<li>🛠️ Easily extensible for more fields and rules</li>
51+
</ul>
52+
<p>This example demonstrates efficient form state management and custom validation using Angular signals, with instant error feedback and a clean, extensible approach.</p>
53+
</mat-card-content>
54+
</mat-card>
55+
</div>
56+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
```scss
2+
:host{
3+
display: contents;
4+
form {
5+
max-width: 400px;
6+
margin: auto;
7+
display: flex;
8+
flex-direction: column;
9+
gap: 1rem;
10+
}
11+
label {
12+
font-weight: bold;
13+
}
14+
input {
15+
padding: 0.5rem;
16+
border: 1px solid #ccc;
17+
border-radius: 4px;
18+
}
19+
button {
20+
background: #1976d2;
21+
color: white;
22+
border: none;
23+
padding: 0.75rem;
24+
border-radius: 4px;
25+
cursor: pointer;
26+
}
27+
button:disabled {
28+
background: #ccc;
29+
cursor: not-allowed;
30+
}
31+
}
32+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
```typescript
2+
import { Component } from '@angular/core';
3+
import { effect, signal } from '@angular/core';
4+
import { form, required, email, minLength, pattern, schema, FieldPath, validate, customError, Control } from '@angular/forms/signals';
5+
import { JsonPipe } from '@angular/common';
6+
import { MatCardModule } from '@angular/material/card';
7+
8+
function matchConfirmPassword(path: FieldPath<string>) {
9+
validate(path, (confirmPassword) => {
10+
const field: any = confirmPassword.field;
11+
const password = field()?.['structure'].parent?.value()?.password;
12+
if(password !== confirmPassword.value()){
13+
return customError({ kind: 'notMatched', message: 'Passwords do not match' });
14+
}
15+
return null;
16+
})
17+
}
18+
19+
const signupSchema = schema<{ name: string; email: string; password: string; confirm_password: string }>((form) => {
20+
required(form.name, { message: 'Name is required' });
21+
pattern(form.name, /^[a-zA-Z ]+$/, { message: 'Enter a valid name' });
22+
required(form.email, { message: 'Email is required' });
23+
email(form.email, { message: 'Enter a valid email' });
24+
required(form.password, { message: 'Password is required' });
25+
minLength(form.password, 6, { message: 'Password must be at least 6 characters' });
26+
required(form.confirm_password, { message: 'Confirm password is required' });
27+
matchConfirmPassword(form.confirm_password); // custom validator for password match
28+
});
29+
30+
@Component({
31+
selector: 'app-form-example3',
32+
standalone: true,
33+
imports: [Control, JsonPipe, MatCardModule],
34+
templateUrl: './example3.component.html',
35+
styleUrls: ['./example3.component.scss']
36+
})
37+
export class FormExample3Component {
38+
public user = signal({ name: '', email: '', password: '', confirm_password: '' });
39+
public userForm = form(this.user, signupSchema);
40+
public errors = effect(() => {
41+
return this.userForm().errors();
42+
});
43+
onSubmit() {
44+
if (this.userForm().valid()) {
45+
alert('Signup successful!');
46+
}
47+
}
48+
}
49+
```

0 commit comments

Comments
 (0)