Angular 2 - formControlName inside component

AngularAngular2 Forms

Angular Problem Overview


I want to create a custom input component that I can use with the FormBuilder API. How do I add formControlName inside a component?

Template:

<label class="custom-input__label"
          *ngIf="label">
        {{ label }}
</label>
<input class="custom-input__input" 
       placeholder="{{ placeholder }}"
       name="title" />
<span class="custom-input__message" 
      *ngIf="message">
        {{ message }}
</span>

Component:

import {
    Component,
    Input,
    ViewEncapsulation
} from '@angular/core';

@Component({
    moduleId: module.id,
    selector: 'custom-input',
    host: {
        '[class.custom-input]': 'true'
    },
    templateUrl: 'input.component.html',
    styleUrls: ['input.component.css'],
    encapsulation: ViewEncapsulation.None,
})
export class InputComponent {
    @Input() label: string;
    @Input() message: string;
    @Input() placeholder: string;
}

Usage:

<custom-input label="Title" 
           formControlName="title" // Pass this to input inside the component>
</custom-input>

Angular Solutions


Solution 1 - Angular

You should not be adding formControlName attribute to the input field in the template of your custom component. You should be adding the formControlName on the custom-input element itself as per the best practice.

Here what you can use in your custom-input component is the controlValueAccessor interface to make your custom-input have the value updated whenever there is an event of input field in the template of your custom input changed or blurred.

It provides a connection (to update values or other needs) between the form control behavior of your custom input and the UI you are providing for that custom form control.

Below is the code of a custom input component in TypeScript.

import { Component, Input, forwardRef, AfterViewInit, trigger, state, animate, transition, style, HostListener, OnChanges, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl } from '@angular/forms';

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

@Component({
  selector: 'inv-input',
  templateUrl:'./input-text.component.html',
	styleUrls: ['./input-text.component.css'],
	encapsulation: ViewEncapsulation.None,
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR],
	animations:[trigger(
		'visibilityChanged',[
			state('true',style({'height':'*','padding-top':'4px'})),
			state('false',style({height:'0px','padding-top':'0px'})),
			transition('*=>*',animate('200ms'))
		]
	)]
})

export class InputComponent implements ControlValueAccessor, AfterViewInit, OnChanges {

	// Input field type eg:text,password
	@Input()  type = "text"; 

	// ID attribute for the field and for attribute for the label
	@Input()  idd = ""; 

	// The field name text . used to set placeholder also if no pH (placeholder) input is given
	@Input()  text = ""; 

	// placeholder input
	@Input()  pH:string; 

	//current form control input. helpful in validating and accessing form control
	@Input() c:FormControl = new FormControl(); 

	// set true if we need not show the asterisk in red color
	@Input() optional : boolean = false;

	//@Input() v:boolean = true; // validation input. if false we will not show error message.
	
	// errors for the form control will be stored in this array
	errors:Array<any> = ['This field is required']; 

	// get reference to the input element
	@ViewChild('input')  inputRef:ElementRef; 

	
	constructor() {
		
	}

	ngOnChanges(){
		
	}

	//Lifecycle hook. angular.io for more info
	ngAfterViewInit(){ 
		// set placeholder default value when no input given to pH property      
		if(this.pH === undefined){
			this.pH = "Enter "+this.text; 
		}

		// RESET the custom input form control UI when the form control is RESET
		this.c.valueChanges.subscribe(
			() => {
				// check condition if the form control is RESET
				if (this.c.value == "" || this.c.value == null || this.c.value == undefined) {
					this.innerValue = "";      
					this.inputRef.nativeElement.value = "";                 
				}
			}
		);
	}

   //The internal data model for form control value access
	private innerValue: any = '';

	// event fired when input value is changed . later propagated up to the form control using the custom value accessor interface
	onChange(e:Event, value:any){
		//set changed value
		this.innerValue = value;
		// propagate value into form control using control value accessor interface
		this.propagateChange(this.innerValue);

		//reset errors 
		this.errors = [];
		//setting, resetting error messages into an array (to loop) and adding the validation messages to show below the field area
		for (var key in this.c.errors) {
			if (this.c.errors.hasOwnProperty(key)) {
				if(key === "required"){
					this.errors.push("This field is required");
				}else{
					this.errors.push(this.c.errors[key]);
				}              
			}
		}
	}
		


	//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;
		}
	}

	//propagate changes into the custom form control
	propagateChange = (_: any) => { }
  
	//From ControlValueAccessor interface
	writeValue(value: any) {
		this.innerValue = value;
	}

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

	//From ControlValueAccessor interface
	registerOnTouched(fn: any) {
		
	}
}

Below is the template HTML for the custom input component

<div class="fg">
	  <!--Label text-->
	  <label [attr.for]="idd">{{text}}<sup *ngIf="!optional">*</sup></label>
	  <!--Input form control element with on change event listener helpful to propagate changes -->
	  <input type="{{type}}" #input id="{{idd}}" placeholder="{{pH}}" (blur)="onChange($event, input.value)">
	  <!--Loop through errors-->
	  <div style="height:0px;" [@visibilityChanged]="!c.pristine && !c.valid" class="error">
			<p *ngFor="let error of errors">{{error}}</p>
	  </div>
</div>

Below is custom input component which can be used in a fromGroup or individually

<inv-input formControlName="title" [c]="newQueryForm.controls.title" [optional]="true" idd="title" placeholder="Type Title to search"
		  text="Title"></inv-input>

In this fashion if you implement your custom form controls you can apply your custom validator directives easily and accumulate the errors on that form control to display your errors.

One can imitate the same style to develop custom select component, radio button group, checkbox, textarea, fileupload etc in the above fashion with minor changes as per what the form control's behavior demands.

Solution 2 - Angular

Angular 8 and 9: Use viewProvider in you custom component. Working example:

@Component({
	selector: 'app-input',
	templateUrl: './input.component.html',
	styleUrls: ['./input.component.scss'],
	viewProviders: [
		{
			provide: ControlContainer,
			useExisting: FormGroupDirective
		}
	]
})

Now, when you assign formControlName, your component will attach itself to the parent form.

<input matInput formControlName="{{name}}">

or

<input matInput [formControlName]='name'>

Solution 3 - Angular

The main idea here is that you have to link the FormControl to the FormGroup, this can be done be passing the FormGroup to each input component...

So your input template might look something like the following:

<div [formGroup]="form">
    <label *ngIf="label">{{ label }}</label>
    <input [formControlName]="inputName" />
    <span *ngIf="message">{{ message }}</span>
</div>

Where the @Input's for the input component will be form, label, inputName and message.

It would be used like this:

<form [FormGroup]="yourFormGroup">
    <custom-input
        [form]="yourFormGroup"
        [inputName]="thisFormControlName"
        [message]="yourMessage"
        [label]="yourLabel">
    </custom-input>
</form>

For more info on custom Form Input Components I would recomend taking a look through Angular's Dynamic Forms. Also if you want more information on how to get the @Input and @Output working take a look through the Angular Docs Here

Solution 4 - Angular

I'm solving this in a similar way like web-master-now. But instead of writing a complete own ControlValueAccessor I'm delegating everything to an inner <input> ControlValueAccessor. The result is a much shorter code and I don't have to handle the interaction with the <input> element on my own.

Here is my code

@Component({
  selector: 'form-field',
  template: `    
    <label>
      {{label}}
      <input ngDefaultControl type="text" >
    </label>
    `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => FormFieldComponent),
    multi: true
  }]
})
export class FormFieldComponent implements ControlValueAccessor, AfterViewInit {
  @Input() label: String;
  @Input() formControlName: String;
  @ViewChild(DefaultValueAccessor) valueAccessor: DefaultValueAccessor;

  delegatedMethodCalls = new ReplaySubject<(_: ControlValueAccessor) => void>();

  ngAfterViewInit(): void {
    this.delegatedMethodCalls.subscribe(fn => fn(this.valueAccessor));
  }

  registerOnChange(fn: (_: any) => void): void {
    this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnChange(fn));
  }
  registerOnTouched(fn: () => void): void {
    this.delegatedMethodCalls.next(valueAccessor => valueAccessor.registerOnTouched(fn));
  }

  setDisabledState(isDisabled: boolean): void {
    this.delegatedMethodCalls.next(valueAccessor => valueAccessor.setDisabledState(isDisabled));
  }

  writeValue(obj: any): void {
    this.delegatedMethodCalls.next(valueAccessor => valueAccessor.writeValue(obj));
  }
}

How does it work?

Generally this won't work, as a simpel <input> won't be a ControlValueAccessor without formControlName-directive, which is not allowed in the component due to missing [formGroup], as others already pointed out. However if we look at Angular's code for the DefaultValueAccessor implementation

@Directive({
    selector:
        'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',

    //...
})
export class DefaultValueAccessor implements ControlValueAccessor {

... we can see that there is another attribute selector ngDefaultControl. It's available for a different purpose, but it seems to be supported officially.

A little disadvantage is that the @ViewChild query result with the value accessor will be available not before the ngAfterViewInit handler is called. (It will be available earlier depending on your template, but that's not supported officially .)

That's why I'm buffering all calls we want to delegate to our inner DefaultValueAccessor using a ReplaySubject. A ReplaySubject is an Observable, which buffers all events and emits them on subscription. A normal Subject would throw them away until subscription.

We emit lambda expressions representing the actual call that can be executed later. On ngAfterViewInit we subscribe to our ReplaySubject and simply call the received lambda functions.

I'm sharing two other ideas here, as they are very important for my own projects and it took me a while to work everything out. I see a lot of people having similar problems and use cases, so I hope this is useful for you:

Idea for improvement 1: Provide the FormControl for the view

I replaced the ngDefaultControl by formControl in my project, so we can pass the FormControl instance to the inner <input>. This is not useful by itself, however it is if you are using other directives which interact with FormControls, such as Angular Material's MatInput. E.g. if we replace our form-field template by...

<mat-form-field>
    <input [placeholder]="label" [formControl]="formControl>
    <mat-error>Error!</mat-error>
</mat-form-field> 

...Angular Material is able to automatically show errors set in the form control.

I have to adjust the component in order to pass the form control. I retrieve the form control from our FormControlName directive:

export class FormFieldComponent implements ControlValueAccessor, AfterContentInit {
  // ... see above

  @ContentChild(FormControlName) private formControlNameRef: FormControlName;
  formControl: FormControl;

  ngAfterContentInit(): void {
    this.formControl = <FormControl>this.formControlNameRef.control;
  }

  // ... see above
}

You should also adjust your selector to require the formControlName attribute: selector: 'form-field[formControlName]'.

Idea for improvement 2: Delegate to a more generic value accessor

I replaced the DefaultValueAccessor @ViewChild query by a query for all ControlValueAccessor implementations. This allows other HTML form controls than <input> like <select> and is useful if you want to make your form control type configurable.

@Component({
    selector: 'form-field',
    template: `    
    <label [ngSwitch]="controlType">
      {{label}}
      <input *ngSwitchCase="'text'" ngDefaultControl type="text" #valueAccessor>
      <select *ngSwitchCase="'dropdown'" ngModel #valueAccessor>
        <ng-content></ng-content>
      </select>
    </label>
    `,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => FormFieldComponent),
        multi: true
    }]
})
export class FormFieldComponent implements ControlValueAccessor {
    // ... see above

    @Input() controlType: String = 'text';
    @ViewChild('valueAccessor', {read: NG_VALUE_ACCESSOR}) valueAccessor: ControlValueAccessor;

    // ... see above
}

Usage example:

<form [formGroup]="form">
  <form-field formControlName="firstName" label="First Name"></form-field>
  <form-field formControlName="lastName" label="Last Name" controlType="dropdown">
    <option>foo</option>
    <option>bar</option>
  </form-field>
  <p>Hello "{{form.get('firstName').value}} {{form.get('lastName').value}}"</p>
</form>

A problem with the select above is that ngModel is already deprecated together with reactive forms. Unfortunately there is nothing like ngDefaultControl for Angular's <select> control value accessor. Therefore I suggest to combine this with my first improvement idea.

Solution 5 - Angular

Definitely worth diving deeper into @web-master-now's answer but to simply answer the question you just need the ElementRef to reference the formControlName to the input.

So if you have a simple form

this.userForm = this.formBuilder.group({
  name: [this.user.name, [Validators.required]],
  email: [this.user.email, [Validators.required]]
});

Then your parent component's html would be

<form [formGroup]="userForm" no-validate>
   <custom-input formControlName="name" 
                 // very useful to pass the actual control item
                 [control]="userForm.controls.name"
                 [label]="'Name'">
   </custom-input>
   <custom-input formControlName="email" 
                 [control]="userForm.controls.email"   
                 [label]="'Email'">
   </custom-input>
   ...
</form>

Then in your custom component custom-input.ts

import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
    selector: 'custom-input',
    templateUrl: 'custom-input.html',
})
export class YInputItem {

   @Input('label') inputLabel: string;
   @Input() control: FormControl;
   @ViewChild('input') inputRef: ElementRef;

   constructor() { 
   }

   ngAfterViewInit(){
      // You should see the actual form control properties being passed in
      console.log('control',this.control);
   }
}

And then in the component's html custom-input.html

<label>
    {{ inputLabel }}
</label>
<input #input/>

Definitely worth checking out the ControlValueAccessor, but depending on how you are developing the control you might want to just use @Output to listen to change events , i.e if different inputs in the form have different events, you can just put the logic in the parent component and listen.

Solution 6 - Angular

Hopefully this simple use case can help someone.

This is an example of a phone number masking component that allows you to pass in the form group and reference the form control inside the component.

Child Component - phone-input.component.html

Add a reference to the FormGroup in the containing div and pass in the formControlName like you normally would on the input.

<div [formGroup]="pFormGroup">
     <input [textMask]="phoneMaskingDef" class="form-control" [formControlName]="pControlName" >
</div>

Parent Component - form.component.html

Reference the component and pass in pFormGroup and pControlName as atttributes.

<div class="form-group">
     <label>Home</label>
     <phone-input [pFormGroup]="myForm" pControlName="homePhone"></phone-input>
</div>

Solution 7 - Angular

You can get the input value by using the ion-input-auto-complete component, as per your code use below code

<form [formGroup]="userForm" no-validate>
   <input-auto-complete formControlName="name"
                 [ctrl]="userForm.controls['name']"
                 [label]="'Name'">
   </input-auto-complete>
</form>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRit_XPDView Question on Stackoverflow
Solution 1 - Angularweb-masterView Answer on Stackoverflow
Solution 2 - Angulardarko99View Answer on Stackoverflow
Solution 3 - AngularP. MoloneyView Answer on Stackoverflow
Solution 4 - AngularfishboneView Answer on Stackoverflow
Solution 5 - Angularuser1752532View Answer on Stackoverflow
Solution 6 - AngularDamian CView Answer on Stackoverflow
Solution 7 - AngularZameelView Answer on Stackoverflow