108

How can I create custom component which would work just like native <input> tag? I want to make my custom form control be able to support ngControl, ngForm, [(ngModel)].

As I understand, I need to implement some interfaces to make my own form control work just like native one.

Also, seems like ngForm directive binds only for <input> tag, is this right? How can i deal with that?


Let me explain why I need this at all. I want to wrap several input elements to make them able to work together as one single input. Is there other way to deal with that? One more time: I want to make this control just like native one. Validation, ngForm, ngModel two way binding and other.

ps: I use Typescript.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Maksim Fomin
  • 1,227
  • 2
  • 10
  • 8
  • 1
    Most answers are outdated regarding current Angular versions. Have a look at https://stackoverflow.com/a/41353306/2176962 – hgoebl Dec 31 '17 at 10:31

7 Answers7

131

I don't understand why every example I find on the internet has to be so complicated. When explaining a new concept, I think it's always best to have the most simple, working example possible. I've distilled it down a little bit:

HTML for external form using component implementing ngModel:

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>

Self-contained component (no separate 'accessor' class - maybe I'm missing the point):

import {Component, Provider, forwardRef, Input} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR, CORE_DIRECTIVES} from "@angular/common";

const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = new Provider(
  NG_VALUE_ACCESSOR, {
    useExisting: forwardRef(() => InputField),
    multi: true
  });

@Component({
  selector : 'inputfield',
  template: `<input [(ngModel)]="value">`,
  directives: [CORE_DIRECTIVES],
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class InputField implements ControlValueAccessor {
  private _value: any = '';
  get value(): any { return this._value; };

  set value(v: any) {
    if (v !== this._value) {
      this._value = v;
      this.onChange(v);
    }
  }

    writeValue(value: any) {
      this._value = value;
      this.onChange(value);
    }

    onChange = (_) => {};
    onTouched = () => {};
    registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
    registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

In fact, I've just abstracted all of this stuff to an abstract class which I now extend with every component I need to use ngModel. For me this is a ton of overhead and boilerplate code which I can do without.

Edit: Here it is:

import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export abstract class AbstractValueAccessor implements ControlValueAccessor {
    _value: any = '';
    get value(): any { return this._value; };
    set value(v: any) {
      if (v !== this._value) {
        this._value = v;
        this.onChange(v);
      }
    }

    writeValue(value: any) {
      this._value = value;
      // warning: comment below if only want to emit on user intervention
      this.onChange(value);
    }

    onChange = (_) => {};
    onTouched = () => {};
    registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
    registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

export function MakeProvider(type : any){
  return {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => type),
    multi: true
  };
}

Here's a component that uses it: (TS):

import {Component, Input} from "@angular/core";
import {CORE_DIRECTIVES} from "@angular/common";
import {AbstractValueAccessor, MakeProvider} from "../abstractValueAcessor";

@Component({
  selector : 'inputfield',
  template: require('./genericinput.component.ng2.html'),
  directives: [CORE_DIRECTIVES],
  providers: [MakeProvider(InputField)]
})
export class InputField extends AbstractValueAccessor {
  @Input('displaytext') displaytext: string;
  @Input('placeholder') placeholder: string;
}

HTML:

<div class="form-group">
  <label class="control-label" >{{displaytext}}</label>
  <input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md">
</div>
Daniel Patrick
  • 3,980
  • 6
  • 29
  • 49
David
  • 15,652
  • 26
  • 115
  • 156
  • 1
    Interestingly, the accepted answer seems to have stopped working since RC2, i tried this approach and it works, not sure why though. – 3urdoch Jun 17 '16 at 14:00
  • Care to share your abstract base class? – 3urdoch Jun 17 '16 at 14:02
  • 1
    @3urdoch Sure, one sec – David Jun 17 '16 at 14:09
  • Is a very useful and portable solution, in fact is better than copy and paste all the empty functions in our class. There is a [Plunker](https://plnkr.co/edit/KUORijCXNuFvALk9Tr3I?p=preview) if someone want play whit this :) – Koronos Jun 22 '16 at 02:28
  • It's always better to avoid using copy paste as a design pattern haha – David Jun 22 '16 at 07:24
  • @David Sorry David,but i'm trying to make a custom component work with the new angular's implementation form, the which you can see [here](https://docs.google.com/document/u/1/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/pub) and i cannot find a way to make it work, do you know a way to accomplished this. Thanks in advance – Koronos Jun 22 '16 at 17:47
  • Do you have a plunkr? – David Jun 23 '16 at 09:17
  • 1
    This method doesn't seem to work with new forms(0.1.1) – Alexey Vagarenko Jun 24 '16 at 12:03
  • 6
    To make it work with new `@angular/forms` just update imports: `import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'` – ulfryk Jun 27 '16 at 08:24
  • Kara posted a similar example for the new forms in https://github.com/angular/angular/issues/9568#issuecomment-228381646 [**Plunker example**](https://plnkr.co/edit/9XogrULe6atg4WMOCiqS?p=preview) – Günter Zöchbauer Jun 27 '16 at 09:02
  • @GünterZöchbauer the plunker is also in the first reply in that ticket – David Jun 27 '16 at 09:52
  • 1
    @David I'm quite new at the angular 2 table and have honestly not spend alot of time with angular 1. However I've already fallen in love with NG2. I have adopted your abstract class into my own library and added your name and LinkedIn profile in the comments to give you credit. If you care to have anything else added in there, please let me know :) – Pwnball Jun 29 '16 at 10:58
  • this was really helpful, now i have to deal with checkboxes,radio,dropdowns and textarea with the same scenario, any help would be be appreciated... – Pratik Kelwalkar Aug 12 '16 at 08:42
  • 1
    @David thanks for this, as well as your abstract class. The only issue is that for some reason, when I use your MakeProvider function it does not work in RC.5 Everything works when I use the CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR from the previous example. Also, since there's such poor documentation on this, I created an article and gave you attribtution. https://medium.com/@paynoattn/custom-angular2-form-components-using-formbuilder-bb9068cd4057#.cy2asay01 – Chris Pawlukiewicz Sep 14 '16 at 18:05
  • 6
    Provider() is not supported in Angular2 Final. Instead, have MakeProvider() return { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true }; – DSoa Nov 15 '16 at 21:43
  • Is anyone else having trouble with getting changes using this code? – Ash McConnell Feb 14 '17 at 14:33
  • 3
    You don't need to import `CORE_DIRECTIVES` and add them in the `@Component` anymore since they are provided by default now since Angular2 final. However, according to my IDE, "Constructors for derived classes must contain a 'super' call.", so I had to add `super();` to my component's constructor. – Joseph Webber Jan 19 '18 at 15:49
  • Any reason why AbstractValueAccessor isn't a generic class where T is the value? – Staeff Feb 20 '18 at 10:58
  • 1
    It took me 2 days to find out why my dirty/pristine state and reset handling were being buggy, the reason is that you should never call `onChange` from your `writeValue` function, because this will mess up the state handling of the controlling form. – Staeff Feb 22 '18 at 14:37
  • @ChrisPawlukiewicz finally it worked with me without using CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR just replace it with NG_VALUE_ACCESSOR – Mahmoud Hboubati Dec 07 '18 at 18:12
  • @David Thank you! I owe you a beverage of your choice. Working great in Angular 8+ – Tyler Jun 21 '19 at 18:59
  • 1
    Great answer! For the people having trouble, CORE_DIRECTIVES is now part of BrowserModule so you don't need to import that anymore. – Sinan Samet Dec 12 '19 at 07:56
103

In fact, there are two things to implement:

  • A component that provides the logic of your form component. It doesn't need an input since it will be provided by ngModel itself
  • A custom ControlValueAccessor that will implement the bridge between this component and ngModel / ngControl

Let's take a sample. I want to implement a component that manages a list of tags for a company. The component will allow to add and remove tags. I want to add a validation to ensure that the tags list isn't empty. I will define it in my component as described below:

(...)
import {TagsComponent} from './app.tags.ngform';
import {TagsValueAccessor} from './app.tags.ngform.accessor';

function notEmpty(control) {
  if(control.value == null || control.value.length===0) {
    return {
      notEmpty: true
    }
  }

  return null;
}

@Component({
  selector: 'company-details',
  directives: [ FormFieldComponent, TagsComponent, TagsValueAccessor ],
  template: `
    <form [ngFormModel]="companyForm">
      Name: <input [(ngModel)]="company.name"
         [ngFormControl]="companyForm.controls.name"/>
      Tags: <tags [(ngModel)]="company.tags" 
         [ngFormControl]="companyForm.controls.tags"></tags>
    </form>
  `
})
export class DetailsComponent implements OnInit {
  constructor(_builder:FormBuilder) {
    this.company = new Company('companyid',
            'some name', [ 'tag1', 'tag2' ]);
    this.companyForm = _builder.group({
       name: ['', Validators.required],
       tags: ['', notEmpty]
    });
  }
}

The TagsComponent component defines the logic to add and remove elements in the tags list.

@Component({
  selector: 'tags',
  template: `
    <div *ngIf="tags">
      <span *ngFor="#tag of tags" style="font-size:14px"
         class="label label-default" (click)="removeTag(tag)">
        {{label}} <span class="glyphicon glyphicon-remove"
                        aria-  hidden="true"></span>
      </span>
      <span>&nbsp;|&nbsp;</span>
      <span style="display:inline-block;">
        <input [(ngModel)]="tagToAdd"
           style="width: 50px; font-size: 14px;" class="custom"/>
        <em class="glyphicon glyphicon-ok" aria-hidden="true" 
            (click)="addTag(tagToAdd)"></em>
      </span>
    </div>
  `
})
export class TagsComponent {
  @Output()
  tagsChange: EventEmitter;

  constructor() {
    this.tagsChange = new EventEmitter();
  }

  setValue(value) {
    this.tags = value;
  }

  removeLabel(tag:string) {
    var index = this.tags.indexOf(tag, 0);
    if (index !== -1) {
      this.tags.splice(index, 1);
      this.tagsChange.emit(this.tags);
    }
  }

  addLabel(label:string) {
    this.tags.push(this.tagToAdd);
    this.tagsChange.emit(this.tags);
    this.tagToAdd = '';
  }
}

As you can see, there is no input in this component but a setValue one (the name isn't important here). We use it later to provide the value from the ngModel to the component. This component defines an event to notify when the state of the component (the tags list) is updated.

Let's implement now the link between this component and ngModel / ngControl. This corresponds to a directive that implements the ControlValueAccessor interface. A provider must be defined for this value accessor against the NG_VALUE_ACCESSOR token (don't forget to use forwardRef since the directive is defined after).

The directive will attach an event listener on the tagsChange event of the host (i.e. the component the directive is attached on, i.e. the TagsComponent). The onChange method will be called when the event occurs. This method corresponds to the one registered by Angular2. This way it will be aware of changes and updates accordingly the associated form control.

The writeValue is called when the value bound in the ngForm is updated. After having injected the component attached on (i.e. TagsComponent), we will be able to call it to pass this value (see the previous setValue method).

Don't forget to provide the CUSTOM_VALUE_ACCESSOR in the bindings of the directive.

Here is the complete code of the custom ControlValueAccessor:

import {TagsComponent} from './app.tags.ngform';

const CUSTOM_VALUE_ACCESSOR = CONST_EXPR(new Provider(
  NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => TagsValueAccessor), multi: true}));

@Directive({
  selector: 'tags',
  host: {'(tagsChange)': 'onChange($event)'},
  providers: [CUSTOM_VALUE_ACCESSOR]
})
export class TagsValueAccessor implements ControlValueAccessor {
  onChange = (_) => {};
  onTouched = () => {};

  constructor(private host: TagsComponent) { }

  writeValue(value: any): void {
    this.host.setValue(value);
  }

  registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

This way when I remove all the tags of the company, the valid attribute of the companyForm.controls.tags control becomes false automatically.

See this article (section "NgModel-compatible component") for more details:

glinda93
  • 7,659
  • 5
  • 40
  • 78
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thanks! You're awesome! How you think - is this way actually fine? I mean: don't use input elements and make own controlls like: ``, ``? Is this "angular" way? – Maksim Fomin Jan 26 '16 at 04:49
  • Forgot to tag you, so @thierry-templier (how this works? huh?) – Maksim Fomin Jan 26 '16 at 11:20
  • 1
    I would say if you want to implement your own field in the form (something custom), use this approach. Otherwise use native HTML elements. That said if you want to modularize the way to display input / textarea / select (for example with Bootstrap3), you can leverage ng-content. See this answer: http://stackoverflow.com/questions/34950950/angular-2-template-component/34951011#34951011 – Thierry Templier Jan 26 '16 at 15:30
  • 3
    The above is missing code and has some discrepancies, like 'removeLabel' instead of 'removeLabel'. See [here](https://github.com/baloodevil/angular2-custom-form-control) for a complete working example. Thanks Thierry for putting the initial example out there! – Blue Mar 29 '16 at 17:22
  • Angular seems to call registerOnChange and registerOnTouched with it's own methods. And the touched and changed properties on the control is always false. – Sheldon Cooper May 24 '16 at 13:50
  • Great example but not working anymore with Angular RC2, any ideas on what needs to change to support RC2? – Cagatay Civici Jun 18 '16 at 11:07
  • 1
    Found it, import from @angular/forms instead of @angular/common and it works. import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; – Cagatay Civici Jun 18 '16 at 11:38
  • 1
    [this](http://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html) link should also be helpful.. – refactor Oct 20 '16 at 11:37
  • This is a magnificent example how to separate an input component from ngModel dependant one! I love it. But I would likely set directive's selector to `tags[ngModel]` so this directive will only be instantiated when it really needs to be used. – Robert Koritnik Feb 15 '18 at 11:35
  • that's a nice approach, but as the example is a bit outdated, i took the liberty to reimplement it based on angular 5+ https://github.com/dandohotaru/labs.angles/tree/angle001 – Dan Dohotaru May 02 '18 at 15:10
19

There's an example in this link for RC5 version: http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

const noop = () => {
};

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
};

@Component({
    selector: 'custom-input',
    template: `<div class="form-group">
                    <label>
                        <ng-content></ng-content>
                        <input [(ngModel)]="value"
                                class="form-control"
                                (blur)="onBlur()" >
                    </label>
                </div>`,
    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class CustomInputComponent implements ControlValueAccessor {

    //The internal data model
    private innerValue: any = '';

    //Placeholders for the callbacks which are later providesd
    //by the Control Value Accessor
    private onTouchedCallback: () => void = noop;
    private onChangeCallback: (_: any) => void = noop;

    //get accessor
    get value(): any {
        return this.innerValue;
    };

    //set accessor including call the onchange callback
    set value(v: any) {
        if (v !== this.innerValue) {
            this.innerValue = v;
            this.onChangeCallback(v);
        }
    }

    //Set touched on blur
    onBlur() {
        this.onTouchedCallback();
    }

    //From ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    //From ControlValueAccessor interface
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    //From ControlValueAccessor interface
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

}

We are then able to use this custom control as follows:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
superjos
  • 12,189
  • 6
  • 89
  • 134
Dániel Kis
  • 191
  • 1
  • 4
  • 4
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. – Maximilian Ast Aug 22 '16 at 09:12
6

Thierry's example is helpful. Here are the imports that are needed for TagsValueAccessor to run...

import {Directive, Provider} from 'angular2/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR } from 'angular2/common';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {forwardRef} from 'angular2/src/core/di';
Blue
  • 728
  • 8
  • 9
2

I wrote a library that helps reduce some boilerplate for this case: s-ng-utils. Some of the other answers are giving example of wrapping a single form control. Using s-ng-utils that can be done very simply using WrappedFormControlSuperclass:

@Component({
    template: `
      <!-- any fancy wrapping you want in the template -->
      <input [formControl]="formControl">
    `,
    providers: [provideValueAccessor(StringComponent)],
})
class StringComponent extends WrappedFormControlSuperclass<string> {
  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }
}

In your post you mention that you want to wrap multiple form controls into a single component. Here is a full example doing that with FormControlSuperclass.

import { Component, Injector } from "@angular/core";
import { FormControlSuperclass, provideValueAccessor } from "s-ng-utils";

interface Location {
  city: string;
  country: string;
}

@Component({
  selector: "app-location",
  template: `
    City:
    <input
      [ngModel]="location.city"
      (ngModelChange)="modifyLocation('city', $event)"
    />
    Country:
    <input
      [ngModel]="location.country"
      (ngModelChange)="modifyLocation('country', $event)"
    />
  `,
  providers: [provideValueAccessor(LocationComponent)],
})
export class LocationComponent extends FormControlSuperclass<Location> {
  location!: Location;

  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }

  handleIncomingValue(value: Location) {
    this.location = value;
  }

  modifyLocation<K extends keyof Location>(field: K, value: Location[K]) {
    this.location = { ...this.location, [field]: value };
    this.emitOutgoingValue(this.location);
  }
}

You can then use <app-location> with [(ngModel)], [formControl], custom validators - everything you can do with the controls Angular supports out of the box.

Eric Simonton
  • 5,702
  • 2
  • 37
  • 54
-1

Why to create a new value accessor when you can use the inner ngModel. Whenever you are creating a custom component which has an input[ngModel] in it, we already are instantiating an ControlValueAccessor. And that's the accessor we need.

template:

<div class="form-group" [ngClass]="{'has-error' : hasError}">
    <div><label>{{label}}</label></div>
    <input type="text" [placeholder]="placeholder" ngModel [ngClass]="{invalid: (invalid | async)}" [id]="identifier"        name="{{name}}-input" />    
</div>

Component:

export class MyInputComponent {
    @ViewChild(NgModel) innerNgModel: NgModel;

    constructor(ngModel: NgModel) {
        //First set the valueAccessor of the outerNgModel
        this.outerNgModel.valueAccessor = this.innerNgModel.valueAccessor;

        //Set the innerNgModel to the outerNgModel
        //This will copy all properties like validators, change-events etc.
        this.innerNgModel = this.outerNgModel;
    }
}

Use as:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>
Nishant
  • 33
  • 4
-3

This is quite easy to do with ControlValueAccessor NG_VALUE_ACCESSOR.

You can read this article to make a simple custom field Create Custom Input Field Component with Angular

Mustafa Dwaikat
  • 3,392
  • 9
  • 27
  • 41