November 2023 State
interface LoginFormModel {
email: FormControl<string | null>;
}
@Component({
selector: "app-login",
template: `
<form [formGroup]="loginForm" (submit)="submit()">
<input type="email" formControlName="email">
<input type="submit" value="submit">
</form>
`
})
export class LoginComponent implements OnInit {
loginForm: FormGroup<LoginFormModel>;
ngOnInit() {
this.loginForm = new FormGroup({
email: new FormControl()
});
}
submit() {
console.log(this.loginForm.value);
}
}
see angular.io
@Component({
selector: "app-rate",
template: `
<form (submit)="submit()">
Rate this training: <app-star-input (changeRating)="rating = $event"></app-star-input>
<input type="submit" value="submit">
</form>
`
})
export class RateComponent implements OnInit {
rating: number;
ngOnInit() {
}
submit() {
console.log(this.rating);
}
}
interface RatingFormModel {
rating: FormControl<number | null>;
}
@Component({
selector: "app-rate",
template: `
<form [formGroup]="ratingForm" (submit)="submit()">
Rate this training: <app-star-input formControlName="rating"></app-star-input>
<input type="submit" value="submit">
</form>
`
})
export class RateComponent implements OnInit {
ratingForm: FormGroup<RatingFormModel>;
ngOnInit() {
this.ratingForm = new FormGroup({
rating: new FormControl()
});
}
submit() {
console.log(this.ratingForm.value);
}
}
interface ControlValueAccessor {
// is the method that writes a new value from the form model
// into the view or (if needed) DOM property
writeValue(obj: any)
// is a method that registers a handler that should be called
// when something in the view has changed
registerOnChange(fn: any)
// it registers a handler specifically for when a control receives
// a touch event
registerOnTouched(fn: any)
// is called when we disable or enable the form control. Can be used
// to propagate the change to child components
setDisabledState?(isDisabled: boolean): void;
}
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomComponent),
multi: true
}
]
@Component({
selector: 'app-rating',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingComponent),
multi: true
}
],
templateUrl: './rating.component.html'
})
export class RatingComponent implements ControlValueAccessor {
rating = 0;
isDisabled = false;
private onChange = (rating: number) => {};
private onTouched = () => {};
writeValue(rating: number) {
this.rating = rating;
}
registerOnChange(fn: (rating: number) => void) {
this.onChange = fn;
}
registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.isDisabled = isDisabled;
}
}
We want to refactor the RatingComponent to implement the CVA interface.
fallback branch: rating/2-implement-cva
Add validation & opt-out
fallback branch: rating/3-validation-opt-out
Reuse the custom control
fallback branch: rating/4-reusability
Add custom validation
final solution branch: rating/5-custom-validation
For Re-validation, the validators will need to be on the top-level form, not at the child component, if you want it to be part of the parent forms validation.
interface Validator {
// is the method that is called for validation
validate(control: AbstractControl): ValidationErrors | null
}
providers: [
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CustomComponent),
multi: true
}
]
@Component({
selector: 'app-address',
providers: [
{ /* CVA provider */ },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AddressComponent),
multi: true
}
],
templateUrl: './address.component.html'
})
export class AddressComponent implements ControlValueAccessor, Validator {
addressForm: FormGroup<AddressFormModel>;
validate(): ValidationErrors | null {
return addressForm.valid ? null : { invalidAddressData: true };
}
// CVA implementation starts here...
}
fallback branch: nested/1-including-address
fallback branch: nested/2-extracted-address
fallback branch: nested/3-basic-cva-implementation
fallback branch: nested/4-introducing-delivery-and-invoice-address
solution branch: nested/5-added-validator