From harness-claude
Build type-safe Angular reactive forms with FormGroup, FormControl, Validators, and dynamic FormArrays. Includes custom validators, FormArray manipulation, and best practices.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:angular-reactive-formsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Build type-safe reactive forms with FormGroup, FormControl, Validators, and dynamic FormArrays
Build type-safe reactive forms with FormGroup, FormControl, Validators, and dynamic FormArrays
FormBuilder (inject via inject(FormBuilder)) to construct FormGroup and FormControl — it reduces boilerplate significantly.FormGroup<{ email: FormControl<string>; password: FormControl<string> }>. Angular 14+ infers types from the FormBuilder.nonNullable builder.FormBuilder.nonNullable when controls should never be null — it eliminates null narrowing on .value reads.Validators.required, Validators.email, Validators.minLength(n). Compose them as an array.(control: AbstractControl): ValidationErrors | null => .... Prefer synchronous validators; use async validators only for server-side checks (e.g., username availability).FormArray for variable-length lists (e.g., multiple phone numbers, line items). Access controls via .controls and mutate via .push(), .removeAt().form.statusChanges and form.valueChanges sparingly — prefer template binding to form.valid and form.value in the submit handler.form.markAllAsTouched() before showing validation errors on submit to trigger error display for untouched fields.import { Component, inject } from '@angular/core';
import {
FormBuilder,
FormGroup,
FormControl,
FormArray,
Validators,
AbstractControl,
ValidationErrors,
} from '@angular/forms';
function noWhitespace(control: AbstractControl): ValidationErrors | null {
const trimmed = (control.value ?? '').trim();
return trimmed.length === 0 && control.value?.length > 0 ? { whitespace: true } : null;
}
@Component({
selector: 'app-signup',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<span *ngIf="form.controls.email.errors?.['email']">Invalid email</span>
<div formArrayName="phones">
<div *ngFor="let phone of phones.controls; let i = index">
<input [formControlName]="i" type="tel" />
<button type="button" (click)="removePhone(i)">Remove</button>
</div>
<button type="button" (click)="addPhone()">Add phone</button>
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`,
})
export class SignupComponent {
private fb = inject(FormBuilder).nonNullable;
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8), noWhitespace]],
phones: this.fb.array([this.fb.control('')]),
});
get phones(): FormArray<FormControl<string>> {
return this.form.controls.phones;
}
addPhone(): void {
this.phones.push(this.fb.control(''));
}
removePhone(index: number): void {
this.phones.removeAt(index);
}
onSubmit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log(this.form.getRawValue());
}
}
Typed forms (Angular 14+): Before Angular 14, .value returned any. Typed forms make the value inferred from the control definition. Use FormBuilder.nonNullable (or new FormControl<string>('')) to avoid string | null everywhere. The getRawValue() method returns values including disabled controls; .value skips them.
Cross-field validators: Attach at the FormGroup level, not the control level. The validator receives the entire group and can compare controls:
function passwordsMatch(group: AbstractControl): ValidationErrors | null {
const pw = group.get('password')?.value;
const confirm = group.get('confirm')?.value;
return pw === confirm ? null : { mismatch: true };
}
this.fb.nonNullable.group({ password: '', confirm: '' }, { validators: passwordsMatch });
Async validators: Return Observable<ValidationErrors | null> or Promise<ValidationErrors | null>. Angular sets status to 'PENDING' while the validator runs. Debounce with switchMap to avoid hammering the server on every keystroke.
updateOn strategy: By default, validation runs on every value change. Use updateOn: 'blur' or updateOn: 'submit' on a control or group to reduce validation frequency:
this.fb.nonNullable.control('', { validators: Validators.required, updateOn: 'blur' });
Resetting vs patching: form.reset() clears all controls and resets touched/dirty flags. form.patchValue({ email: 'x' }) updates only the supplied keys. form.setValue({...}) requires every key to be provided or throws. Prefer patchValue when loading partial data.
Performance: Avoid creating reactive form controls inside *ngFor loops without caching — Angular recreates them on every change detection cycle. Use FormArray and index-based formControlName instead.
https://angular.dev/guide/forms/reactive-forms
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeBuild signal-based forms in Angular v21+ using the experimental Signal Forms API with automatic two-way binding, schema-based validation, and dynamic form support.
Generates Angular code and provides architectural guidance on signals, forms, DI, routing, SSR, accessibility, animations, styling, testing, and CLI tooling.
Generates Angular code and architecture guidance for projects, components, services, signals, forms, DI, routing, SSR, accessibility, animations, styling, testing, and CLI tools.