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-claudeAngular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).
Build 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.