How to implement ngModel on custom elements?
AngularAngular2 TemplateAngular2 DirectivesAngular 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 andngModel
/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:
-
providers: [createCustomInputControlValueAccessor(MyInputComponent)]
-
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>