2017-08-12

Angular - How to create a data aware custom component

Introduction

This blog will demonstrate how to create an Angular component that you are able to add [ngModel] [formControl] and [formControlName] attributes to your custom component, and have your component correct implement the features required to work with Angular forms.

Setting up the ngModule

First add FormsModule and ReactiveFormsModule to your main NgModule's import declaration
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Creating the custom component's template

Note: If you have installed @angular/cli using npm you can type ng g component /components/custom-input in a command prompt to create the component + template + test cases.
Next create a new component with the following html template
My custom input 
<input 
  (blur)="blurred()" 
  (focus)="focused()" 
  [(ngModel)]="value" />

The custom component's class code

First you need to import a few references
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
What these identifiers do:
  • NG_VALUE_ACCESSOR: A marker in the component's providers declaration to indicate that the new component implements ControlValueAccessor
  • ControlValueAccessor: The interface that needs to be implemented in order to support data-binding through [formControl] and [formControlName]
Now update the component's @Component declaration and add providers, and update the selector.
@Component({
  selector: '[formControl] custom-input, [formControlName] custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})

And finally, the component's class code

export class CustomInputComponent implements ControlValueAccessor {
  private _value: any;

  private hasHadFocus = false;
  private hasNotifiedTouched = false;
  private propagateChange: any = () => {};
  private propogateTouched: any = () => {};

  public get value(): any {
    return this._value;
  }

  @Input('value')
  public set value(value: any) {
    this._value = value;
    this.propagateChange(value);
  }

  /**
   * Called when input (focus) is triggered
   */
  public focused() {
    this.hasHadFocus = true;
  }

  /**
   * Called when input (blur) is triggered
   */
  public blurred() {
    if (this.hasHadFocus && !this.hasNotifiedTouched) {
      this.hasNotifiedTouched = true;
      this.propogateTouched();
    }
  }

  /**
   * Called when a new value is set via code
   * @param obj
   */
  writeValue(value: any): void {
    this.value = value;
  }

  /**
   * Register Angular's call back to execute when our value changes
   * @param fn
   */
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  /**
   * Register Angular's call back to execute when our value is first touched
   * @param fn
   */
  registerOnTouched(fn: any): void {
    this.propogateTouched = fn;
  }
}

No comments: