How to implement ngModel on custom elements?

AngularAngular2 TemplateAngular2 Directives

Angular Problem Overview


Given a simple input element I can do this: {{ name }}

This doesn't work for my custom elements:

How can I implement it?

Angular Solutions


Solution 1 - Angular

[(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

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.

Solution 2 - Angular

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

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...

Solution 3 - Angular

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

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();
	}
}

Solution 4 - Angular

Step 1: Add the providers property below:

@Component({
    selector: 'my-cool-element',
    templateUrl: './MyCool.component.html',
    styleUrls: ['./MyCool.component.css'],
    providers: [{   // <================================================ ADD THIS
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => MyCoolComponent),
        multi: true
    }]
})

Step 2: Implement ControlValueAccessor:

    export class MyCoolComponent implements ControlValueAccessor {
    
      private _value: string;
      // Whatever name for this (myValue) you choose here, use it in the .html file.
      public get myValue(): string { return this._value }
      public set myValue(v: string) {
        if (v !== this._value) {     
          this._value = v;
          this.onChange(v);
        }
      }
    
      constructor() {}
    
      onChange = (_) => { };
      onTouched = () => { };
    
      writeValue(value: any): void {    
        this.myValue = value;
      }
      registerOnChange(fn: any): void {
        this.onChange = fn;
      }
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
      setDisabledState?(isDisabled: boolean): void {
        throw new Error("Method not implemented.");
      }
    
    }

Step 3: In the html, bind whatever control you want to myValue:


    <my-cool-element [(value)]="myValue">
              <!-- ..... -->
     </my-cool-element>

Solution 5 - Angular

You can implement a custom two-way binding yourself. For angular 10, see the official example SizerComponent, here the [(size)] behaves just like the [(ngModel)]:

<app-sizer [(size)]="fontSizePx"></app-sizer>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser1563700View Question on Stackoverflow
Solution 1 - AngularTezraView Answer on Stackoverflow
Solution 2 - AngularThierry TemplierView Answer on Stackoverflow
Solution 3 - AngularShlomi AharoniView Answer on Stackoverflow
Solution 4 - AngularA-SharabianiView Answer on Stackoverflow
Solution 5 - AngularIcebergView Answer on Stackoverflow