What is ngDefaultControl in Angular?
Asked Answered
B

1

215

No, this is not a duplicate question. You see, there is a ton of questions and issues in SO and Github that prescribe that I add this directive to a tag that has [(ngModel)] directive and is not contained in a form. If I don't add it I get an error:

ERROR Error: No value accessor for form control with unspecified name attribute

Ok, the error goes away if I put this attribute there. BUT, wait! Nobody knows what it does! And Angular's doc doesn't mention it at all. Why do I need a value accessor when I know that I don't need it? How is this attribute connected to value accessors? What does this directive do? What is a value accessor and how do I use it?

And why does everybody keep doing things that they don't understand at all? Just add this line of code and it works, thank you, this is not the way to write good programs.

And then. I read not one but two huge guides about forms in Angular and a section about ngModel:

And you know what? Not a single mention of either value accessors or ngDefaultControl. Where is it?

Boys answered 28/9, 2017 at 9:41 Comment(1)
> "And why does everybody keep doing things that they don't understand at all?" - yes! exactly! it could use some more exclamation points, though ;-)Ramburt
A
354

[ngDefaultControl]

Third party controls require a ControlValueAccessor to function with angular forms. Many of them, like Polymer's <paper-input>, behave like the <input> native element and thus can use the DefaultValueAccessor. Adding an ngDefaultControl attribute will allow them to use that directive.

<paper-input ngDefaultControl [(ngModel)]="value>

or

<paper-input ngDefaultControl formControlName="name">

So this is the main reason why this attribute was introduced.

It was called ng-default-control attribute in alpha versions of angular2.

So ngDefaultControl is one of selectors for DefaultValueAccessor directive:

@Directive({
  selector:
      'input:not([type=checkbox])[formControlName],
       textarea[formControlName],
       input:not([type=checkbox])[formControl],
       textarea[formControl],
       input:not([type=checkbox])[ngModel],
       textarea[ngModel],
       [ngDefaultControl]', <------------------------------- this selector
  ...
})
export class DefaultValueAccessor implements ControlValueAccessor {

What does it mean?

It means that we can apply this attribute to element (like polymer component) that doesn't have its own value accessor. So this element will take behaviour from DefaultValueAccessor and we can use this element with angular forms.

Otherwise you have to provide your own implementation of ControlValueAccessor

ControlValueAccessor

Angular docs states

A ControlValueAccessor acts as a bridge between the Angular forms API and a native element in the DOM.

Let's write the following template in simple angular2 application:

<input type="text" [(ngModel)]="userName">

To understand how our input above will behave we need to know which directives are applied to this element. Here angular gives out some hint with the error:

Unhandled Promise rejection: Template parse errors: Can't bind to 'ngModel' since it isn't a known property of 'input'.

Okay, we can open SO and get the answer: import FormsModule to your @NgModule:

@NgModule({
  imports: [
    ...,
    FormsModule
  ]
})
export AppModule {}

We imported it and all works as intended. But what's going on under the hood?

FormsModule exports for us the following directives:

@NgModule({
 ...
  exports: [InternalFormsSharedModule, TEMPLATE_DRIVEN_DIRECTIVES]
})
export class FormsModule {}

enter image description here

After some investigation we can discover that three directives will be applied to our input

  1. NgControlStatus

    @Directive({ selector: '[formControlName],[ngModel],[formControl]', ... }) export class NgControlStatus extends AbstractControlStatus { ... }

  2. NgModel

    @Directive({ selector: '[ngModel]:not([formControlName]):not([formControl])', providers: [formControlBinding], exportAs: 'ngModel' }) export class NgModel extends NgControl implements OnChanges,

  3. DEFAULT_VALUE_ACCESSOR

    @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 {

NgControlStatus directive just manipulates classes like ng-valid, ng-touched, ng-dirty and we can omit it here.


DefaultValueAccesstor provides NG_VALUE_ACCESSOR token in providers array:

export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true
};
...
@Directive({
  ...
  providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {

NgModel directive injects in constructor NG_VALUE_ACCESSOR token that was declared on the same host element.

export NgModel extends NgControl implements OnChanges, OnDestroy {
 constructor(...
  @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {

In our case NgModel will inject DefaultValueAccessor. And now NgModel directive calls shared setUpControl function:

export function setUpControl(control: FormControl, dir: NgControl): void {
  if (!control) _throwError(dir, 'Cannot find control with');
  if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');

  control.validator = Validators.compose([control.validator !, dir.validator]);
  control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
  dir.valueAccessor !.writeValue(control.value);

  setUpViewChangePipeline(control, dir);
  setUpModelChangePipeline(control, dir);

  ...
}

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void 
{
  dir.valueAccessor !.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingDirty = true;

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
  control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
    // control -> view
    dir.valueAccessor !.writeValue(newValue);

    // control -> ngModel
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  });
}

And here is the bridge in action:

enter image description here

NgModel sets up control (1) and calls dir.valueAccessor !.registerOnChange method. ControlValueAccessor stores callback in onChange(2) property and fires this callback when input event happens (3). And finally updateControl function is called inside callback (4)

function updateControl(control: FormControl, dir: NgControl): void {
  dir.viewToModelUpdate(control._pendingValue);
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
}

where angular calls forms API control.setValue.

That's a short version of how it works.

Administrative answered 28/9, 2017 at 9:44 Comment(8)
I just made @Input() ngModel and @Output() ngModelChange for bidirectional binding and I thought it should be enough a bridge. This looks like doing the same thing in a completely different way. Maybe I should not name my field ngModel?Boys
If you don't use this component with angular forms then you can just create your own two-way binding like @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter(); and then just use [(value)]="someProp"Administrative
That's exactly what I was doing. But I named my "value" as ngModel and Angular started throwing an error at me and asking for with ControlValueAccessor.Boys
Does anyone who what is the equivalent to ngDefaultControl in vue and React? I mean, i made a custom input component in angular using control value accessor and wrap it as a web component in Angular elements. In the same project i had to use ngDefaultControl to make that work with angular forms. But what should i do to make them work in Vue and React? Also in native JS?Flyblow
I'm using ngDefaultControl on my custom component but I struggle with one problem. When I set default value for formControl inside my formBuilder view (custom input component) is not updated, only model. What am I doing wrong?Muscadine
I am getting error while build --prod the application: Property '_pendingValue' does not exist on type 'FormControl'Stringy
@Administrative will this work when trying to bind ngModel to a nested value in an array?Allie
Thanks a lot for this extended answer! This is what I was searching for when I was starting to receive this error: a good in-depth explanation of what is going on. A good starting point to understand a little bit more how Angular works behind the scenes. It is such a complex tech and for the most part you're exposed to a very little superficial aspect of it.Pinprick

© 2022 - 2024 — McMap. All rights reserved.