typescript - reactivos - Entrada de formulario personalizado angular 2
validar formulario angular 5 (8)
¿Cómo puedo crear un componente personalizado que funcione igual que la etiqueta
<input>
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 igual que el nativo.
Además, parece que la directiva ngForm se une solo para la etiqueta
<input>
, ¿es esto correcto?
¿Cómo puedo lidiar con eso?
Déjame explicarte por qué necesito esto. Quiero ajustar varios elementos de entrada para que puedan trabajar juntos como una sola entrada. ¿Hay otra manera de lidiar con eso? Una vez más: quiero hacer este control al igual que uno nativo. Validación, ngForm, ngModel enlace bidireccional y otros.
ps: yo uso Typecript.
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á proporcionada por el propio
ngModel
-
Un
ControlValueAccessor
personalizado 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 garantizar 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 componente
TagsComponent
define la lógica para agregar y eliminar elementos en la lista de
tags
.
@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 entrada en este componente sino un
setValue
(el nombre no es importante aquí).
Lo usamos más tarde para proporcionar el valor del
ngModel
al componente.
Este componente define un evento para notificar cuando se actualiza el estado del componente (la lista de etiquetas).
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 valor de acceso con el token
NG_VALUE_ACCESSOR
(no olvide usar
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 se adjunta 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 manera, estará al tanto de los cambios y actualizaciones correspondientes al control de formulario asociado.
Se llama a
writeValue
cuando se actualiza el valor enlazado en el
ngForm
.
Después de haber inyectado el componente conectado (es decir, TagsComponent), podremos llamarlo para pasar este valor (consulte el método
setValue
anterior).
No olvide proporcionar el
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 manera, cuando elimino todas las
tags
de la empresa, el atributo
valid
del control
companyForm.controls.tags
vuelve
false
automáticamente.
Consulte este artículo (sección "Componente compatible con NgModel") para obtener más detalles:
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'';
Escribí una biblioteca que ayuda a reducir algunas repeticiones para este caso:
s-ng-utils
.
Algunas de las otras respuestas dan ejemplos de envolver un control de formulario
único
.
Usando
s-ng-utils
que se pueden 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 ajustar múltiples controles de formulario en un solo componente.
Aquí hay un ejemplo completo haciendo eso con
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);
}
}
Luego puede usar
<app-location>
con
[(ngModel)]
,
[formControl]
, validadores personalizados: todo lo que puede hacer con los controles que Angular admite de fábrica.
Esto es bastante fácil de hacer con
ControlValueAccessor
NG_VALUE_ACCESSOR
.
Puede leer este artículo para crear un campo personalizado simple Crear componente de campo de entrada personalizado con angular
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>
No entiendo por qué cada ejemplo que encuentro en Internet tiene que ser tan complicado. 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 utilizando componentes que implementan ngModel:
EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>
Componente autónomo (no hay una clase ''accesora'' 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 resumir todo esto en una clase abstracta que ahora extiendo con cada componente que necesito para usar ngModel. Para mí, esto es una tonelada de código indirecto y repetitivo 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>
Por qué crear un nuevo valor de acceso cuando puede usar el ngModel interno. Cada vez que crea un componente personalizado que tiene una entrada [ngModel], ya estamos instanciando un ControlValueAccessor. Y ese es el descriptor de acceso 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>
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 niño inyectado.
Ver: Cómo acceder a los campos de entrada del componente de formulario inyectado