From harness-claude
Builds type-safe reactive forms in Angular with FormGroup, FormControl, Validators, and dynamic FormArrays. Use for complex validation, dynamic controls, and multi-step wizards.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Build type-safe reactive forms with FormGroup, FormControl, Validators, and dynamic FormArrays
Build signal-based reactive forms in Angular v21+ using Signal Forms API for two-way binding, schema validation, field state, dynamic/multi-step forms. Triggers on form implementation, validation, conditional fields.
Guides Angular development for SPAs: components, services, modules, routing, lazy-loading, forms, DI, RxJS, TypeScript, and CLI setup. For app creation and feature implementation.
Implements form validation using React Hook Form, Formik, Vee-Validate, and custom validators with real-time feedback and TypeScript safety. Use for user input, multi-step forms, and server-side sync.
Share bugs, ideas, or general feedback.
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