install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/angular-reactive-forms" ~/.claude/skills/intense-visions-harness-engineering-angular-reactive-forms && rm -rf "$T"
manifest:
agents/skills/claude-code/angular-reactive-forms/SKILL.mdsource content
Angular Reactive Forms
Build type-safe reactive forms with FormGroup, FormControl, Validators, and dynamic FormArrays
When to Use
- Building forms with non-trivial validation logic or cross-field validators
- Creating dynamic forms where controls are added/removed at runtime
- Needing to programmatically reset, patch, or observe form value changes via observables
- Replacing template-driven forms to gain full TypeScript type safety (Angular 14+ typed forms)
- Building wizard-style multi-step forms backed by a single FormGroup
Instructions
- Use
(inject viaFormBuilder
) to constructinject(FormBuilder)
andFormGroup
— it reduces boilerplate significantly.FormControl - Type your forms explicitly:
. Angular 14+ infers types from theFormGroup<{ email: FormControl<string>; password: FormControl<string> }>
builder.FormBuilder.nonNullable - Use
when controls should never be null — it eliminates null narrowing onFormBuilder.nonNullable
reads..value - Attach built-in validators via
,Validators.required
,Validators.email
. Compose them as an array.Validators.minLength(n) - Write custom validators as plain functions:
. Prefer synchronous validators; use async validators only for server-side checks (e.g., username availability).(control: AbstractControl): ValidationErrors | null => ... - Use
for variable-length lists (e.g., multiple phone numbers, line items). Access controls viaFormArray
and mutate via.controls
,.push()
..removeAt() - Subscribe to
andform.statusChanges
sparingly — prefer template binding toform.valueChanges
andform.valid
in the submit handler.form.value - Call
before showing validation errors on submit to trigger error display for untouched fields.form.markAllAsTouched()
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()); } }
Details
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.
strategy: By default, validation runs on every value change. Use updateOn
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.
Source
https://angular.dev/guide/forms/reactive-forms
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.