validar reactivos formularios formulario typescript angular

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 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 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>&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 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.



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>