How to implement ngModel on custom elements?

I implemented the ngModel one time for input in my shared components and from then I can extend it very simple.

Only two lines of code:

  1. providers: [createCustomInputControlValueAccessor(MyInputComponent)]

  2. extends InputComponent

my-input.component.ts

import { Component, Input } from '@angular/core';
import { InputComponent, createCustomInputControlValueAccessor } from '../../../shared/components/input.component';
@Component({
   selector: 'my-input',
   templateUrl: './my-input-component.component.html',
   styleUrls: ['./my-input-component.scss'],
   providers: [createCustomInputControlValueAccessor(MyInputComponent)]
})
export class MyInputComponent extends InputComponent {
    @Input() model: string;
}

my-input.component.html

<div class="my-input">
    <input [(ngModel)]="model">
</div>

input.component.ts

import { Component, forwardRef, ViewChild, ElementRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
export function createCustomInputControlValueAccessor(extendedInputComponent: any) {
    return {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => extendedInputComponent),
        multi: true
    };
}

@Component({
    template: ''
})
export class InputComponent implements ControlValueAccessor, OnInit {
    @ViewChild('input') inputRef: ElementRef;

    // The internal data model
    public innerValue: any = '';

    // Placeholders for the callbacks which are later provided
    // by the Control Value Accessor
    private onChangeCallback: any;

    // implements ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }
    // implements ControlValueAccessor interface
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    // implements ControlValueAccessor interface - not used, used for touch input
    registerOnTouched() { }

    // change events from the textarea
    private onChange() {
        const input = <HTMLInputElement>this.inputRef.nativeElement;
        // get value from text area
        const newValue = input.value;

        // update the form
        this.onChangeCallback(newValue);
    }
    ngOnInit() {
        const inputElement = <HTMLInputElement>this.inputRef.nativeElement;
        inputElement.onchange = () => this.onChange();
        inputElement.onkeyup = () => this.onChange();
    }
}

[(ngModel)]="item" is a shorthand for [ngModel]="item" (ngModelChange)="item = $event"

That means that if you want to add a 2-way bind property to your component, for example

<app-my-control [(myProp)]="value"></app-my-control>

All you need to do in your component is add

@Input()
myProp: string;

// Output prop name must be Input prop name + 'Change'
// Use in your component to write an updated value back out to the parent
@Output()
myPropChange = new EventEmitter<string>();

The @Input will handle the write ins and to write a new value back out to the parent, just call this.myPropChange.emit("Awesome") (You can put the emit in a setter for your property if you just want to make sure it is updated every time the value changes.)

You can read a more detailed explanation of how/why it works here.


If you want to use the name ngModel (because there are extra directives that bind to elements with ngModel), or this is for a FormControl element rather than a component (AKA, for use in an ngForm), then you will need to play with the ControlValueAccessor. A detailed explanation for making your own FormControl and why it works can be read here.


If you really need [(ngModel)] (which supports ngForm, unlike [(myProp)] approach), I think this link will answer your question:

  • Angular 2 custom form input

We need to implement two things to achieve that:

  • A component that provides the logic of your form component. It doesn't need an input since that will be provided by ngModel itself
  • A custom ControlValueAccessor that will implement the bridge between this component and ngModel / ngControl

The previous link gives you a complete sample...