Installs into .claude/skills of the current project.
Are you the author of Angular Signal Forms?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/danielsogl-angular-signal-forms)
---
name: angular-signal-forms
description: "Generate an Angular Signal Forms component with schema validation and dynamic field management for a given entity."
argument-hint: "Entity name and target feature folder (e.g. Task src/app/features/tasks)"
disable-model-invocation: true
---
# Angular 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-developer` skill's `references/signal-forms.md` for 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:
1. **Entity Definition**: The target entity model must be defined and linked in the chat context
2. **Form Purpose**: Specify the form's purpose (e.g., "User Registration", "Task Creation", "Profile Update")
3. **Form Type**: Indicate if it's a create, edit, or multi-purpose form
4. **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
```typescript
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:
```typescript
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
```typescript
@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
```html
<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:**
```html
<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:**
```html
<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:**
```html
<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:
```typescript
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.
```typescript
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:
```scss
.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
```html
<app-[entity-name]-form
[initialData]="[entityName]"
(formSubmit)="onFormSubmit($event)"
(formCancel)="onFormCancel()">
</app-[entity-name]-form>
```
#### Input/Output Properties
```typescript
// 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
```typescript
// 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
1. **Schema-Based Validation**: Always use Angular's built-in `schema()` function for declarative validation following the `angular-developer` skill (`references/signal-forms.md`). Define schemas at module level (not inside the class) for memoization and reusability.
2. **Modern Angular Patterns**: Use standalone components, function-based DI with `inject()`, and modern control flow (`@if`, `@for`)
3. **Signal-First Approach**: Leverage signals for all reactive state management
4. **Material Design**: Use Angular Material components for consistent UI and built-in accessibility
5. **Type Safety**: Maintain strict TypeScript typing throughout the form with proper interfaces
6. **Dirty Check at Field Level**: Use `field().dirty()` on individual fields — the form-level `form()` object only exposes `valid()`, not `dirty()`
7. **Responsive Design**: Ensure forms work well on all device sizes
8. **Error Handling**: Provide clear, user-friendly error messages with custom messages in validators
9. **Performance**: OnPush and zoneless are Angular 22 defaults — don't set `changeDetection`; derive state with `computed`
10. **Built-in Validators**: Use Angular's built-in validators (`required`, `email`, `minLength`, etc.) for common validation needs
## Key References
- `angular-developer` skill — `references/signal-forms.md` for `form()`, `schema()`, validators and `FormField`
- Styling and TypeScript conventions from the project instructions