[ngDefaultControl]
Los controles de terceros requieren un ControlValueAccessor
funcionamiento con formas angulares. Muchos de ellos, como el de Polymer <paper-input>
, se comportan como el <input>
elemento nativo y, por lo tanto, pueden usar DefaultValueAccessor
. Agregar un ngDefaultControl
atributo les permitirá usar esa directiva.
<paper-input ngDefaultControl [(ngModel)]="value>
o
<paper-input ngDefaultControl formControlName="name">
Entonces, esta es la razón principal por la que se introdujo este atributo.
Se le llamó ng-default-control
atributo en las versiones alfa de angular2 .
También lo ngDefaultControl
es uno de los selectores para la directiva DefaultValueAccessor :
@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 {
Qué significa eso?
Significa que podemos aplicar este atributo a un elemento (como un componente de polímero) que no tiene su propio descriptor de acceso de valor. Entonces, este elemento tomará el comportamiento de DefaultValueAccessor
y podemos usar este elemento con formas angulares.
De lo contrario, debe proporcionar su propia implementación de ControlValueAccessor
ControlValueAccessor
Estados de documentos angulares
Un ControlValueAccessor actúa como un puente entre la API de formas angulares y un elemento nativo en el DOM.
Escribamos la siguiente plantilla en una aplicación angular2 simple:
<input type="text" [(ngModel)]="userName">
Para comprender cómo input
se comportará nuestro anterior, necesitamos saber qué directivas se aplican a este elemento. Aquí angular da una pista con el error:
Rechazo de promesa no controlado: errores de análisis de plantilla: no se puede enlazar a 'ngModel' porque no es una propiedad conocida de 'input'.
Bien, podemos abrir SO y obtener la respuesta: importar FormsModule
a su @NgModule
:
@NgModule({
imports: [
...,
FormsModule
]
})
export AppModule {}
Lo importamos y todo funciona según lo previsto. Pero, ¿qué está pasando bajo el capó?
FormsModule exporta para nosotros las siguientes directivas:
@NgModule({
...
exports: [InternalFormsSharedModule, TEMPLATE_DRIVEN_DIRECTIVES]
})
export class FormsModule {}
Después de alguna investigación, podemos descubrir que se aplicarán tres directivas a nuestra 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
directiva clases sólo manipula como ng-valid
, ng-touched
, ng-dirty
y podemos omitir aquí.
DefaultValueAccesstor
proporciona NG_VALUE_ACCESSOR
token en la matriz de proveedores:
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
La directiva inyecta el NG_VALUE_ACCESSOR
token del constructor que se declaró en el mismo elemento de host.
export NgModel extends NgControl implements OnChanges, OnDestroy {
constructor(...
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
En nuestro caso NgModel
inyectaremos DefaultValueAccessor
. Y ahora la directiva NgModel llama a la setUpControl
función compartida :
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);
});
}
Y aquí está el puente en acción:
NgModel
configura control (1) y llama al dir.valueAccessor !.registerOnChange
método. ControlValueAccessor
almacena la devolución de llamada en la propiedad onChange
(2) y activa esta devolución de llamada cuando input
ocurre el evento (3) . Y finalmente la updateControl
función se llama dentro de la devolución de llamada (4)
function updateControl(control: FormControl, dir: NgControl): void {
dir.viewToModelUpdate(control._pendingValue);
if (control._pendingDirty) control.markAsDirty();
control.setValue(control._pendingValue, {emitModelToViewChange: false});
}
donde las llamadas angulares forman API control.setValue
.
Esa es una versión corta de cómo funciona.
@Input() ngModel
y@Output() ngModelChange
para encuadernación bidireccional y pensé que debería ser suficiente un puente. Esto parece hacer lo mismo de una manera completamente diferente. ¿Quizás no debería nombrar mi campongModel
?@Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();
y luego usarlo[(value)]="someProp"
ngModel
y Angular comenzó a arrojarme un error y a preguntar con ControlValueAccessor.