Skip to content
Back to skills

Angular Signal Forms

ASecurity

Generate an Angular Signal Forms component with schema validation and dynamic field management for a given entity.

  • 39 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmenttypescriptgoreactangulargitapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add danielsogl/copilot-workflow-demo --skill angular-signal-forms --agent claude-code

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.

Security grade badge for Angular Signal Forms
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielsogl-angular-signal-forms/badge)](https://www.skillsdirectory.com/skills/danielsogl-angular-signal-forms)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.