Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create custom input component with ngModel working in angular 6?

Tags:

angular

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?

like image 409
Arsenii Fomin Avatar asked May 31 '18 06:05

Arsenii Fomin


People also ask

What is [( ngModel )]?

[(ngModel)]="overRideRate" is the short form of [ngModel]="overRideRate" (ngModelChange)="overRideRate = $event" [ngModel]="overRideRate" is to bind overRideRate to the input.value.

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.

Can't bind to ngModel since it isn't a known property of input Ng?

To fix Can't bind to 'ngModel' since it isn't a known property of 'input' error in Angular applications we have to import FormModule in app. module. ts file. If you are using FormBuilder class to create reactive form we have to import ReactiveFormsModule as well to avoid below error.


1 Answers

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"
like image 156
andreas Avatar answered Sep 18 '22 13:09

andreas