Skip to content
Back to skills

Angular Forms

ASecurity

Angular Reactive Forms with validators, FormArray, and dynamic forms. Covers typed forms, custom validators, and form patterns. USE WHEN: user mentions "Angular forms", "reactive forms", "FormGroup", "FormArray", "validators", "form validation", "Angular form patterns" DO NOT USE FOR: React forms - use `react-hook-form` or `react-forms`, template-driven forms for simple cases

  • 31 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 8, 2026
ai-agentstypescriptreactangulardocumentation

Works with

  • mcp

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill angular-forms --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Forms?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Forms
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-angular-forms/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-angular-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-forms
description: |
  Angular Reactive Forms with validators, FormArray, and dynamic forms.
  Covers typed forms, custom validators, and form patterns.

  USE WHEN: user mentions "Angular forms", "reactive forms", "FormGroup", "FormArray",
  "validators", "form validation", "Angular form patterns"

  DO NOT USE FOR: React forms - use `react-hook-form` or `react-forms`,
  template-driven forms for simple cases
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Angular Forms - Quick Reference

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `angular`, topic: `forms` for comprehensive documentation.

## Typed Reactive Forms (Angular 14+)

```typescript
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="name" />
      <input formControlName="email" type="email" />
      <button type="submit" [disabled]="form.invalid">Save</button>
    </form>
  `
})
export class UserFormComponent {
  private fb = inject(FormBuilder);

  form = this.fb.nonNullable.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    age: [0, [Validators.min(0), Validators.max(150)]],
  });

  onSubmit() {
    if (this.form.valid) {
      const value = this.form.getRawValue(); // Typed!
      console.log(value.name); // string, not string | null
    }
  }
}
```

## FormArray

```typescript
form = this.fb.group({
  name: ['', Validators.required],
  addresses: this.fb.array([this.createAddress()]),
});

createAddress() {
  return this.fb.group({
    street: ['', Validators.required],
    city: ['', Validators.required],
    zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]],
  });
}

get addresses() {
  return this.form.get('addresses') as FormArray;
}

addAddress() {
  this.addresses.push(this.createAddress());
}

removeAddress(index: number) {
  this.addresses.removeAt(index);
}
```

## Custom Validators

```typescript
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Sync validator
export function forbiddenName(name: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = control.value === name;
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Async validator
export function uniqueEmail(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return userService.checkEmail(control.value).pipe(
      map(exists => exists ? { emailTaken: true } : null),
      catchError(() => of(null))
    );
  };
}

// Cross-field validator
export const passwordMatchValidator: ValidatorFn = (form: AbstractControl) => {
  const password = form.get('password')?.value;
  const confirm = form.get('confirmPassword')?.value;
  return password === confirm ? null : { passwordMismatch: true };
};
```

## Error Display Pattern

```typescript
@Component({
  template: `
    <input formControlName="email" />
    @if (form.controls.email.hasError('required') && form.controls.email.touched) {
      <span class="error">Email is required</span>
    }
    @if (form.controls.email.hasError('email') && form.controls.email.touched) {
      <span class="error">Invalid email format</span>
    }
  `
})
```

## Anti-Patterns

| Anti-Pattern | Why It's Bad | Correct Approach |
|--------------|--------------|------------------|
| Template-driven for complex forms | Hard to test, no type safety | Use reactive forms |
| Not using `nonNullable` | Nullable types everywhere | Use `fb.nonNullable.group()` |
| Validation in component | Not reusable | Extract to validator functions |
| Subscribing to valueChanges without cleanup | Memory leaks | Use `takeUntilDestroyed()` |

## Quick Troubleshooting

| Issue | Likely Cause | Solution |
|-------|--------------|----------|
| Form always invalid | Missing required field value | Check initial values |
| Type errors with controls | Using untyped FormGroup | Use `fb.nonNullable.group()` |
| Async validator fires too often | No debounce | Add `updateOn: 'blur'` |
| FormArray changes not reflected | Missing trackBy | Use track in `@for` |

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.