Entrada de formulário personalizado Angular 2


89

Como posso criar um componente personalizado que funcione como uma <input>tag nativa ? Quero fazer com que meu controle de formulário personalizado seja capaz de oferecer suporte a ngControl, ngForm, [(ngModel)].

Pelo que entendi, preciso implementar algumas interfaces para fazer meu próprio controle de formulário funcionar como o nativo.

Além disso, parece que a diretiva ngForm se vincula apenas a <input>tag, certo? Como posso lidar com isso?


Deixe-me explicar por que preciso disso. Quero agrupar vários elementos de entrada para torná-los capazes de trabalhar juntos como uma única entrada. Existe outra maneira de lidar com isso? Mais uma vez: quero fazer esse controle igualzinho ao nativo. Validação, ngForm, ngModel ligação bidirecional e outros.

ps: Eu uso o Typescript.


1
A maioria das respostas está desatualizada em relação às versões angulares atuais. Dê uma olhada em stackoverflow.com/a/41353306/2176962
— hgoebl

Respostas:


82

Na verdade, há duas coisas a serem implementadas:

  • Um componente que fornece a lógica do seu componente de formulário. Não é uma entrada, pois será fornecida por ngModelsi mesma
  • Um costume ControlValueAccessorque irá implementar a ponte entre este componente e ngModel/ngControl

Vamos dar uma amostra. Quero implementar um componente que gerencia uma lista de tags para uma empresa. O componente permitirá adicionar e remover tags. Quero adicionar uma validação para garantir que a lista de tags não está vazia. Vou defini-lo em meu componente conforme descrito abaixo:

(...)
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]
    });
  }
}

O TagsComponentcomponente define a lógica para adicionar e remover elementos da 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 você pode ver, não há nenhuma entrada neste componente, mas uma setValue(o nome não é importante aqui). Nós o usaremos posteriormente para fornecer o valor de ngModelpara o componente. Este componente define um evento para notificar quando o estado do componente (a lista de tags) é atualizado.

Vamos implementar agora o link entre este componente e ngModel/ ngControl. Isso corresponde a uma diretiva que implementa a ControlValueAccessorinterface. Um provedor deve ser definido para este acessador de valor em relação ao NG_VALUE_ACCESSORtoken (não se esqueça de usarforwardRef pois a diretiva é definida depois).

A diretiva anexará um ouvinte de evento no tagsChangeevento do host (ou seja, o componente ao qual a diretiva está anexada, ou seja, o TagsComponent). O onChangemétodo será chamado quando o evento ocorrer. Este método corresponde ao registrado pelo Angular2. Desta forma, ele estará ciente das alterações e atualizações de acordo com o controle de formulário associado.

O writeValueé chamado quando o valor vinculado a ngFormé atualizado. Depois de injetar o componente anexado (isto é, TagsComponent), poderemos chamá-lo para passar este valor (veja o setValuemétodo anterior ).

Não se esqueça de fornecer CUSTOM_VALUE_ACCESSORnas ligações da diretiva.

Aqui está o código completo do custom 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; }
}

Desta forma, quando eu removo todos os tagsda empresa, o validatributo do companyForm.controls.tagscontrole passa falsea ser automático.

Consulte este artigo (seção "Componente compatível com NgModel") para obter mais detalhes:


Obrigado! Você é incrível! Como você pensa - é realmente bom assim? Quer dizer: não use elementos de entrada e faça seus próprios controladores como: <textfield>, <dropdown>? É esta forma "angular"?
— Maksim Fomin

1
Eu diria que se você deseja implementar seu próprio campo no formulário (algo customizado), use essa abordagem. Caso contrário, use elementos HTML nativos. Dito isso, se você quiser modularizar a maneira de exibir input / textarea / select (por exemplo, com Bootstrap3), você pode aproveitar o ng-content. Veja esta resposta: stackoverflow.com/questions/34950950/…
— Thierry Templier

3
O código acima está faltando e tem algumas discrepâncias, como 'removeLabel' em vez de 'removeLabel'. Veja aqui um exemplo de trabalho completo. Obrigado Thierry por divulgar o exemplo inicial!
— Azul

1
Encontre-o, importe de @ angular / forms em vez de @ angular / common e funciona. importar {NG_VALUE_ACCESSOR, ControlValueAccessor} de '@ angular / forms';
— Cagatay Civici

1
este link também deve ser útil ..
— refatorar

109

Não entendo porque todos os exemplos que encontro na internet têm que ser tão complicados. Ao explicar um novo conceito, acho que é sempre melhor ter o exemplo mais simples e funcional possível. Eu destilei um pouco:

HTML para formulário externo usando componente que implementa ngModel:

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

Componente independente (sem classe de 'acessador' separada - talvez eu esteja perdendo o ponto):

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; }
}

Na verdade, acabei de abstrair todas essas coisas para uma classe abstrata que agora estendo com todos os componentes de que preciso para usar o ngModel. Para mim, isso é uma tonelada de código indireto e clichê que posso dispensar.

Editar: Aqui 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
  };
}

Este é um componente que o 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>

1
Curiosamente, a resposta aceita parece ter parado de funcionar desde RC2, tentei essa abordagem e funciona, mas não tenho certeza do porquê.
— 3urdoch

1
@ 3urdoch Claro, um segundo
— David

6
Para fazer funcionar com as novas @angular/formsimportações, basta atualizar: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
— ulfryk

6
Provider () não é compatível com Angular2 Final. Em vez disso, faça com que MakeProvider () retorne {fornecer: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => tipo), multi: true};
— DSoa

2
Você não precisa mais importar CORE_DIRECTIVESe adicioná-los ao, @Componentpois eles são fornecidos por padrão desde o Angular2 final. No entanto, de acordo com meu IDE, "Construtores para classes derivadas devem conter uma 'super' chamada.", Então eu tive que adicionar super();ao construtor do meu componente.
— Joseph Webber

16

Há um exemplo neste link para a versão 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;
    }

}

Podemos então usar esse controle personalizado da seguinte maneira:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>

4
Embora este link possa responder à pergunta, é melhor incluir as partes essenciais da resposta aqui e fornecer o link para referência. As respostas somente com link podem se tornar inválidas se a página vinculada mudar.
— Maximilian Ast

5

O exemplo de Thierry é útil. Aqui estão as importações que são necessárias para o TagsValueAccessor rodar ...

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';

1

Eu escrevi uma biblioteca que ajuda a reduzir alguns clichê para este caso: s-ng-utils. Algumas das outras respostas são exemplos de agrupamento de um único controle de formulário. Usando s-ng-utilsque pode ser feito muito simplesmente 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);
  }
}

Em sua postagem, você menciona que deseja agrupar vários controles de formulário em um único componente. Aqui está um exemplo completo de como fazer isso com 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);
  }
}

Então você pode usar <app-location>com [(ngModel)], [formControl], validadores customizados - tudo o que você pode fazer com os controles suportes angulares fora da caixa.



-1

Por que criar um novo acessador de valor quando você pode usar o ngModel interno. Sempre que você está criando um componente personalizado que tem uma entrada [ngModel] nele, já estamos instanciando um ControlValueAccessor. E esse é o acessor de que precisamos.

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;
    }
}

Use como:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>

Embora pareça promissor, como você está chamando de super, está faltando um "estende"
— Dave Nottage

1
Sim, não copiei todo o meu código aqui e esqueci de remover o super ().
— Nishant

9
Além disso, de onde vem outerNgModel? Esta resposta seria melhor servida com o código completo
— Dave Nottage

De acordo com angular.io/docs/ts/latest/api/core/index/… innerNgModel é definido emngAfterViewInit
— Matteo Suppo

2
Isso não funciona de jeito nenhum. innerNgModel nunca é inicializado, outerNgModel nunca é declarado e ngModel passado para o construtor nunca é usado.
— user2350838

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.