¿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.
typescript
angular
Maksim Fomin
fuente
fuente
Respuestas:
De hecho, hay dos cosas que implementar:
ngModel
sí mismoControlValueAccessor
que implementará el puente entre este componente yngModel
/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
TagsComponent
componente define la lógica para agregar y eliminar elementos en latags
lista.@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> | </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 delngModel
al 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 laControlValueAccessor
interfaz. Se debe definir un proveedor para este descriptor de acceso de valor contra elNG_VALUE_ACCESSOR
token (no olvide usarloforwardRef
ya que la directiva se define después).La directiva adjuntará un detector de
tagsChange
eventos al evento del host (es decir, el componente al que se adjunta la directiva, es decir, elTagsComponent
). ElonChange
mé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
writeValue
invoca cuandongForm
se actualiza el valor enlazado en . Después de haber inyectado el componente adjunto (es decir, TagsComponent), podremos llamarlo para pasar este valor (ver elsetValue
método anterior ).No olvide proporcionar
CUSTOM_VALUE_ACCESSOR
en 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
tags
de la empresa, elvalid
atributo delcompanyForm.controls.tags
control se conviertefalse
automáticamente.Consulte este artículo (sección "Componente compatible con NgModel") para obtener más detalles:
fuente
<textfield>
,<dropdown>
? ¿Es esta forma "angular"?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>
fuente
@angular/forms
importaciones de solo actualización:import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
CORE_DIRECTIVES
y agregarlos,@Component
ya 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 agregarsuper();
al constructor de mi componente.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>
fuente
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';
fuente
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 . Usars-ng-utils
eso se puede hacer de manera muy simple usandoWrappedFormControlSuperclass
:@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.fuente
También puede resolver esto con una directiva @ViewChild. Esto le da al padre acceso completo a todas las variables miembro y funciones de un hijo inyectado.
Ver: Cómo acceder a los campos de entrada del componente de formulario inyectado
fuente
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>
fuente
innerNgModel
se define enngAfterViewInit
Esto es bastante fácil de hacer
ControlValueAccessor
NG_VALUE_ACCESSOR
.Puede leer este artículo para hacer un campo personalizado simple Crear componente de campo de entrada personalizado con Angular
fuente