Angular4 - No value accessor for form control
Asked Answered
F

5

198

I have a custom element :

<div formControlName="surveyType">
  <div *ngFor="let type of surveyTypes"
       (click)="onSelectType(type)"
       [class.selected]="type === selectedType">
    <md-icon>{{ type.icon }}</md-icon>
    <span>{{ type.description }}</span>
  </div>
</div>

When I try to add the formControlName, I get an error message:

ERROR Error: No value accessor for form control with name: 'surveyType'

I tried to add ngDefaultControl without success. It seems it's because there is no input/select... and I dont know what to do.

I would like to bind my click to this formControl in order that when someone clicks on the entire card that would push my 'type' into the formControl. Is it possible?

Foreclosure answered 13/8, 2017 at 11:13 Comment(5)
I don't know my point is that : formControl go for form control in html but div is not a form control. I would like tu bind my surveyType with the type.id of my card divForeclosure
i know i could use the old angular way and have my selectedType bind to it but i was trying to use and learn reactive form from angular 4 and dont know how to use formControl with this type of case.Foreclosure
Ok i it s maybe jsut that case can't be handle by a reactive form so. Thx anyway :)Foreclosure
I've made an answer about how to break down huge forms into sub components here https://mcmap.net/q/129857/-angular-2-large-scale-application-forms-39-handling but this also apply very well with just a custom control value accessor. Also check out github.com/cloudnc/ngx-sub-form :)Rosalbarosalee
I had the same issue ans solved it in this post: https://mcmap.net/q/129858/-angular-2-creating-reactive-forms-with-nested-componentsAfterbrain
C
326

You can use formControlName only on directives which implement ControlValueAccessor.

Implement the interface

So, in order to do what you want, you have to create a component which implements ControlValueAccessor, which means implementing the following three functions:

  • writeValue (tells Angular how to write value from model into view)
  • registerOnChange (registers a handler function that is called when the view changes)
  • registerOnTouched (registers a handler to be called when the component receives a touch event, useful for knowing if the component has been focused).

Register a provider

Then, you have to tell Angular that this directive is a ControlValueAccessor (interface is not gonna cut it since it is stripped from the code when TypeScript is compiled to JavaScript). You do this by registering a provider.

The provider should provide NG_VALUE_ACCESSOR and use an existing value. You'll also need a forwardRef here. Note that NG_VALUE_ACCESSOR should be a multi provider.

For example, if your custom directive is named MyControlComponent, you should add something along the following lines inside the object passed to @Component decorator:

providers: [
  { 
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: forwardRef(() => MyControlComponent),
  }
]

Usage

Your component is ready to be used. With template-driven forms, ngModel binding will now work properly.

With reactive forms, you can now properly use formControlName and the form control will behave as expected.

Resources

Calcite answered 13/8, 2017 at 12:55 Comment(2)
also don't forget ngDefaultControl on the concerned input.Taffrail
Is it possible to include ngDefaultControl directive directly on the component, so we don't have to add it every time ? If so how please?Johniejohnna
T
88

You should use formControlName="surveyType" on an input and not on a div

Tingaling answered 13/8, 2017 at 11:20 Comment(4)
Yes sure, but i dont know how to turn my card div into something else which will be a html form controlForeclosure
The point of CustomValueAccessor is to add form control to ANYTHING, even a divJanerich
@Janerich This is a bad pattern though. You mimic Input functionality in a wrapper component, re-implementing standard HTML-methods yourself (thus basically re-inventing the wheel and making your code verbose). but in 90% of the cases you can accomplish all you want by using <ng-content> in a wrapper component and let the parent component that defines formControls simply put the <input> inside the <wrapper>Inning
This answer gave me the notion that the structure of the HTML element in the template was incomplete. I added a MatSelectModule import to the test configuration and this solved the problem for me.Uprush
C
38

The error means, that Angular doesn't know what to do when you put a formControl on a div. To fix this, you have two options.

  1. You put the formControlName on an element, that is supported by Angular out of the box. Those are: input, textarea and select.
  2. You implement the ControlValueAccessor interface. By doing so, you're telling Angular "how to access the value of your control" (hence the name). Or in simple terms: What to do, when you put a formControlName on an element, that doesn't naturally have a value associated with it.

Now, implementing the ControlValueAccessor interface can be a bit daunting at first. Especially because there isn't much good documentation of this out there and you need to add a lot of boilerplate to your code. So let me try to break this down in some simple-to-follow steps.

Move your form control into its own component

In order to implement the ControlValueAccessor, you need to create a new component (or directive). Move the code related to your form control there. Like this it will also be easily reusable. Having a control already inside a component might be the reason in the first place, why you need to implement the ControlValueAccessor interface, because otherwise you will not be able to use your custom component together with Angular forms.

Add the boilerplate to your code

Implementing the ControlValueAccessor interface is quite verbose, here's the boilerplate that comes with it:

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


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // a) copy paste this providers property (adjust the component name in the forward ref)
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // c) copy paste this code
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // d) copy paste this code
  writeValue(input: string) {
    // TODO
  }

So what are the individual parts doing?

  • a) Lets Angular know during runtime that you implemented the ControlValueAccessor interface
  • b) Makes sure you're implementing the ControlValueAccessor interface
  • c) This is probably the most confusing part. Basically what you're doing is, you give Angular the means to override your class properties/methods onChange and onTouch with it's own implementation during runtime, such that you can then call those functions. So this point is important to understand: You don't need to implement onChange and onTouch yourself (other than the initial empty implementation). The only thing your doing with (c) is to let Angular attach it's own functions to your class. Why? So you can then call the onChange and onTouch methods provided by Angular at the appropriate time. We'll see how this works down below.
  • d) We'll also see how the writeValue method works in the next section, when we implement it. I've put it here, so all required properties on ControlValueAccessor are implemented and your code still compiles.

Implement writeValue

What writeValue does, is to do something inside your custom component, when the form control is changed on the outside. So for example, if you have named your custom form control component app-custom-input and you'd be using it in the parent component like this:

<form [formGroup]="form">
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

then writeValue gets triggered whenever the parent component somehow changes the value of myFormControl. This could be for example during the initialization of the form (this.form = this.formBuilder.group({myFormControl: ""});) or on a form reset this.form.reset();.

What you'll typically want to do if the value of the form control changes on the outside, is to write it to a local variable which represents the form control value. For example, if your CustomInputComponent revolves around a text based form control, it could look like this:

writeValue(input: string) {
  this.input = input;
}

and in the html of CustomInputComponent:

<input type="text"
       [ngModel]="input">

You could also write it directly to the input element as described in the Angular docs.

Now you have handled what happens inside of your component when something changes outside. Now let's look at the other direction. How do you inform the outside world when something changes inside of your component?

Calling onChange

The next step is to inform the parent component about changes inside of your CustomInputComponent. This is where the onChange and onTouch functions from (c) from above come into play. By calling those functions you can inform the outside about changes inside your component. In order to propagate changes of the value to the outside, you need to call onChange with the new value as the argument. For example, if the user types something in the input field in your custom component, you call onChange with the updated value:

<input type="text"
       [ngModel]="input"
       (ngModelChange)="onChange($event)">

If you check the implementation (c) from above again, you'll see what's happening: Angular bound it's own implementation to the onChange class property. That implementation expects one argument, which is the updated control value. What you're doing now is you're calling that method and thus letting Angular know about the change. Angular will now go ahead and change the form value on the outside. This is the key part in all this. You told Angular when it should update the form control and with what value by calling onChange. You've given it the means to "access the control value".

By the way: The name onChange is chosen by me. You could choose anything here, for example propagateChange or similar. However you name it though, it will be the same function that takes one argument, that is provided by Angular and that is bound to your class by the registerOnChange method during runtime.

Calling onTouch

Since form controls can be "touched", you should also give Angular the means to understand when your custom form control is touched. You can do it, you guessed it, by calling the onTouch function. So for our example here, if you want to stay compliant with how Angular is doing it for the out-of-the-box form controls, you should call onTouch when the input field is blurred:

<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">

Again, onTouch is a name chosen by me, but what it's actual function is provided by Angular and it takes zero arguments. Which makes sense, since you're just letting Angular know, that the form control has been touched.

Putting it all together

So how does that look when it comes all together? It should look like this:

// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.scss'],

  // Step 1: copy paste this providers property
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true
    }
  ]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {

  // Step 3: Copy paste this stuff here
  onChange: any = () => {}
  onTouch: any = () => {}
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }

  // Step 4: Define what should happen in this component, if something changes outside
  input: string;
  writeValue(input: string) {
    this.input = input;
  }

  // Step 5: Handle what should happen on the outside, if something changes on the inside
  // in this simple case, we've handled all of that in the .html
  // a) we've bound to the local variable with ngModel
  // b) we emit to the ouside by calling onChange on ngModelChange

}
// custom-input.component.html
<input type="text"
       [(ngModel)]="input"
       (ngModelChange)="onChange($event)"
       (blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>

// OR

<form [formGroup]="form" >
  <app-custom-input formControlName="myFormControl"></app-custom-input>
</form>

More Examples

Nested Forms

Note that Control Value Accessors are NOT the right tool for nested form groups. For nested form groups you can simply use an @Input() subform instead. Control Value Accessors are meant to wrap controls, not groups! See this example how to use an input for a nested form: https://stackblitz.com/edit/angular-nested-forms-input-2

Sources

Cerebrovascular answered 8/6, 2020 at 10:41 Comment(0)
G
0

1-Open app.module.ts

2-Add ReactiveFormModule, FormsModule to the imports section under the @NgModule decorator.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // Adding FormsModule

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, ReactiveFormsModule,FormsModule], // Adding ReactiveFormModule, FormsModule
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

3-After adding the FormsModule, refresh the page as the formControlName property should work correctly.

I hope it helps

Greater answered 19/10, 2023 at 9:12 Comment(0)
V
-1

For me it was due to "multiple" attribute on select input control as Angular has different ValueAccessor for this type of control.

const countryControl = new FormControl();

And inside template use like this

    <select multiple name="countries" [formControl]="countryControl">
      <option *ngFor="let country of countries" [ngValue]="country">
       {{ country.name }}
      </option>
    </select>

More details ref Official Docs

Volute answered 28/4, 2020 at 14:31 Comment(1)
What was due to "multiple"? I don't see how your code solves anything, or what was the original problem. Your code shows the usual basic usage.Aminaamine

© 2022 - 2024 — McMap. All rights reserved.