Angular

Custom Form Controls

Sometimes, when we decide to build a little bit complex form we realize that basic form controls are insufficient.
For example, let’s imagine you have to build a form that allows user to input his data. You want to avoid code multiplication that’s why you would like to move input and it’s label to a seperate component. Instead of using the following structure:

<label for="name">Name:</label>   
<input type="text"
       name="name"
       [(ngModel)]="model.data" >

you would like to use something like this:

<my-custom-control 
[label]="'Name'" 
[(ngModel)]="model.name"
name="name">
</my-custom-control>

To do that, start with creating MyCustomControlComponent:

my-custom-control.component.ts

import { Component, ViewChild, Input } from '@angular/core';
import { NgForm, NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'my-custom-control',
  templateUrl: './my-custom-control.component.html',
  providers: [
    {
        provide: NG_VALUE_ACCESSOR, 
        useExisting: MyCustomControlComponent,
        multi: true
    }
  ]

})
export class MyCustomControlComponent implements ControlValueAccessor {

    @Input() label: string
    @ViewChild('in') inp: HTMLInputElement;

    private data :any;
    private propagateChange = (a: any) => {
    };

    private change(){
        this.propagateChange(this.data);
    }

    writeValue(obj: any): void {
       this.data = obj;
    }

    registerOnChange(f: any): void {
         this.propagateChange = f;
    }

    registerOnTouched(f: any): void {   
    }

    setDisabledState(isDisabled: boolean): void {
       this.inp.disabled=isDisabled;
    }
}

my-custom-control.component.html

<label for="label">{{label}}:</label>
<input name="label" #in
class="form-control" 
(input)=change()
[(ngModel)]="data"/>

Under providers, we are extending the NG_VALUE_ACCESSOR and allow to use MyCustomControlComponent when necessary.
NG_VALUE_ACCESSOR token can have more than one registered providers with the same component, that’s why we set multi to true.
MyCustomControlComponent is not yet defined, therefore we have to use forwardRef to allow to refer to our component.
By using useExisting, we do not create a new provider instance.
MyCustomControlComponent has to implement ControlValueAccessor interface, so we have to write our implementation of the following methods:

  • writeValue – used to update internal model with incoming values from the outside
  • registerOnChange – registers a callback function that is called if the value of control changes
  • registerOnTouched – registers a callback function that is called when the input has been touched
  • setDisabledState (optional) – used to enable or disable the appropriate element

The usage of the MyCustomControlComponent could be as follows:

app.component.html

<div class="container">
  <div class="row">
    <div>
      <form (ngSubmit)="onSubmit()">
       
          <div class="form-group">
            <my-custom-control 
            [label]="'First Name'" 
            [(ngModel)]="model.firstname"
            name="firstname">
          </my-custom-control>
           </div>

          <div class="form-group">
            <my-custom-control 
            [label]="'Last Name'" 
            [(ngModel)]="model.lastname"
            name="lastname">
          </my-custom-control>
          </div>
  
        
          <button  
           class="btn btn-default"
           type="submit">
           Send
          </button>   
        </form>
    </div>
  </div>
</div>

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  model = {
    firstname:"Adam",
    lastname: "Nowak"
  }
 
  onSubmit() {
    console.log(this.model)
  }
}
 

Leave a Reply

Your email address will not be published. Required fields are marked *