Entrada de formulario personalizado angular 2

¿Cómo puedo crear un componente personalizado que funcione igual que la etiqueta nativa ? Quiero hacer que mi control de formulario personalizado sea compatible con ngControl, ngForm, [(ngModel)].

Según tengo entendido, necesito implementar algunas interfaces para hacer que mi propio control de formularios funcione como uno nativo.

Además, parece que la directiva ngForm solo se une a la etiqueta , ¿es así? ¿Cómo puedo lidiar con eso?


Déjame explicarte por qué necesito esto en absoluto. Quiero envolver varios elementos de entrada para que puedan trabajar juntos como una sola entrada. ¿Hay alguna otra forma de lidiar con eso? Una vez más: quiero que este control sea como el nativo. Validación, ngForm, ngModel enlace bidireccional y otros.

ps: yo uso Typescript.

De hecho, hay dos cosas para implementar:

  • Un componente que proporciona la lógica de su componente de formulario. No es una entrada ya que será proporcionado por ngModel
  • Un ControlValueAccessor personalizado que implementará el puente entre este componente y ngModel / ngControl

Tomemos una muestra. Quiero implementar un componente que administre una lista de tags para una empresa. El componente permitirá agregar y eliminar tags. Quiero agregar una validación para asegurarme de que la lista de tags no esté vacía. Lo definiré en mi componente como se describe a continuación:

 (...) import {TagsComponent} from './app.tags.ngform'; import {TagsValueAccessor} from './app.tags.ngform.accessor'; function notEmpty(control) { if(control.value == null || control.value.length===0) { return { notEmpty: true } } return null; } @Component({ selector: 'company-details', directives: [ FormFieldComponent, TagsComponent, TagsValueAccessor ], template: ` 
Name: Tags:
` }) export class DetailsComponent implements OnInit { constructor(_builder:FormBuilder) { this.company = new Company('companyid', 'some name', [ 'tag1', 'tag2' ]); this.companyForm = _builder.group({ name: ['', Validators.required], tags: ['', notEmpty] }); } }

El componente TagsComponent define la lógica para agregar y eliminar elementos en la lista de tags .

 @Component({ selector: 'tags', template: ` 
{{label}}  | 
` }) export class TagsComponent { @Output() tagsChange: EventEmitter; constructor() { this.tagsChange = new EventEmitter(); } setValue(value) { this.tags = value; } removeLabel(tag:string) { var index = this.tags.indexOf(tag, 0); if (index != undefined) { this.tags.splice(index, 1); this.tagsChange.emit(this.tags); } } addLabel(label:string) { this.tags.push(this.tagToAdd); this.tagsChange.emit(this.tags); this.tagToAdd = ''; } }

Como puede ver, no hay entrada en este componente sino un setValue uno (el nombre no es importante aquí). Lo usamos más tarde para proporcionar el valor de ngModel al componente. Este componente define un evento para notificar cuando se actualiza el estado del componente (la lista de tags).

Implementemos ahora el enlace entre este componente y ngModel / ngControl . Esto corresponde a una directiva que implementa la interfaz ControlValueAccessor . Se debe definir un proveedor para este acceso de valor contra el token NG_VALUE_ACCESSOR (no olvide utilizar forwardRef ya que la directiva se define después).

La directiva adjuntará un detector de eventos en el evento tagsChange del host (es decir, el componente al que está asociada la directiva, es decir, el TagsComponent ). Se onChange método onChange cuando ocurra el evento. Este método corresponde al registrado por Angular2. De esta forma, se dará cuenta de los cambios y las actualizaciones de acuerdo con el control de formulario asociado.

Se llama a writeValue cuando se actualiza el valor enlazado en ngForm . Después de haber inyectado el componente adjunto (es decir, TagsComponent), podremos llamarlo para pasar este valor (consulte el método setValue anterior).

No olvide proporcionar CUSTOM_VALUE_ACCESSOR en los enlaces de la directiva.

Aquí está el código completo del ControlValueAccessor personalizado:

 import {TagsComponent} from './app.tags.ngform'; const CUSTOM_VALUE_ACCESSOR = CONST_EXPR(new Provider( NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => TagsValueAccessor), multi: true})); @Directive({ selector: 'tags', host: {'(tagsChange)': 'onChange($event)'}, providers: [CUSTOM_VALUE_ACCESSOR] }) export class TagsValueAccessor implements ControlValueAccessor { onChange = (_) => {}; onTouched = () => {}; constructor(private host: TagsComponent) { } writeValue(value: any): void { this.host.setValue(value); } registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } } 

De esta forma, cuando elimino todas las tags de la compañía, el atributo valid del control companyForm.controls.tags convierte en false automáticamente.

Consulte este artículo (sección “Componente compatible con NgModel”) para obtener más detalles:

No entiendo por qué cada ejemplo que encuentro en Internet tiene que ser tan complicado. Cuando explico un nuevo concepto, creo que siempre es mejor tener el ejemplo más simple y funcional posible. Lo he destilado un poco:

HTML para formulario externo utilizando el componente que implementa ngModel:

 EmailExternal=  

Componente autocontenido (no hay clase de ‘accesor’ por separado, tal vez me falta el punto):

 import {Component, Provider, forwardRef, Input} from "@angular/core"; import {ControlValueAccessor, NG_VALUE_ACCESSOR, CORE_DIRECTIVES} from "@angular/common"; const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = new Provider( NG_VALUE_ACCESSOR, { useExisting: forwardRef(() => InputField), multi: true }); @Component({ selector : 'inputfield', template: ``, directives: [CORE_DIRECTIVES], providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] }) export class InputField implements ControlValueAccessor { private _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; this.onChange(value); } onChange = (_) => {}; onTouched = () => {}; registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } } 

De hecho, acabo de abstraer todo esto a una clase abstracta que ahora extiendo con cada componente que necesito para usar ngModel. Para mí, este es un montón de código general y repetitivo del que puedo prescindir.

Editar: Aquí está:

 import { forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; export abstract class AbstractValueAccessor implements ControlValueAccessor { _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; // warning: comment below if only want to emit on user intervention this.onChange(value); } onChange = (_) => {}; onTouched = () => {}; registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } } export function MakeProvider(type : any){ return { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true }; } 

Aquí hay un componente que lo usa: (TS):

 import {Component, Input} from "@angular/core"; import {CORE_DIRECTIVES} from "@angular/common"; import {AbstractValueAccessor, MakeProvider} from "../abstractValueAcessor"; @Component({ selector : 'inputfield', template: require('./genericinput.component.ng2.html'), directives: [CORE_DIRECTIVES], providers: [MakeProvider(InputField)] }) export class InputField extends AbstractValueAccessor { @Input('displaytext') displaytext: string; @Input('placeholder') placeholder: string; } 

HTML:

 

Hay un ejemplo en este enlace para la versión RC5: http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

 import { Component, forwardRef } from '@angular/core'; import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; const noop = () => { }; export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true }; @Component({ selector: 'custom-input', template: `
`, providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] }) export class CustomInputComponent implements ControlValueAccessor { //The internal data model private innerValue: any = ''; //Placeholders for the callbacks which are later providesd //by the Control Value Accessor private onTouchedCallback: () => void = noop; private onChangeCallback: (_: any) => void = noop; //get accessor get value(): any { return this.innerValue; }; //set accessor including call the onchange callback set value(v: any) { if (v !== this.innerValue) { this.innerValue = v; this.onChangeCallback(v); } } //Set touched on blur onBlur() { this.onTouchedCallback(); } //From ControlValueAccessor interface writeValue(value: any) { if (value !== this.innerValue) { this.innerValue = value; } } //From ControlValueAccessor interface registerOnChange(fn: any) { this.onChangeCallback = fn; } //From ControlValueAccessor interface registerOnTouched(fn: any) { this.onTouchedCallback = fn; } }

Entonces podemos usar este control personalizado de la siguiente manera:

 
Enter data:

El ejemplo de Thierry es útil. Aquí están las importaciones que se necesitan para que TagsValueAccessor se ejecute …

 import {Directive, Provider} from 'angular2/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR } from 'angular2/common'; import {CONST_EXPR} from 'angular2/src/facade/lang'; import {forwardRef} from 'angular2/src/core/di'; 

Esto es bastante fácil de hacer con ControlValueAccessor NG_VALUE_ACCESSOR .

Puede leer este artículo para hacer un campo personalizado simple Crear componente de campo de entrada personalizado con angular

También puedes resolver esto con una directiva @ViewChild. Esto le da al padre acceso completo a todas las variables y funciones de miembro de un niño inyectado.

Ver: Cómo acceder a los campos de entrada del componente de formulario inyectado

Por qué crear un nuevo acceso de valor cuando puede usar el ngModel interno. Siempre que esté creando un componente personalizado que tenga una entrada [ngModel], ya estamos creando una instancia de ControlValueAccessor. Y ese es el accesorio que necesitamos.

modelo:

 

Componente:

 export class MyInputComponent { @ViewChild(NgModel) innerNgModel: NgModel; constructor(ngModel: NgModel) { //First set the valueAccessor of the outerNgModel this.outerNgModel.valueAccessor = this.innerNgModel.valueAccessor; //Set the innerNgModel to the outerNgModel //This will copy all properties like validators, change-events etc. this.innerNgModel = this.outerNgModel; } } 

Usar como: