angular-signal-forms
Generate an Angular Signal Forms component with schema validation and dynamic field management for a given entity.
SKILL.md.claude/skills/angular-signal-forms/SKILL.md
GitHubAngular Signal Forms Generator
Goal
Generate a complete Angular Signal Form with schema validation, error handling, and dynamic field management for a specified entity, following the project's architecture patterns and modern Angular Signal Forms API with built-in schema validation.
Important: Read the
angular-developerskill'sreferences/signal-forms.mdfor comprehensive Signal Forms patterns, schema validation approaches, and best practices before generating forms. Project-wide conventions live in the generated rules (.claude/rules/for Claude,.github/instructions/for Copilot).
Prerequisites
Before running this skill, ensure you have:
- Entity Definition: The target entity model must be defined and linked in the chat context
- Form Purpose: Specify the form's purpose (e.g., "User Registration", "Task Creation", "Profile Update")
- Form Type: Indicate if it's a create, edit, or multi-purpose form
- Field Requirements: Any specific field requirements or validation rules
Instructions
Step 1: Entity and Requirements Analysis
- Analyze the provided entity model to understand its structure, types, and relationships
- Identify which fields should be included in the form (some entity fields might be auto-generated or readonly)
- Determine appropriate input types for each field (text, email, number, date, select, checkbox, etc.)
- Check for any existing validation patterns or business rules in the codebase
- Identify nested objects and array fields that require dynamic management
Step 2: Form Component Generation
Generate a complete Angular Signal Form component with schema validation:
Core Imports and Dependencies
import { Component, signal, computed } from '@angular/core';
import {
form,
schema,
FormField,
FormRoot,
required,
email,
minLength,
maxLength,
min,
max,
pattern,
applyEach,
} from '@angular/forms/signals';
import { JsonPipe } from '@angular/common';
// Material 21+: import individual components, not *Module umbrellas
import { MatButton } from '@angular/material/button';
import { MatCard, MatCardHeader, MatCardTitle, MatCardContent } from '@angular/material/card';
import { MatFormField, MatLabel, MatError } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { MatSelect, MatOption } from '@angular/material/select';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepicker, MatDatepickerInput, MatDatepickerToggle } from '@angular/material/datepicker';
Form Definition with Inline Schema
Define the form by passing the model signal and a schema function directly to form(). The schema function receives a fieldPath for type-safe validator wiring:
interface [EntityType] {
name: string;
email: string;
description?: string;
address: {
street: string;
city: string;
};
tags: string[];
}
const EMPTY_[ENTITY_NAME]: [EntityType] = {
name: '',
email: '',
description: '',
address: { street: '', city: '' },
tags: [],
};
Component Structure
@Component({
selector: 'app-[entity-name]-form',
imports: [
MatCard, MatCardHeader, MatCardTitle, MatCardContent,
MatButton,
MatFormField, MatLabel, MatError,
MatInput,
MatSelect, MatOption,
MatCheckbox,
MatDatepicker, MatDatepickerInput, MatDatepickerToggle,
FormField,
FormRoot,
JsonPipe,
],
templateUrl: './[entity-name]-form.html',
styleUrl: './[entity-name]-form.scss',
})
export class [EntityName]Form {
// Signal-backed form model
protected readonly [entityName]Model = signal<[EntityType]>(EMPTY_[ENTITY_NAME]);
// Validation schema (defined at module level for reuse and memoization)
private readonly [entityName]Schema = schema<[EntityType]>((f) => {
required(f.name, { message: 'Name is required' });
minLength(f.name, 2, { message: 'Name must be at least 2 characters' });
required(f.email, { message: 'Email is required' });
email(f.email, { message: 'Enter a valid email address' });
maxLength(f.description, 500, { message: 'Description cannot exceed 500 characters' });
// Nested objects
required(f.address.street, { message: 'Street is required' });
required(f.address.city, { message: 'City is required' });
// Arrays
applyEach(f.tags, (tag) => {
minLength(tag, 1, { message: 'Tag cannot be empty' });
});
});
// Form with schema; `submission.action` runs on submit when the form is valid
protected readonly [entityName]Form = form(this.[entityName]Model, this.[entityName]Schema, {
submission: { action: async () => this.onSubmit() },
});
// Computed form state — dirty check at field level, not form level
protected readonly canSubmit = computed(() =>
this.[entityName]Form().valid() &&
(this.[entityName]Form.name().dirty() || this.[entityName]Form.email().dirty()),
);
private onSubmit(): void {
const value = this.[entityName]Model();
// ... call store / API
}
}
Step 3: Template Generation
Create a comprehensive template with modern Angular patterns:
Form Structure
<mat-card>
<mat-card-header>
<mat-card-title>[Form Title]</mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- [formRoot] submits via Enter and type="submit" buttons; no (ngSubmit), no FormsModule -->
<form [formRoot]="[entityName]Form">
<!-- Form fields based on entity structure -->
<button mat-flat-button type="submit" [disabled]="!canSubmit()">Save</button>
</form>
</mat-card-content>
</mat-card>
Field Templates
For each entity field, generate appropriate input with validation:
Text/Email Fields:
<mat-form-field class="w-full">
<mat-label>[Field Label]</mat-label>
<input matInput [formField]="[entityName]Form.[fieldName]" placeholder="[Placeholder]">
@if([entityName]Form.[fieldName]().touched() || [entityName]Form.[fieldName]().dirty()) {
@for (error of [entityName]Form.[fieldName]().errors(); track error.kind) {
<mat-error>{{ error.message }}</mat-error>
}
}
</mat-form-field>
Select Fields:
<mat-form-field class="w-full">
<mat-label>[Field Label]</mat-label>
<mat-select [formField]="[entityName]Form.[fieldName]">
@for (option of [options]; track option.value) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-select>
@if([entityName]Form.[fieldName]().touched() || [entityName]Form.[fieldName]().dirty()) {
@for (error of [entityName]Form.[fieldName]().errors(); track error.kind) {
<mat-error>{{ error.message }}</mat-error>
}
}
</mat-form-field>
Dynamic Array Fields:
<div class="array-field-section">
<h3>[Array Field Label]</h3>
@for (item of [entityName]Form.[arrayField]; track item; let i = $index) {
<mat-card class="mb-4">
<!-- Array item fields -->
<button mat-icon-button class="remove-button" type="button" aria-label="Remove [item]" (click)="remove[ArrayItem](i)">
<mat-icon>delete</mat-icon>
</button>
</mat-card>
}
@empty {
<p class="text-muted">No [items] added yet.</p>
}
<button mat-stroked-button type="button" (click)="add[ArrayItem]()">
<mat-icon>add</mat-icon> Add [Item]
</button>
</div>
Step 4: Component Methods Implementation
Dynamic Field Management
Implement methods for adding/removing dynamic fields:
add[ArrayItem](): void {
this.[entityName].update(state => ({
...state,
[arrayField]: [...state.[arrayField], {
// Default values for new item
}]
}));
}
remove[ArrayItem](index: number): void {
this.[entityName].update(state => ({
...state,
[arrayField]: state.[arrayField].filter((_, i) => i !== index)
}));
}
Form Submission
[formRoot] calls submit() on the form: it marks every field as touched (so errors show), then runs
submission.action only if the form is valid. No manual valid check or "mark all touched" helper.
protected readonly [entityName]Form = form(this.[entityName]Model, this.[entityName]Schema, {
submission: {
action: async () => this.formSubmit.emit(this.[entityName]Model()),
onInvalid: () => { /* optional: focus the first invalid field */ },
},
});
Step 5: Styling and Accessibility
Component Styles
Create responsive and accessible styles:
.form-container {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.form-section {
margin-bottom: 2rem;
}
.array-field-section {
.mat-card {
position: relative;
.remove-button {
color: var(--mat-sys-error); // `color="warn"` has no effect in Material 22
position: absolute;
top: 8px;
right: 8px;
}
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 2rem;
}
@media (max-width: 768px) {
.form-container {
padding: 0.5rem;
}
.form-actions {
flex-direction: column;
}
}
Accessibility Features
- Proper ARIA labels and descriptions
- Keyboard navigation support
- Focus management
- Screen reader compatibility
Step 6: Integration Guide
Component Usage
<app-[entity-name]-form
[initialData]="[entityName]"
(formSubmit)="onFormSubmit($event)"
(formCancel)="onFormCancel()">
</app-[entity-name]-form>
Input/Output Properties
// Add these to the component if needed
readonly initialData = input<Partial<[EntityType]>>();
readonly mode = input<'create' | 'edit'>('create');
readonly formSubmit = output<[EntityType]>();
readonly formCancel = output<void>();
Parent Component Integration
// Example parent component usage
@Component({
template: `
<app-[entity-name]-form
[initialData]="selectedEntity()"
(formSubmit)="handleFormSubmit($event)">
</app-[entity-name]-form>
`
})
export class Parent {
selectedEntity = signal<[EntityType] | null>(null);
handleFormSubmit(data: [EntityType]): void {
// Handle form submission
}
}
File Structure
Create the following files in the appropriate domain folder:
src/app/[domain]/
├── feature/
│ ├── [entity-name]-form/
│ │ ├── [entity-name]-form.ts
│ │ ├── [entity-name]-form.html
│ │ └── [entity-name]-form.scss
└── data/
└── models/
└── [entity-name].model.ts # Entity model reference
Best Practices
- Schema-Based Validation: Always use Angular's built-in
schema()function for declarative validation following theangular-developerskill (references/signal-forms.md). Define schemas at module level (not inside the class) for memoization and reusability. - Modern Angular Patterns: Use standalone components, function-based DI with
inject(), and modern control flow (@if,@for) - Signal-First Approach: Leverage signals for all reactive state management
- Material Design: Use Angular Material components for consistent UI and built-in accessibility
- Type Safety: Maintain strict TypeScript typing throughout the form with proper interfaces
- Dirty Check at Field Level: Use
field().dirty()on individual fields — the form-levelform()object only exposesvalid(), notdirty() - Responsive Design: Ensure forms work well on all device sizes
- Error Handling: Provide clear, user-friendly error messages with custom messages in validators
- Performance: OnPush and zoneless are Angular 22 defaults — don't set
changeDetection; derive state withcomputed - Built-in Validators: Use Angular's built-in validators (
required,email,minLength, etc.) for common validation needs
Key References
angular-developerskill —references/signal-forms.mdforform(),schema(), validators andFormField- Styling and TypeScript conventions from the project instructions