Angular Forms
January 13, 2023Reactive Forms
Custom Validators
- Adding Integrated Validation to Custom Form Controls in Angular
- The best way to implement custom validators
- Add validation to Angular material disabled field
- Angular Custom Form Validators: Complete Guide
- Adding Integrated Validation to Custom Form Controls in Angular
Control Value Accessor
Use Case
- Non-native form control elements
- Custom styling / functionality
- Control wrapped with related elements
- Parser / formatter directives
References
- ControlValueAccessor
- Never again be confused when implementing ControlValueAccessor in Angular forms
- Kara Erickson's AngularConnect 2017 talk
- Custom Material control example and details
- Working with Angular forms in an enterprise environment
- The Control Value Accessor | Jennifer Wadella
- Understanding Angular's Control Value Accessor Interface
- Galaxy Rating App
- How to PROPERLY implement ControlValueAccessor - Angular Form
- Angular Custom Form Controls: Complete Guide
Details of CVA (Control Value Accessor)
writeValue
: called when the form control is instantiated, when setValue, or patchValue are called. Called when programatic changes from model to view are called (outside CVA)
registerOnChange(fn)
: let the parent know a value changed. e.g. name.valueChanges.subscribe
on the parent. onChange is called when the CVA needs to propagate view to model changes upward.
registerOnTouched(fn)
: let the parent know a component was interacted with, like validation. e.g. name.touched
on the parent. Adds the class 'ng-touched'
setDisabledState
: optional called when the form is instantiated if the disabled key is present and when .enabled() or .disabled() are called.
Tips (from Jennifer's talk)
- Keep wrapper components dumb
- Just input/output form values
- Leave validation logic to the parent form component
- CVA can be used with any form API
Implementation
-
Add a lookup to the NgControl to the custructor of your custom control.
NOTE: Alternate approach is using
NG_VALUE_ACCESSOR
// @Optional is required for unit testing to work constructor(@Optional() @Self() public controlDir: NgControl) { controlDir.valueAccessor = this; }
-
Implement the ControlValueAccessor interface
@Component({ standalone: true, imports: [CommonModule, ReactiveFormsModule, FormsModule], providers: [], selector: "my-custom-field", styleUrls: ["./my-custom-field.component.scss"], templateUrl: "./my-custom-field.component.html", changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class MyCustomFieldComponent implements ControlValueAccessor, OnInit { disabled: boolean; // if you need special handling use a getter/setter _value: string; // ... constructor from first step onChanged = (v: string): void => {}; onTouched = (): void => {}; writeValue(obj: any): void { this._value = value; } registerOnChange(fn: (v: string) => void): void { this.onChanged = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } sweetChange($event) { this.onTouched(); this.onChanged($event.currentTarget.value); } }
<input (change)="sweetChange($event)" type="text" />
-
To add validators to custom field
ngOnInit(): void { const control = this.controlDir.control let validators = control.validator ? [control.validator, Validators.required] : Validators.required control.setValidators(validators) control.updateValueAndValidity() }
-
Bind to the reactive form with
formControlName
and configuring a formGroup.<form [formGroup]="myForm"> <my-custom-field formControlName="dynamicText"></my-custom-field> </form>
@Component({ standalone: true, imports: [ CommonModule, ReactiveFormsModule, MyCustomFieldComponent ], selector: 'my-form', styleUrls: ['./my-form.component.scss'], templateUrl: './my-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class MyFormComponent implements OnInit { myForm: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit() { this.myForm: FormGroup = this.formBuilder.group( { name: ['', [Validators.required]], dynamicText: ['', [Validators.required]] }, { validators: customFormValidator } } }
Issues
Infinite loop with writeValue and set value
-
Don't call onChange from the value setter
-
How to PROPERLY implement ControlValueAccessor - Angular Form