¿Qué es ngDefaultControl en Angular?

101

No, esta no es una pregunta duplicada. Verá, hay un montón de preguntas y problemas en SO y Github que prescriben que agregue esta directiva a una etiqueta que tiene [(ngModel)]directiva y no está contenida en un formulario. Si no lo agrego, aparece un error:

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

Ok, el error desaparece si pongo este atributo allí. ¡Pero espera! ¡Nadie sabe lo que hace! Y el documento de Angular no lo menciona en absoluto. ¿Por qué necesito un descriptor de acceso de valor cuando sé que no lo necesito? ¿Cómo se relaciona este atributo con los descriptores de acceso al valor? ¿Qué hace esta directiva? ¿Qué es un procesador de valor y cómo lo uso?

¿Y por qué todo el mundo sigue haciendo cosas que no entienden del todo? Simplemente agregue esta línea de código y funciona, gracias, esta no es la forma de escribir buenos programas.

Y entonces. Leí no una, sino dos guías enormes sobre formularios en Angular y una sección sobre ngModel:

¿Y sabes qué? Ni una sola mención de los accesores de valor o ngDefaultControl. ¿Dónde está?

Gherman
fuente

Respuestas:

179

[ngDefaultControl]

Los controles de terceros requieren un ControlValueAccessorfuncionamiento 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 ngDefaultControlatributo 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-controlatributo en las versiones alfa de angular2 .

También lo ngDefaultControles 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 DefaultValueAccessory 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 inputse 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 FormsModulea 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 {}

ingrese la descripción de la imagen aquí

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 {

NgControlStatusdirectiva clases sólo manipula como ng-valid, ng-touched, ng-dirtyy podemos omitir aquí.


DefaultValueAccesstorproporciona NG_VALUE_ACCESSORtoken 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 {

NgModelLa directiva inyecta el NG_VALUE_ACCESSORtoken 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 NgModelinyectaremos DefaultValueAccessor. Y ahora la directiva NgModel llama a la setUpControlfunció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:

ingrese la descripción de la imagen aquí

NgModelconfigura control (1) y llama al dir.valueAccessor !.registerOnChangemétodo. ControlValueAccessoralmacena la devolución de llamada en la propiedad onChange(2) y activa esta devolución de llamada cuando inputocurre el evento (3) . Y finalmente la updateControlfunció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.

yurzui
fuente
Acabo de hacer @Input() ngModely @Output() ngModelChangepara 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 campo ngModel?
Gherman
2
Si no usa este componente con formas angulares, puede crear su propio enlace bidireccional @Input() value; @Output() valueChange: EventEmitter<any> = new EventEmitter();y luego usarlo[(value)]="someProp"
yurzui
1
Eso es exactamente lo que estaba haciendo. Pero nombré mi "valor" como ngModely Angular comenzó a arrojarme un error y a preguntar con ControlValueAccessor.
Gherman
¿Alguien que es el equivalente a ngDefaultControl en vue y React? Quiero decir, hice un componente de entrada personalizado en angular usando el descriptor de acceso de valor de control y lo envolví como un componente web en elementos angulares. En el mismo proyecto, tuve que usar ngDefaultControl para que funcionara con formas angulares. Pero, ¿qué debo hacer para que funcionen en Vue y React? ¿También en JS nativo?
Kavinda Jayakody
Estoy usando ngDefaultControl en mi componente personalizado pero lucho con un problema. Cuando establezco el valor predeterminado para formControl dentro de mi vista de formBuilder (componente de entrada personalizado) no se actualiza, solo model. ¿Qué estoy haciendo mal?
Igor Janković