Last active
February 25, 2022 14:49
-
-
Save jhades/9f1b0a12436085f852ea337ecea90e73 to your computer and use it in GitHub Desktop.
Angular Custom Form Controls: Complete Guide - https://blog.angular-university.io/angular-custom-form-controls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div [formGroup]="form"> | |
<input placeholder="Course title" formControlName="title"> | |
<input type="checkbox" placeholder="Free course" formControlName="free"> | |
<textarea placeholder="Description" formControlName="longDescription"> | |
</textarea> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Directive({ | |
selector: | |
'input[type=checkbox][formControlName], | |
input[type=checkbox][formControl], | |
input[type=checkbox][ngModel]', | |
}) | |
export class CheckboxControlValueAccessor implements ControlValueAccessor { | |
.... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'choose-quantity', | |
template: ` | |
<div class="choose-quantity"> | |
<mat-icon (click)="onRemove()" color="primary">minus_box</mat-icon> | |
<div class="quantity">{{quantity}}</div> | |
<mat-icon (click)="onAdd()" color="primary">add_box</mat-icon> | |
</div> | |
`, | |
styleUrls: ["choose-quantity.component.scss"] | |
}) | |
export class ChooseQuantityComponent { | |
quantity = 0; | |
@Input() | |
increment: number; | |
onAdd() { | |
this.quantity+= this.increment; | |
} | |
onRemove() { | |
this.quantity-= this.increment; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div [formGroup]="form"> | |
<choose-quantity [increment]="10" formControlName="totalQuantity"></choose-quantity> | |
... | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component(...) | |
export class ExampleForms implements OnInit { | |
form = this.fb.group({ | |
... other fields | |
totalQuantity: [60, [Validators.required, Validators.max(100)]] | |
}); | |
constructor(private fb: FormBuilder) {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'choose-quantity', | |
templateUrl: "choose-quantity.component.html", | |
styleUrls: ["choose-quantity.component.scss"], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
multi:true, | |
useExisting: ChooseQuantityComponent | |
} | |
] | |
}) | |
export class ChooseQuantityComponent implements ControlValueAccessor { | |
quantity = 0; | |
@Input() | |
increment: number; | |
onChange = (quantity) => {}; | |
onTouched = () => {}; | |
touched = false; | |
disabled = false; | |
onAdd() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity+= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
onRemove() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity-= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
writeValue(quantity: number) { | |
this.quantity = quantity; | |
} | |
registerOnChange(onChange: any) { | |
this.onChange = onChange; | |
} | |
registerOnTouched(onTouched: any) { | |
this.onTouched = onTouched; | |
} | |
markAsTouched() { | |
if (!this.touched) { | |
this.onTouched(); | |
this.touched = true; | |
} | |
} | |
setDisabledState(disabled: boolean) { | |
this.disabled = disabled; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
writeValue(quantity: number) { | |
this.quantity = quantity; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
onChange = (quantity) => {}; | |
registerOnChange(onChange: any) { | |
this.onChange = onChange; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
onAdd() { | |
this.quantity+= this.increment; | |
this.onChange(this.quantity); | |
} | |
onRemove() { | |
this.quantity-= this.increment; | |
this.onChange(this.quantity); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
registerOnTouched(onTouched: any) { | |
this.onTouched = onTouched; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
touched = false; | |
onAdd() { | |
this.markAsTouched(); | |
this.quantity+= this.increment; | |
this.onChange(this.quantity); | |
} | |
onRemove() { | |
this.markAsTouched(); | |
this.quantity-= this.increment; | |
this.onChange(this.quantity); | |
} | |
markAsTouched() { | |
if (!this.touched) { | |
this.onTouched(); | |
this.touched = true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
disabled = false; | |
onAdd() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity+= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
onRemove() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity-= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
setDisabledState(disabled: boolean) { | |
this.disabled = disabled; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'choose-quantity', | |
templateUrl: "choose-quantity.component.html", | |
styleUrls: ["choose-quantity.component.scss"], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
multi:true, | |
useExisting: ChooseQuantityComponent | |
} | |
] | |
}) | |
export class ChooseQuantityComponent implements ControlValueAccessor { | |
.... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
validate(control: AbstractControl): ValidationErrors | null { | |
const quantity = control.value; | |
if (quantity <= 0) { | |
return { | |
mustBePositive: { | |
quantity | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'choose-quantity', | |
templateUrl: "choose-quantity.component.html", | |
styleUrls: ["choose-quantity.component.scss"], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
multi:true, | |
useExisting: ChooseQuantityComponent | |
}, | |
{ | |
provide: NG_VALIDATORS, | |
multi: true, | |
useExisting: ChooseQuantityComponent | |
} | |
] | |
}) | |
export class ChooseQuantityComponent implements ControlValueAccessor, Validator { | |
.... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'choose-quantity', | |
templateUrl: "choose-quantity.component.html", | |
styleUrls: ["choose-quantity.component.scss"], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
multi:true, | |
useExisting: ChooseQuantityComponent | |
}, | |
{ | |
provide: NG_VALIDATORS, | |
multi: true, | |
useExisting: ChooseQuantityComponent | |
} | |
] | |
}) | |
export class ChooseQuantityComponent implements ControlValueAccessor, Validator { | |
quantity = 0; | |
@Input() | |
increment: number; | |
onChange = (quantity) => {}; | |
onTouched = () => {}; | |
touched = false; | |
disabled = false; | |
onAdd() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity+= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
onRemove() { | |
this.markAsTouched(); | |
if (!this.disabled) { | |
this.quantity-= this.increment; | |
this.onChange(this.quantity); | |
} | |
} | |
writeValue(quantity: number) { | |
this.quantity = quantity; | |
} | |
registerOnChange(onChange: any) { | |
this.onChange = onChange; | |
} | |
registerOnTouched(onTouched: any) { | |
this.onTouched = onTouched; | |
} | |
markAsTouched() { | |
if (!this.touched) { | |
this.onTouched(); | |
this.touched = true; | |
} | |
} | |
setDisabledState(disabled: boolean) { | |
this.disabled = disabled; | |
} | |
validate(control: AbstractControl): ValidationErrors | null { | |
const quantity = control.value; | |
if (quantity <= 0) { | |
return { | |
mustBePositive: { | |
quantity | |
} | |
}; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"max": { | |
"max": 100, | |
"actual": 110 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"mustBePositive": { | |
"quantity": -10 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div [formGroup]="form"> | |
... other form controls | |
<address-form formControlName="address" legend="Address"></address-form> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<fieldset [formGroup]="form"> | |
<legend>{{legend}}</legend> | |
<mat-form-field> | |
<input matInput | |
placeholder="Address Line 1" | |
formControlName="addressLine1" (blur)="onTouched()"> | |
</mat-form-field> | |
<mat-form-field> | |
<input matInput | |
placeholder="Address Line 2" | |
formControlName="addressLine2" (blur)="onTouched()"> | |
</mat-form-field> | |
<mat-form-field> | |
<input matInput | |
placeholder="Zip Code" | |
formControlName="zipCode" (blur)="onTouched()"> | |
</mat-form-field> | |
<mat-form-field> | |
<input matInput | |
placeholder="City" | |
formControlName="city" (blur)="onTouched()"> | |
</mat-form-field> | |
</fieldset> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component({ | |
selector: 'address-form', | |
templateUrl: './address-form.component.html', | |
styleUrls: ['./address-form.component.scss'], | |
providers: [ | |
{ | |
provide: NG_VALUE_ACCESSOR, | |
multi: true, | |
useExisting: AddressFormComponent | |
}, | |
{ | |
provide: NG_VALIDATORS, | |
multi: true, | |
useExisting: AddressFormComponent | |
}, | |
] | |
}) | |
export class AddressFormComponent implements ControlValueAccessor, OnDestroy, Validator { | |
@Input() | |
legend:string; | |
form: FormGroup = this.fb.group({ | |
addressLine1: [null, [Validators.required]], | |
addressLine2: [null, [Validators.required]], | |
zipCode: [null, [Validators.required]], | |
city: [null, [Validators.required]] | |
}); | |
onTouched: Function = () => {}; | |
onChangeSubs: Subscription[] = []; | |
constructor(private fb: FormBuilder) {} | |
ngOnDestroy() { | |
for (let sub of this.onChangeSubs) { | |
sub.unsubscribe(); | |
} | |
} | |
registerOnChange(onChange: any) { | |
const sub = this.form.valueChanges.subscribe(onChange); | |
this.onChangeSubs.push(sub); | |
} | |
registerOnTouched(onTouched: Function) { | |
this.onTouched = onTouched; | |
} | |
setDisabledState(disabled: boolean) { | |
if (disabled) { | |
this.form.disable(); | |
} | |
else { | |
this.form.enable(); | |
} | |
} | |
writeValue(value: any) { | |
if (value) { | |
this.form.setValue(value, {emitEvent: false}); | |
} | |
} | |
validate(control: AbstractControl) { | |
if (this.form.valid) { | |
return null; | |
} | |
let errors : any = {}; | |
errors = this.addControlErrors(errors, "addressLine1"); | |
errors = this.addControlErrors(errors, "addressLine2"); | |
errors = this.addControlErrors(errors, "zipCode"); | |
errors = this.addControlErrors(errors, "city"); | |
return errors; | |
} | |
addControlErrors(allErrors: any, controlName:string) { | |
const errors = {...allErrors}; | |
const controlErrors = this.form.controls[controlName].errors; | |
if (controlErrors) { | |
errors[controlName] = controlErrors; | |
} | |
return errors; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
... other form control values ... | |
"address": { | |
"addressLine1": "Street name 1", | |
"addressLine2": "Street name 2", | |
"zipCode": "1000", | |
"city": "New York" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment