Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Each major example category has its own dedicated README with detailed examples
- [Control Flow Examples](src/app/examples/control-flow/README.md) — @if, @for, @switch blocks (**7 examples**)
- [Resource API Examples](src/app/examples/resource-api/README.md) — Data fetching and resource patterns (**9 examples**)
- [Advanced Examples](src/app/examples/advanced/README.md) — Real-world and advanced patterns (**10 examples**, including collaborative list, undo/redo, zoneless change detection demo, and more)
- [Signal Forms Examples](src/app/examples/signal-form/README.md) — Reactive forms with signals (**4 examples**)
- [Signal Forms Examples](src/app/examples/signal-form/README.md) — Reactive forms with signals (**6 examples**)

Jump into any category above to explore all the examples and details!

Expand Down
1 change: 1 addition & 0 deletions src/app/examples/signal-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This section contains examples for building reactive forms with Angular signals.
| 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) |
| 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) |
| 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) |
| 6 | **Dynamic Projects & Tasks Signal Form** | Signals, nested arrays, schema validation, dynamic add/remove | Form with dynamic projects and tasks, schema-based validation, instant error feedback, and extensible logic using Angular signals. | [🔗 Demo](/signal-forms/example6) |

---

Expand Down
54 changes: 37 additions & 17 deletions src/app/examples/signal-form/example5/example5.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="flex flex-wrap gap-2 items-center justify-start mt-4 mb-4">
<button mat-stroked-button color="primary" routerLink="/signal-forms">Go to List</button>
<button mat-stroked-button color="primary" routerLink="/signal-forms/example4">Prev</button>
<button mat-stroked-button color="primary" disabled>Next</button>
<button mat-stroked-button color="primary" routerLink="/signal-forms/example6">Next</button>
</div>

<mat-tab-group>
Expand Down Expand Up @@ -67,12 +67,27 @@ <h3>Address</h3>
<div class="flex gap-2 mb-4">
<div>
<input placeholder="Street" [control]="userForm.address.street" />
@if(userForm.address.street().touched() || userForm.address.street().dirty()) {
@for (err of userForm.address.street().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
</div>
<div>
<input placeholder="City" [control]="userForm.address.city" />
@if(userForm.address.city().touched() || userForm.address.city().dirty()) {
@for (err of userForm.address.city().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
</div>
<div>
<input placeholder="Zip" [control]="userForm.address.zip" />
@if(userForm.address.zip().touched() || userForm.address.zip().dirty()) {
@for (err of userForm.address.zip().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
</div>
<div>
<select [control]="userForm.address.country">
Expand All @@ -97,19 +112,23 @@ <h3>Contacts</h3>
@for (contact of userForm.address.contacts; track contact; let i = $index) {
<div class="contact-card">
<div class="flex gap-2 items-center mb-2">
<input [control]="contact.type" placeholder="Type (e.g. phone, email)" />
<input [control]="contact.value" placeholder="Value" />
<button mat-flat-button color="warn" type="button" (click)="removeContact(i)">Remove Contact</button>
@if(contact.type().touched() || contact.type().dirty()) {
@for (err of contact.type().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
<div>
<input [control]="contact.type" placeholder="Type (e.g. phone, email)" />
@if(contact.type().touched() || contact.type().dirty()) {
@for (err of contact.type().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
}
@if(contact.value().touched() || contact.value().dirty()) {
@for (err of contact.value().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
</div>
<div>
<input [control]="contact.value" placeholder="Value" />
@if(contact.value().touched() || contact.value().dirty()) {
@for (err of contact.value().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
}
</div>
<button mat-flat-button color="warn" type="button" (click)="removeContact(i)">Remove Contact</button>
</div>
<!-- Tags array for each contact -->
<div class="ml-6">
Expand All @@ -118,12 +137,13 @@ <h4>Tags</h4>
<div class="flex gap-2 items-center mb-2">
<input [control]="tag" placeholder="Tag" />
<button mat-flat-button color="warn" type="button" (click)="removeTag(i, j)">Remove Tag</button>
@if(tag().touched() || tag().dirty()) {
@for (err of tag().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
</div>

@if(tag().touched() || tag().dirty()) {
@for (err of tag().errors(); track err.kind) {
<p style="color:red">{{ err.message }}</p>
}
}
}
@empty {
<p class="text-gray-500 py-2">No tags added yet.</p>
Expand Down
143 changes: 143 additions & 0 deletions src/app/examples/signal-form/example6/example6.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<div class="example-container p-8">
<div class="flex flex-wrap gap-2 items-center justify-start mt-4 mb-4">
<button mat-stroked-button color="primary" routerLink="/signal-forms">Go to List</button>
<button mat-stroked-button color="primary" routerLink="/signal-forms/example5">Prev</button>
<button mat-stroked-button color="primary" disabled>Next</button>
</div>

<mat-tab-group>
<mat-tab label="Demo">
<div class="content-area mb-6">
<h2 class="text-2xl font-bold mb-2">Example 6: Dynamic Projects & Tasks Signal Form</h2>
<p class="text-gray-600">This example demonstrates a dynamic array of nested objects (projects with tasks) using Angular signals forms.</p>
</div>
<div class="demo-section flex gap-4 flex-col">
<mat-card class="w-full shadow-md">
<mat-card-content>
<form (ngSubmit)="onSubmit()" class="space-y-6">
<!-- Username field -->
<div>
<input placeholder="Username" [control]="userProjectsForm.username" class="input input-bordered w-full" />
@if(userProjectsForm.username().touched() || userProjectsForm.username().dirty()) {
@for (err of userProjectsForm.username().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
</div>

<!-- Projects array -->
<div>
<h3 class="text-lg font-semibold mb-2">Projects</h3>
@for (project of userProjectsForm.projects; track project; let i = $index) {
<div class="project-card border border-gray-200 rounded-lg p-4 mb-4 bg-gray-50">
<div class="flex gap-2 items-center mb-2">
<div class="flex-1">
<input [control]="project.name" placeholder="Project Name" class="input input-bordered w-full" />
@if(project.name().touched() || project.name().dirty()) {
@for (err of project.name().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
</div>
<div class="flex flex-col gap-2 ml-4">
<input type="date" [control]="project.deadline" class="input input-bordered" placeholder="Deadline" />
@if(project.deadline().touched() || project.deadline().dirty()) {
@for (err of project.deadline().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
<select [control]="project.status" class="input input-bordered">
<option value="Not Started">Not Started</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
</select>
@if(project.status().touched() || project.status().dirty()) {
@for (err of project.status().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
</div>
<button mat-flat-button color="warn" type="button" (click)="removeProject(i)" class="ml-2">Remove Project</button>
</div>
<!-- Project summary -->
<div class="mb-2 text-sm text-gray-600">
{{ getProjectTaskSummary(i) }}
</div>
<!-- Tasks array for each project -->
<div class="ml-6">
<h4 class="font-medium mb-2">Tasks</h4>
@for (task of project.tasks; track task; let j = $index) {
<div class="flex gap-2 items-center mb-2">
<input [control]="task.title" placeholder="Task Title" class="input input-bordered w-64" />
<select [control]="task.priority" class="input input-bordered">
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
</select>
<input type="date" [control]="task.dueDate" class="input input-bordered w-40" placeholder="Due Date" />
<label class="flex items-center gap-1"><input type="checkbox" [control]="task.done" /> <span>Done</span></label>
<button mat-flat-button color="warn" type="button" (click)="removeTask(i, j)">Remove Task</button>
</div>
@if(task.title().touched() || task.title().dirty()) {
@for (err of task.title().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
@if(task.priority().touched() || task.priority().dirty()) {
@for (err of task.priority().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
@if(task.dueDate().touched() || task.dueDate().dirty()) {
@for (err of task.dueDate().errors(); track err.kind) {
<p class="text-red-500 text-sm mt-1">{{ err.message }}</p>
}
}
}
@empty {
<p class="text-gray-500 py-2">No tasks added yet.</p>
}
<button mat-flat-button color="primary" type="button" (click)="addTask(i)">+ Add Task</button>
</div>
</div>
}
@empty {
<p class="text-gray-500 py-2">No projects added yet.</p>
}
<button mat-flat-button color="primary" type="button" (click)="addProject()">+ Add Project</button>
</div>

<div>
<button mat-stroked-button color="primary" type="submit" [disabled]="!userProjectsForm().valid()" class="mt-4">Save</button>
</div>
</form>
</mat-card-content>
</mat-card>
<mat-card class="w-full shadow-md">
<mat-card-content>
<pre class="bg-gray-100 p-4 rounded text-xs overflow-x-auto">{{ userProjects() | json }}</pre>
<h3 class="text-lg font-semibold mt-4 mb-2">Key Features</h3>
<ul class="list-disc ml-6 text-gray-700">
<li>⚡ Dynamic nested array form state powered by Angular signals</li>
<li>✅ Schema-based validation for all fields (username, projects, tasks)</li>
<li>🔄 Real-time error feedback and UI updates</li>
<li>➕ Dynamic add/remove for projects and tasks</li>
<li>🧩 Minimal, readable, and extensible form logic</li>
<li>🛡️ Instant validation and error display for each field</li>
</ul>
<p class="text-gray-600 mt-2">This example demonstrates efficient dynamic form state management and custom validation using Angular signals, with instant error feedback and a clean, extensible approach.</p>
</mat-card-content>
</mat-card>
</div>
</mat-tab>
<mat-tab label="HTML">
<markdown clipboard [src]="'assets/examples/signal-forms/example6/example6.component.html.md'"></markdown>
</mat-tab>
<mat-tab label="TS">
<markdown clipboard [src]="'assets/examples/signal-forms/example6/example6.component.ts.md'"></markdown>
</mat-tab>
<mat-tab label="SCSS">
<markdown clipboard [src]="'assets/examples/signal-forms/example6/example6.component.scss.md'"></markdown>
</mat-tab>
</mat-tab-group>
</div>
26 changes: 26 additions & 0 deletions src/app/examples/signal-form/example6/example6.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
:host{
display: contents;
.project-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
background: #fafafa;
}

input {
padding: 0.7rem 1.1rem;
border: 1px solid #bdbdbd;
border-radius: 6px;
min-width: 200px;
font-size: 1rem;
background: #f9f9f9;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus {
border-color: #1976d2;
outline: none;
background: #fff;
box-shadow: 0 0 0 2px #1976d220;
}
}
Loading