Ingresso modulo personalizzato angolare 2


89

Come posso creare un componente personalizzato che funzioni come un <input>tag nativo ? Voglio fare in modo che il mio controllo modulo personalizzato sia in grado di supportare ngControl, ngForm, [(ngModel)].

Da quanto ho capito, ho bisogno di implementare alcune interfacce per far funzionare il mio controllo del modulo proprio come quello nativo.

Inoltre, sembra che la direttiva ngForm si colleghi solo per il <input>tag, è giusto? Come posso affrontarlo?


Lascia che ti spieghi perché ne ho bisogno. Voglio racchiudere diversi elementi di input per renderli in grado di lavorare insieme come un unico input. C'è un altro modo per affrontarlo? Ancora una volta: voglio rendere questo controllo proprio come quello nativo. Validation, ngForm, ngModel two way binding e altro.

ps: io uso Typescript.


1
La maggior parte delle risposte sono obsolete per quanto riguarda le attuali versioni di Angular. Date un'occhiata a stackoverflow.com/a/41353306/2176962
hgoebl

Risposte:


82

In effetti, ci sono due cose da implementare:

  • Un componente che fornisce la logica del componente del modulo. Non è un input poiché verrà fornito da ngModelsolo
  • Una consuetudine ControlValueAccessorche implementerà il ponte tra questo componente e ngModel/ngControl

Facciamo un campione. Voglio implementare un componente che gestisce un elenco di tag per un'azienda. Il componente consentirà di aggiungere e rimuovere tag. Voglio aggiungere una convalida per assicurarmi che l'elenco dei tag non sia vuoto. Lo definirò nel mio componente come descritto di seguito:

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

Il TagsComponentcomponente definisce la logica per aggiungere e rimuovere elementi tagsnell'elenco.

@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 = '';
  }
}

Come puoi vedere, non c'è alcun input in questo componente ma setValueuno (il nome non è importante qui). Lo usiamo in seguito per fornire il valore dal ngModelal componente. Questo componente definisce un evento per notificare quando lo stato del componente (l'elenco dei tag) viene aggiornato.

Implementiamo ora il collegamento tra questo componente e ngModel/ ngControl. Ciò corrisponde a una direttiva che implementa l' ControlValueAccessorinterfaccia. È necessario definire un provider per questa funzione di accesso del valore rispetto al NG_VALUE_ACCESSORtoken (non dimenticare di utilizzare forwardRefpoiché la direttiva è definita dopo).

La direttiva collegherà un listener di eventi tagsChangeall'evento dell'host (cioè il componente su cui è attaccata la direttiva, cioè il TagsComponent). Il onChangemetodo verrà chiamato quando si verifica l'evento. Questo metodo corrisponde a quello registrato da Angular2. In questo modo sarà a conoscenza delle modifiche e aggiornerà di conseguenza il controllo del modulo associato.

La writeValuesi chiama quando il valore legato alla ngFormviene aggiornato. Dopo aver iniettato il componente collegato su (ie TagsComponent), potremo chiamarlo per passare questo valore (vedi il setValuemetodo precedente ).

Non dimenticare di fornire il CUSTOM_VALUE_ACCESSORnei collegamenti della direttiva.

Ecco il codice completo della 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; }
}

In questo modo quando rimuovo tutti i dati tagsdell'azienda, l' validattributo del companyForm.controls.tagscontrollo diventa falseautomaticamente.

Consulta questo articolo (sezione "Componente compatibile con NgModel") per maggiori dettagli:


Grazie! Sei fantastico! Come pensi - in questo modo va davvero bene? Voglio dire: non utilizzare elementi di input e creare controller propri come: <textfield>, <dropdown>? È questo modo "angolare"?
Maksim Fomin

1
Direi che se vuoi implementare il tuo campo nel modulo (qualcosa di personalizzato), usa questo approccio. Altrimenti usa elementi HTML nativi. Detto questo, se vuoi modulare il modo di visualizzare input / textarea / select (ad esempio con Bootstrap3), puoi sfruttare ng-content. Vedi questa risposta: stackoverflow.com/questions/34950950/…
Thierry Templier,

3
Quanto sopra manca di codice e presenta alcune discrepanze, come "removeLabel" invece di "removeLabel". Vedi qui per un esempio di lavoro completo. Grazie Thierry per aver messo in circolazione l'esempio iniziale!
Blu

1
Trovato, importa da @ angular / forms invece di @ angular / common e funziona. importa {NG_VALUE_ACCESSOR, ControlValueAccessor} da "@ angular / forms";
Cagatay Civici

1
anche questo collegamento dovrebbe essere utile ..
refactoring

109

Non capisco perché ogni esempio che trovo su Internet debba essere così complicato. Quando si spiega un nuovo concetto, penso che sia sempre meglio avere l'esempio più semplice e funzionante possibile. L'ho distillato un po ':

HTML per il modulo esterno utilizzando il componente che implementa ngModel:

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

Componente autonomo (nessuna classe "accessor" separata - forse mi manca il 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; }
}

In effetti, ho appena estratto tutto questo materiale in una classe astratta che ora estendo con ogni componente di cui ho bisogno per usare ngModel. Per me questo è un sacco di codice overhead e boilerplate di cui posso fare a meno.

Modifica: eccolo:

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

Ecco un componente che lo utilizza: (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
È interessante notare che la risposta accettata sembra aver smesso di funzionare da RC2, ho provato questo approccio e funziona, non so perché.
3urdoch

1
@ 3urdoch Certo, un secondo
David

6
Per farlo funzionare con le nuove @angular/formsimportazioni basta aggiornare: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
Provider () non è supportato in Angular2 Final. Invece, fai in modo che MakeProvider () ritorni {fornire: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa

2
Non è più necessario importarli CORE_DIRECTIVESe aggiungerli in @Componentquanto sono forniti di default ora da Angular2 final. Tuttavia, secondo il mio IDE, "I costruttori per le classi derivate devono contenere una chiamata 'super'.", Quindi ho dovuto aggiungere super();al costruttore del mio componente.
Joseph Webber

16

C'è un esempio in questo collegamento per la versione 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;
    }

}

Siamo quindi in grado di utilizzare questo controllo personalizzato come segue:

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

4
Sebbene questo collegamento possa rispondere alla domanda, è meglio includere le parti essenziali della risposta qui e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia.
Maximilian Ast

5

L'esempio di Thierry è utile. Ecco le importazioni necessarie per l'esecuzione di TagsValueAccessor ...

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

Ho scritto una libreria che aiuta a ridurre un po 'di testo standard per questo caso: s-ng-utils. Alcune delle altre risposte forniscono un esempio di avvolgimento di un singolo controllo del modulo. Usarlo s-ng-utilspuò essere fatto molto semplicemente 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);
  }
}

Nel tuo post dici che vuoi racchiudere più controlli del modulo in un singolo componente. Ecco un esempio completo che lo fa 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);
  }
}

È quindi possibile utilizzare <app-location>con [(ngModel)], [formControl], validatori personalizzati - tutto ciò che si può fare con i controlli supporti angolari, fuori dalla scatola.



-1

Perché creare una nuova funzione di accesso ai valori quando puoi utilizzare il file ngModel interno. Ogni volta che crei un componente personalizzato che contiene un input [ngModel], stiamo già creando un'istanza di ControlValueAccessor. E questo è l'accessorio di cui abbiamo bisogno.

modello:

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

Usare come:

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

Anche se questo sembra promettente, dal momento che stai chiamando super, manca una "estensione"
Dave Nottage,

1
Sì, non ho copiato il mio intero codice qui e ho dimenticato di rimuovere il super ().
Nishant

9
Inoltre, da dove viene outerNgModel? Questa risposta sarebbe meglio fornita con il codice completo
Dave Nottage

Secondo angular.io/docs/ts/latest/api/core/index/… innerNgModel è definito inngAfterViewInit
Matteo Suppo

2
Questo non funziona affatto. innerNgModel non viene mai inizializzato, outerNgModel non viene mai dichiarato e ngModel passato al costruttore non viene mai utilizzato.
user2350838

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.