65

Since I use inputs with a lot of the same directives and .css classes applyed, I want to extract the repeated code to some component like this:

  @Component({
  selector: "app-input",
  template: `
    <div class="...">
      <input type="..." name="..." class="..." [(ngModel)]="value" someDirectives...>
      <label for="...">...</label>
    </div>
  `,
  ...
  })
  export class InputComponent implements OnInit {
    // some implementation connecting external ngModel with internal "value" one
  }

The problem here is creating a component in such a way that it can be used with ngModel as an ordinary input:

<app-input [(ngModel)]="externalValue" ... ></app-input>

I've found several solutions on the internet that can be partially or completely outdated now like: Angular 2 custom form input Can this be done in a better way in angular 6?

Arsenii Fomin
  • 3,120
  • 3
  • 22
  • 42

5 Answers5

115

I came across the same problem some time ago and want to share a minimal example that works with Angular 2+.

For newer Angular versions, there is a simplified approach (scroll down)!


Angular 2+

Assume you would like to use following code anywhere in your app:

<app-input-slider [(ngModel)]="inputSliderValue"></app-input-slider>
  1. Now create a component called InputSlider.

  2. In the input-slider.component.html, add the following:

    <input type="range" [(ngModel)]="value" (ngModelChange)="updateChanges()">
    
  3. Now we have to do some work in the input-slider.component.ts file:

    import {Component, forwardRef, OnInit} from "@angular/core";
    import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
    
    @Component({
        selector: "app-input-slider",
        templateUrl: "./input-slider.component.html",
        styleUrls: ["./input-slider.component.scss"],
        providers: [{
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => InputSliderComponent),
            multi: true
        }]
    
    })
    export class InputSliderComponent implements ControlValueAccessor {
    
     /**
      * Holds the current value of the slider
      */
     value: number = 0;
    
     /**
      * Invoked when the model has been changed
      */
     onChange: (_: any) => void = (_: any) => {};
    
     /**
      * Invoked when the model has been touched
      */
     onTouched: () => void = () => {};
    
     constructor() {}
    
     /**
      * Method that is invoked on an update of a model.
      */
     updateChanges() {
         this.onChange(this.value);
     }
    
     ///////////////
     // OVERRIDES //
     ///////////////
    
     /**
      * Writes a new item to the element.
      * @param value the value
      */
     writeValue(value: number): void {
         this.value = value;
         this.updateChanges();
     }
    
     /**
      * Registers a callback function that should be called when the control's value changes in the UI.
      * @param fn
      */
     registerOnChange(fn: any): void {
         this.onChange = fn;
     }
    
     /**
      * Registers a callback function that should be called when the control receives a blur event.
      * @param fn
      */
     registerOnTouched(fn: any): void {
         this.onTouched = fn;
     }
    

    }

Of course you could add more functionality and value checks using this class, but I hope it will get you some ideas.

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. That is why we need to variables (for example onChange and onTouched - but you can name them however you like.

Finally, we need to define a function that lets the component know to update the underlying ngModel. I did that with the function updateChanges. It needs to be invoked whenever the value changes, either from outside (that's why it is called in writeValue), or from inside (that's why it is called from the html ngModelChange).


Angular 7+

While the first approach still works for newer versions, you might prefer the following version that needs less typing.

In earlier days, you would achieve two-way binding by adding something like this in the outer component:

<app-input-slider [inputSliderValue]="inputSliderValue" (inputSliderValueChange)="inputSliderValue = $event"></app-input-slider>

Angular implemented syntactic sugar for that, so you can now write

<app-input-slider [(inputSliderValue)]="inputSliderValue"></app-input-slider>

if you follow the steps below.

  1. Create a component called InputSlider.

  2. In the input-slider.component.html, add the following:

     <input type="range" [(ngModel)]="inputSliderValue" (ngModelChange)="inputSliderValueChange.emit(inputSliderValue)">
    
  3. Now we have to do some work in the input-slider.component.ts file:

    import {Component, forwardRef, OnInit} from "@angular/core";
    
    @Component({
        selector: "app-input-slider",
        templateUrl: "./input-slider.component.html",
        styleUrls: ["./input-slider.component.scss"],
        providers: []
    })
    export class InputSliderComponent {
    
        /**
         * Holds the current value of the slider
         */
        @Input() inputSliderValue: string = "";
    
        /**
         * Invoked when the model has been changed
         */
        @Output() inputSliderValueChange: EventEmitter<string> = new EventEmitter<string>();
    
     }
    

It is important that the output property (EventEmitter) has the same name than the input property with the appended string Change.


If we compare both approaches, we note the following:

  • The first approach allows you to use [(ngModel)]="propertyNameOutsideTheComponent" as if the component were any form element.
  • Only the first approach lets you directly use validation (for forms).
  • But the first approach needs more coding inside the component class than the second approach
  • The second approach lets you use a two-way binding on your property with the syntax [(propertyNameInsideTheComponent)]="propertyNameOutsideTheComponent"
maidi
  • 3,219
  • 6
  • 27
  • 55
andreas
  • 7,844
  • 9
  • 51
  • 72
  • 1
    According to other examples I've seen it should work. Current solution I use has the next differences in html: (ngModelChange)="onChangeCallback($event)" [ngModel]="innerValue" and in ts: registerOnChange(fn: any): void { this.onChangeCallback = fn; } writeValue(value: any) { if(value !== this.innerValue) { this.innerValue = value; } } so basically I don't call onChange manually. I've encountered one situation when model wasn't updated may be it's related. What do you think? – Arsenii Fomin Jun 14 '18 at 10:48
  • For the Angular 7 version: I use this solution, but this is not working in case when you want to use it this way: `` and you change `someObject` meanwhile (update from service call). – Kristóf Dombi May 30 '19 at 13:30
  • Indeed! This never works, because it will keep the reference to the original object instance and use this val property instead. – andreas May 31 '19 at 00:10
  • YEEESSS! Thank you dude :) – Rui Marques Mar 28 '20 at 17:46
  • Option with VALUE_ACCESSOR is exactly what I needed, thanks! – Konstantin Malikov Jul 27 '20 at 11:39
70

this can also be done like this, when you create a two way binding [()] you can bind it to a function using the same name + 'change' (in our case inputModel and inputModelChange) this way the ngModel will update when you trigger inputModelChange.emit('updatedValue'). and you only need to declare it once inside your component.

app-input.component.ts

import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-input',
  template: `  <input type="text" [(ngModel)]="inputModel" (ngModelChange)="inputModelChange.emit(inputModel)"/>`,
  styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent {
  @Input() inputModel: string;
  @Output() inputModelChange = new EventEmitter<string>();
}

app.component.html

<app-input [(inputModel)]="externalValue"></app-input>
Maxim Pyshko
  • 551
  • 4
  • 14
omer
  • 2,435
  • 2
  • 24
  • 28
  • 4
    Currently I'm using exactly this solution. The only problem is that it's not using ngModel in interface, it is using some other speciail field ("inputModel" in your case). But I've found this drawback to be much less worring than implementing the whole ngModel correctly for the any new shell input or other component I create. – Arsenii Fomin Nov 11 '18 at 17:24
  • 1
    Works for me, Angular.io Version 7.1.5 – David from Studio.201 Dec 26 '18 at 13:34
  • how would you track model change from the parent? – ernerock Sep 03 '19 at 17:05
22

If you don't care about binding your variable by [ngModel] in template-model or [formControl] in reactive-form, you can use omer answer.

Otherwise:

  1. Add NG_VALUE_ACCESSOR injection token into your component definition:

    import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
    
    @Component({
       ...,
       providers: [
         {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AppInputComponent),
            multi: true
         }
       ]
    })
    
  2. Implement ControlValueAccessor interface:

    export class AppInputComponent implements ControlValueAccessor {
    
      writeValue(obj: any): void {
        // Step 3
      }
    
      registerOnChange(fn: any): void {
        this.onChange = fn;
      }
    
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
    
      setDisabledState?(isDisabled: boolean): void {
      }
    
      onChange: any = () => { };
    
      onTouched: any = () => { };
    
    }
    
  3. Manage value when it changes:

    private _value;
    
    public get value(){
      return this._value;
    }
    
    public set value(v){
      this._value = v;
      this.onChange(this._value);
      this.onTouched();
    }
    
    writeValue(obj: any): void {
      this._value = obj;
    }
    
    // Optional
    onSomeEventOccured(newValue){
      this.value = newValue;
    }
    

Now you can use <app-input [(ngModel)]="externalValue" ... ></app-input>

  • 1
    You can use **getter** and **setter** function as a variable. more in this [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) – Pedram A. Keyvani May 18 '19 at 06:30
-4

You can use @Input directive for passing the externalValue into the component and binding with it.

Here's a code:

  @Component({
  selector: "app-input",
  template: `
    <div class="...">
      <input type="..." name="..." class="..." [(ngModel)]="externalValue" someDirectives...>
      <label for="...">...</label>
    </div>
  `,
  })

  export class InputComponent implements OnInit {
     @Input('externalValue') externalValue : any;
  }

And in your parent component, you can use it like:

<app-input [externalValue]="externalValue" ... ></app-input>
Prachi
  • 3,478
  • 17
  • 34
  • Looks like you haven't catched the idea. Passing value is super simple. The question is how to return updated value using onlye [(ngModel)]. I could return updated value by using Output and EventEmitter, but I would have to pass input value and output function to every custom input component I have which increase boilerplate a lot and is not acceptable. – Arsenii Fomin May 31 '18 at 07:04
  • 1
    Have you gone through [this](https://stackoverflow.com/q/35639650/9766215) post? I think this is what you are looking for. – Prachi May 31 '18 at 07:08
  • 1
    [This](https://www.infragistics.com/community/blogs/b/dhananjay_kumar/posts/simplifying-two-way-data-binding-in-angular-2) can also be viewed. See the last example of the article. – Prachi May 31 '18 at 07:10
-6

You can use shareservice which communicate between tow components without use input or output like below

Service

import {Injectable} from '@angular/core';

@Injectable()
export class ShareService {
   public data: string;

   setData(newdata : string){
       this.data = newdata;
   }

   clearData(){
       this.data = '';
   }
}

Component that sets the value

export class PageA {
    constructor(private shareService: ShareService, private router: Router){
    }
    gotoPageB(){
        this.shareService.setData("Sample data");
        this.router.navigate(['pageb']);  
    }

}

Component that gets the value

export class PageB {
    constructor(private shareService: ShareService){ }

    get somedata() : string {
      return this.shareService.data;
    }
}

The key here is to use a getter property in the component that gets the value (PageB in this example) so it is updated any time that the data service value changes.

Vega
  • 27,856
  • 27
  • 95
  • 103
mittal bhatt
  • 955
  • 6
  • 13
  • Sorry, it's completely other solution. I need to use [(ngModel)] on custom component the same way as on ordinary , not just searching for data sharing possibilities. – Arsenii Fomin May 31 '18 at 08:18