Skip to content

Instantly share code, notes, and snippets.

@jhades
Last active February 25, 2022 14:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jhades/9f1b0a12436085f852ea337ecea90e73 to your computer and use it in GitHub Desktop.
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
<div [formGroup]="form">
<input placeholder="Course title" formControlName="title">
<input type="checkbox" placeholder="Free course" formControlName="free">
<textarea placeholder="Description" formControlName="longDescription">
</textarea>
</div>
@Directive({
selector:
'input[type=checkbox][formControlName],
input[type=checkbox][formControl],
input[type=checkbox][ngModel]',
})
export class CheckboxControlValueAccessor implements ControlValueAccessor {
....
}
@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;
}
}
<div [formGroup]="form">
<choose-quantity [increment]="10" formControlName="totalQuantity"></choose-quantity>
...
</div>
@Component(...)
export class ExampleForms implements OnInit {
form = this.fb.group({
... other fields
totalQuantity: [60, [Validators.required, Validators.max(100)]]
});
constructor(private fb: FormBuilder) {}
}
@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;
}
}
writeValue(quantity: number) {
this.quantity = quantity;
}
onChange = (quantity) => {};
registerOnChange(onChange: any) {
this.onChange = onChange;
}
onAdd() {
this.quantity+= this.increment;
this.onChange(this.quantity);
}
onRemove() {
this.quantity-= this.increment;
this.onChange(this.quantity);
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
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;
}
}
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;
}
@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 {
....
}
validate(control: AbstractControl): ValidationErrors | null {
const quantity = control.value;
if (quantity <= 0) {
return {
mustBePositive: {
quantity
}
};
}
}
@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 {
....
}
@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
}
};
}
}
}
{
"max": {
"max": 100,
"actual": 110
}
}
{
"mustBePositive": {
"quantity": -10
}
}
<div [formGroup]="form">
... other form controls
<address-form formControlName="address" legend="Address"></address-form>
</div>
<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>
@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;
}
}
{
... 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