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.
FormControlName is used to sync a FormControl in an existing FormGroup to a form control element by name.
NG_VALUE_ACCESSOR provider specifies a class that implements ControlValueAccessor interface and is used by Angular to setup synchronization with formControl . It's usually the class of the component or directive that registers the provider.
Control Value Accessor is an interface that provides us the power to leverage the Angular forms API and create a communication between Angular Form API and the DOM element. It provides us many facilities in angular like we can create custom controls or custom component with the help of control value accessor interface.
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 attrubute 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
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 {}
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With