Loading...

Attaching ngModel to our components


Custom Form Control

So lets talk a bit about angular...
It's common that we will build custom form elements of our own, and we would like those form elements to be ng modelable meaning we would like to be able to use ngModel directive and 2 way binding on those elements. This tutorial will explain how to create your form control which you can attach to ngModel.

Creating our component

We want to create a custom form control and make angular aware that this is a form control.
We will create a component with a text input (ngModel will be connected to this input)
in order to be able to attach ngModel we need to implement the interface: ControlValueAccessor

ControlValueAccessor

We need to implement some methods from which ngModel will access the value and state of our form control.
with ControlValueAccessor we will need to implement the following methods:

  • writeValue - this method will get the argument which is used to initiate our custom form control.
    In our case the value is a simple string but of course that doesn't need to be the case, you can accept any object just know how you can initialize your form control based on the object.
  • registerOnChange - when there is a change in the custom form component, we will need to call the callback we receive in this function.
    It would be best to save the callback in a private property
  • registerOnTouched - this is a callback we need to call on the blur event

NG_VALUE_ACCESSOR

we need to introduce our custom form element to angular.
We do this through the NG_VALUE_ACCESSOR.
We need to supply a provider for the token: NG_VALUE_ACCESSOR

Custom Validation

We can also create custom validation for our controller, by implementing the interface: Validator.
There is a single method which we need to implement here which is called validate(c: AbstractControl)
This method will return a dictionary with the errors.
After we implement the interface we need to make sure that angular knows that this control has custom validation in it.
We do this by adding the token: NG_VALIDATORS
To our component providers

CustomInputComponent

our custom form control component will look like this:


    import {Component, ElementRef, forwardRef, ViewChild} from '@angular/core';
    import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator} from "@angular/forms";
    import {ValidationErrors} from "@angular/forms/src/directives/validators";
    import {AbstractControl} from "@angular/forms/src/model";
    
    @Component({
        selector: 'app-custom-input',
        template: `
        <div class="form-group">
            <label>Custom control</label>
            <input
            type="text"
            name="custom-control"
            class="form-control"
            (blur)="inputBlurred()"
            #textInput (change)="userTypes($event)" />
        </div>
        `,
        providers: [
        {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true},
        {
            provide: NG_VALIDATORS,
            useExisting: forwardRef(() => CustomInputComponent),
            multi: true,
        }
        ],
    })
    export class CustomInputComponent implements ControlValueAccessor, Validator{
        @ViewChild('textInput') public inputElement: ElementRef;
        private _cb: (_: any) => void;
        private _cbBlurred: any;
    
        writeValue(value: string) {
            this.inputElement.nativeElement.value = value;
        }
    
        registerOnChange(fn: (_: any) => void) {
            this._cb = fn;
        }
    
        userTypes(event) {
            this._cb(event.target.value);
        }
    
        registerOnTouched(fn: any) {
            this._cbBlurred = fn;
        }
    
        inputBlurred() {
            this._cbBlurred();
        }
    
        validate(c: AbstractControl): ValidationErrors | null {
            if (c.value === 'nerdeez') return null;
            return {
                'BAD_INPUT': ['input has to be nerdeez']
            }
        }
    }
  • We create an angular component which implements ControlValueAccessor so ngModel can get our custom control value and state
  • We implement the following methods from ControlValueAccessor: writeValue, registerOnChange, registerOnTouched
  • We create a custom validation by implementing the Validator interface and the validate method.
    If the control doesn't have the text nerdeez it won't be valid
  • We are informing angular that this is a form control with a custom validator by providing the NG_VALUE_ACCESSOR, NG_VALIDATORS
  • The forwardRef is to tell angular the the service is the component class but it is not defined yet.

Using the custom form control

Now in our parent component template, we can do this


    <app-custom-input
        required
        #titleInput="ngModel"
        [(ngModel)]="title" name="title" label="title"></app-custom-input>
    {{titleInput.errors | json}}

notice that we are using ngModel directive, we can add validation like required, we can use the ngModel class properties and methods.

More useful cases

Our custom form control can mean wrapping form control for entire classes, and not just simple form elements like text input.
Let's say we have the following simple todo class


    export class Todo {
      constructor(public title: string, public description: string) {}
    }

We want a custom form control which will accept a todo instance in the ngModel.
This form control can have custom validation on the instance and can change the properties of the instance we pass.
our custom form control will now look like this:


    import {Component, forwardRef} from '@angular/core';
    import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
    import {Todo} from "../todo";

    @Component({
        selector: 'app-todo-form-control',
        template: `
        <input type="text" name="title" [value]="todo?.title"
            (blur)="callBlur()"
            (input)="todo.title=$event.target.value; callChangeCb()" />
        <textarea [value]="todo?.description"
                    (blur)="callBlur()"
                    (input)="todo.description=$event.target.value; callChangeCb()"></textarea>
        `,
        styleUrls: ['./todo-form-control.component.scss'],
        providers: [
        {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TodoFormControlComponent), multi: true}
        ]
    })
    export class TodoFormControlComponent implements ControlValueAccessor{
        public todo: Todo;
        private _changeCb: any;
        private _blurCb: any;

        constructor() { }

        writeValue(obj: Todo): void {
            this.todo = obj;
        }

        registerOnChange(fn: any) {
            this._changeCb = fn;
        }

        registerOnTouched(fn: any) {
            this._blurCb = fn;
        }

        callChangeCb() {
            this._changeCb(this.todo);
        }

        callBlur() {
            this._blurCb();
        }

    }

so in this case our custom control is getting in the ngModel and instance of a class and builds an entire form for that instance.

Summary

With this in mind, start thinking about your company utilities for forms.
Maybe you can already create a FormsModule with your custom form components.