Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement ngModel on custom elements?

People also ask

How do you create a custom input with ngModel working in Angular 6?

Quick explanation: The trick is to add the provider NG_VALUE_ACCESSOR on the decorator of the class and implement ControlValueAccessor . Then we need to define the functions writeValue , registerOnChange and registerOnTouched . The two latter are directly called on creation of the component.

Can I use ngModel without form?

standalone: When set to true, the ngModel will not register itself with its parent form, and acts as if it's not in the form. Defaults to false. If no parent form exists, this option has no effect. updateOn: Defines the event upon which the form control value and validity update.


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


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

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>