Entrada de formulario personalizado angular 2

93

¿Cómo puedo crear un componente personalizado que funcione como una <input>etiqueta nativa ? Quiero que mi control de formulario personalizado sea compatible con ngControl, ngForm, [(ngModel)].

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

Además, parece que la directiva ngForm se enlaza solo para la <input>etiqueta, ¿es así? ¿Cómo puedo lidiar con eso?


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

ps: yo uso TypeScript.

Maksim Fomin
fuente
1
La mayoría de las respuestas están desactualizadas con respecto a las versiones actuales de Angular. Eche un vistazo a stackoverflow.com/a/41353306/2176962
hgoebl

Respuestas:

85

De hecho, hay dos cosas que implementar:

  • Un componente que proporciona la lógica de su componente de formulario. No necesita una entrada ya que se proporcionará por ngModelsí mismo
  • Una costumbre ControlValueAccessorque implementará el puente entre este componente y ngModel/ngControl

Tomemos una muestra. Quiero implementar un componente que gestione una lista de etiquetas para una empresa. El componente permitirá agregar y eliminar etiquetas. Quiero agregar una validación para asegurarme de que la lista de etiquetas 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: `
    <form [ngFormModel]="companyForm">
      Name: <input [(ngModel)]="company.name"
         [ngFormControl]="companyForm.controls.name"/>
      Tags: <tags [(ngModel)]="company.tags" 
         [ngFormControl]="companyForm.controls.tags"></tags>
    </form>
  `
})
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 TagsComponentcomponente define la lógica para agregar y eliminar elementos en la tagslista.

@Component({
  selector: 'tags',
  template: `
    <div *ngIf="tags">
      <span *ngFor="#tag of tags" style="font-size:14px"
         class="label label-default" (click)="removeTag(tag)">
        {{label}} <span class="glyphicon glyphicon-remove"
                        aria-  hidden="true"></span>
      </span>
      <span>&nbsp;|&nbsp;</span>
      <span style="display:inline-block;">
        <input [(ngModel)]="tagToAdd"
           style="width: 50px; font-size: 14px;" class="custom"/>
        <em class="glyphicon glyphicon-ok" aria-hidden="true" 
            (click)="addTag(tagToAdd)"></em>
      </span>
    </div>
  `
})
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 ninguna entrada en este componente, sino una setValue(el nombre no es importante aquí). Lo usamos más tarde para proporcionar el valor del ngModelal componente. Este componente define un evento para notificar cuando se actualiza el estado del componente (la lista de etiquetas).

Implementemos ahora el vínculo entre este componente y ngModel/ ngControl. Esto corresponde a una directiva que implementa la ControlValueAccessorinterfaz. Se debe definir un proveedor para este descriptor de acceso de valor contra el NG_VALUE_ACCESSORtoken (no olvide usarlo forwardRefya que la directiva se define después).

La directiva adjuntará un detector de tagsChangeeventos al evento del host (es decir, el componente al que se adjunta la directiva, es decir, el TagsComponent). El onChangemétodo será llamado cuando ocurra el evento. Este método corresponde al registrado por Angular2. De esta manera, estará al tanto de los cambios y actualizará el control de formulario asociado.

Se writeValueinvoca cuando ngFormse actualiza el valor enlazado en . Después de haber inyectado el componente adjunto (es decir, TagsComponent), podremos llamarlo para pasar este valor (ver el setValuemétodo anterior ).

No olvide proporcionar CUSTOM_VALUE_ACCESSORen los enlaces de la directiva.

Aquí está el código completo de la costumbre ControlValueAccessor:

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 manera cuando elimino todos los tagsde la empresa, el validatributo del companyForm.controls.tagscontrol se convierte falseautomáticamente.

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

Thierry Templier
fuente
¡Gracias! ¡Eres increíble! ¿Cómo piensas? ¿Está bien así? Quiero decir: no use elementos de entrada y cree controles propios como: <textfield>, <dropdown>? ¿Es esta forma "angular"?
Maksim Fomin
1
Yo diría que si desea implementar su propio campo en el formulario (algo personalizado), use este enfoque. De lo contrario, utilice elementos HTML nativos. Dicho esto, si desea modularizar la forma de mostrar input / textarea / select (por ejemplo, con Bootstrap3), puede aprovechar ng-content. Vea esta respuesta: stackoverflow.com/questions/34950950/…
Thierry Templier
3
A lo anterior le falta código y tiene algunas discrepancias, como 'removeLabel' en lugar de 'removeLabel'. Vea aquí un ejemplo de trabajo completo. ¡Gracias Thierry por dar el ejemplo inicial!
Azul
1
Lo encontré, importe desde @ angular / forms en lugar de @ angular / common y funciona. importar {NG_VALUE_ACCESSOR, ControlValueAccessor} desde '@ angular / forms';
Cagatay Civici
1
este enlace también debería ser útil ..
refactorización
110

No entiendo por qué todos los ejemplos que encuentro en Internet tienen que ser tan complicados. Al explicar 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 usando el componente que implementa ngModel:

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>

Componente autónomo (sin clase de 'acceso' separada, tal vez me estoy perdiendo 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: `<input [(ngModel)]="value">`,
  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 en una clase abstracta que ahora extiendo con cada componente que necesito para usar ngModel. Para mí, esto es un montón de código general y general 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:

<div class="form-group">
  <label class="control-label" >{{displaytext}}</label>
  <input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md">
</div>
David
fuente
1
Curiosamente, la respuesta aceptada parece haber dejado de funcionar desde RC2, probé este enfoque y funciona, aunque no estoy seguro de por qué.
3 de
1
@ 3urdoch Claro, un segundo
David
6
Para que funcione con las nuevas @angular/formsimportaciones de solo actualización: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk
6
Provider () no es compatible con Angular2 Final. En su lugar, haga que MakeProvider () devuelva {proporcionar: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa
2
Ya no es necesario importarlos CORE_DIRECTIVESy agregarlos, @Componentya que ahora se proporcionan de forma predeterminada desde Angular2 final. Sin embargo, de acuerdo con mi IDE, "Los constructores para clases derivadas deben contener una llamada 'super'", por lo que tuve que agregar super();al constructor de mi componente.
Joseph Webber
16

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: `<div class="form-group">
                    <label>
                        <ng-content></ng-content>
                        <input [(ngModel)]="value"
                                class="form-control"
                                (blur)="onBlur()" >
                    </label>
                </div>`,
    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:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
Dániel Kis
fuente
4
Si bien este enlace puede responder a la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si cambia la página enlazada.
Maximilian Ast
5

El ejemplo de Thierry es útil. Estas son 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';
Azul
fuente
1

Escribí una biblioteca que ayuda a reducir algo repetitivo para este caso: s-ng-utils. Algunas de las otras respuestas dan un ejemplo de cómo ajustar un control de formulario único . Usar s-ng-utilseso se puede hacer de manera muy simple usando WrappedFormControlSuperclass:

@Component({
    template: `
      <!-- any fancy wrapping you want in the template -->
      <input [formControl]="formControl">
    `,
    providers: [provideValueAccessor(StringComponent)],
})
class StringComponent extends WrappedFormControlSuperclass<string> {
  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }
}

En su publicación, menciona que desea envolver múltiples controles de formulario en un solo componente. Aquí hay un ejemplo completo de cómo hacerlo FormControlSuperclass.

import { Component, Injector } from "@angular/core";
import { FormControlSuperclass, provideValueAccessor } from "s-ng-utils";

interface Location {
  city: string;
  country: string;
}

@Component({
  selector: "app-location",
  template: `
    City:
    <input
      [ngModel]="location.city"
      (ngModelChange)="modifyLocation('city', $event)"
    />
    Country:
    <input
      [ngModel]="location.country"
      (ngModelChange)="modifyLocation('country', $event)"
    />
  `,
  providers: [provideValueAccessor(LocationComponent)],
})
export class LocationComponent extends FormControlSuperclass<Location> {
  location!: Location;

  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }

  handleIncomingValue(value: Location) {
    this.location = value;
  }

  modifyLocation<K extends keyof Location>(field: K, value: Location[K]) {
    this.location = { ...this.location, [field]: value };
    this.emitOutgoingValue(this.location);
  }
}

A continuación, puede utilizar <app-location>con [(ngModel)], [formControl], validadores personalizados - todo lo que puede hacer con los controles soportes angulares de la caja.

Eric Simonton
fuente
-1

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

modelo:

<div class="form-group" [ngClass]="{'has-error' : hasError}">
    <div><label>{{label}}</label></div>
    <input type="text" [placeholder]="placeholder" ngModel [ngClass]="{invalid: (invalid | async)}" [id]="identifier"        name="{{name}}-input" />    
</div>

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:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>
Nishant
fuente
Si bien esto parece prometedor, ya que está llamando a super, falta una "extensión"
Dave Nottage
1
Sí, no copié todo mi código aquí y olvidé eliminar el super ().
Nishant
9
Además, ¿de dónde viene externalNgModel? Esta respuesta estaría mejor servida con el código completo
Dave Nottage
Según angular.io/docs/ts/latest/api/core/index/… innerNgModel se define enngAfterViewInit
Matteo Suppo
2
Esto no funciona en absoluto. innerNgModel nunca se inicializa, outsideNgModel nunca se declara y ngModel pasado al constructor nunca se usa.
user2350838