Jak mogę stworzyć komponent niestandardowy, który będzie działał jak <input>
tag natywny ? Chcę, aby moja niestandardowa kontrolka formularza obsługiwała ngControl, ngForm, [(ngModel)].
Jak rozumiem, muszę zaimplementować kilka interfejsów, aby moja własna formant formularza działała jak natywna.
Wygląda na to, że dyrektywa ngForm wiąże się tylko z <input>
tagiem, czy to prawda? Jak sobie z tym poradzić?
Pozwól mi wyjaśnić, dlaczego w ogóle tego potrzebuję. Chcę owinąć kilka elementów wejściowych, aby mogły współpracować jako jedno wejście. Czy jest inny sposób, aby sobie z tym poradzić? Jeszcze raz: chcę, aby ta kontrolka była jak natywna. Validation, ngForm, ngModel dwukierunkowe wiązanie i inne.
ps: Używam Typescript.
typescript
angular
Maksim Fomin
źródło
źródło
Odpowiedzi:
W rzeczywistości należy wdrożyć dwie rzeczy:
ngModel
samodzielnieControlValueAccessor
, który zaimplementuje most między tym komponentem angModel
/ngControl
Weźmy próbkę. Chcę zaimplementować komponent zarządzający listą tagów dla firmy. Komponent pozwoli na dodawanie i usuwanie tagów. Chcę dodać weryfikację, aby upewnić się, że lista tagów nie jest pusta. Zdefiniuję to w moim komponencie, jak opisano poniżej:
(...) 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] }); } }
TagsComponent
Komponent definiuje logikę dodawania i usuwania elementów wtags
liście.@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 = ''; } }
Jak widać, w tym komponencie nie ma wejścia, ale
setValue
jeden (nazwa nie jest tutaj ważna). Użyjemy go później do podania wartości z elementungModel
do komponentu. Ten komponent definiuje zdarzenie, które ma być powiadamiane o aktualizacji stanu komponentu (listy tagów).Zaimplementujmy teraz połączenie między tym komponentem a
ngModel
/ngControl
. Odpowiada to dyrektywie implementującejControlValueAccessor
interfejs. Dostawca musi być zdefiniowany dla tego akcesora wartości względemNG_VALUE_ACCESSOR
tokenu (nie zapomnij go użyć,forwardRef
ponieważ dyrektywa jest zdefiniowana później).Dyrektywa dołączy detektor zdarzeń do
tagsChange
zdarzenia hosta (tj. Komponentu, do którego jest dołączona dyrektywa, tj.TagsComponent
).onChange
Metoda zostanie wywołana, gdy wystąpi zdarzenie. Ta metoda odpowiada metodzie zarejestrowanej przez Angular2. W ten sposób będzie świadomy zmian i odpowiednio aktualizuje powiązaną kontrolkę formularza.Plik
writeValue
Jest wywoływana, gdy wartość związana wngForm
jest aktualizowana. Po wstrzyknięciu dołączonego komponentu (tj. TagsComponent), będziemy mogli wywołać go w celu przekazania tej wartości (zobacz poprzedniąsetValue
metodę).Nie zapomnij podać
CUSTOM_VALUE_ACCESSOR
w wiązaniach dyrektywy.Oto pełny kod niestandardowego
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; } }
W ten sposób, gdy usunę całą
tags
firmę, plikvalid
atrybutcompanyForm.controls.tags
kontroli staje sięfalse
automatycznie.Więcej informacji można znaleźć w tym artykule (sekcja „Komponent zgodny z NgModel”):
źródło
<textfield>
,<dropdown>
? Czy jest to sposób „kanciasty”?Nie rozumiem, dlaczego każdy przykład, który znajduję w Internecie, musi być tak skomplikowany. Przy wyjaśnianiu nowej koncepcji myślę, że zawsze najlepiej jest mieć jak najprostszy, działający przykład. Trochę to przedestylowałem:
HTML dla zewnętrznego formularza za pomocą komponentu implementującego ngModel:
EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield>
Samodzielny komponent (brak oddzielnej klasy „akcesor” - być może brakuje mi sensu):
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; } }
W rzeczywistości właśnie wyabstrahowałem to wszystko do klasy abstrakcyjnej, którą teraz rozszerzam o każdy komponent, którego potrzebuję, aby użyć ngModel. Dla mnie jest to mnóstwo kodów ogólnych i standardowych, bez których mogę się obejść.
Edycja: oto jest:
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 }; }
Oto komponent, który go używa: (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>
źródło
@angular/forms
wystarczy zaktualizować import:import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
CORE_DIRECTIVES
i dodawać,@Component
ponieważ są one dostarczane domyślnie od czasu wersji ostatecznej Angular2. Jednak według mojego IDE „Konstruktory klas pochodnych muszą zawierać wywołanie„ super ”.”, Więc musiałem dodaćsuper();
do konstruktora mojego komponentu.W tym linku znajduje się przykład dla wersji 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; } }
Następnie możemy użyć tej kontrolki niestandardowej w następujący sposób:
<form> <custom-input name="someValue" [(ngModel)]="dataModel"> Enter data: </custom-input> </form>
źródło
Przykład Thierry'ego jest pomocny. Oto importy potrzebne do uruchomienia 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';
źródło
Napisałem bibliotekę, która pomaga zredukować pewne boilerplate dla tego przypadku:
s-ng-utils
. Niektóre inne odpowiedzi podają przykład zawijania pojedynczej kontrolki formularza. Korzystanies-ng-utils
co można zrobić w bardzo prosty sposób za pomocą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); } }
W swoim poście wspominasz, że chcesz zawinąć wiele kontrolek formularza w jeden składnik. Oto pełny przykład, jak to zrobić z
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); } }
Można wtedy skorzystać
<app-location>
z[(ngModel)]
,[formControl]
, niestandardowe validators - wszystko, co można zrobić z kontroli kątowe wsporniki po wyjęciu z pudełka.źródło
Możesz również rozwiązać ten problem za pomocą dyrektywy @ViewChild. Daje to rodzicowi pełny dostęp do wszystkich zmiennych składowych i funkcji wstrzykniętego dziecka.
Zobacz: Jak uzyskać dostęp do pól wejściowych wtryskiwanego komponentu formularza
źródło
Po co tworzyć nowe akcesorium wartości, skoro można użyć wewnętrznego ngModel. Za każdym razem, gdy tworzysz komponent niestandardowy, który ma dane wejściowe [ngModel], już tworzymy wystąpienie ControlValueAccessor. I to jest akcesorium, którego potrzebujemy.
szablon:
<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>
Składnik:
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; } }
Użyj jako:
<my-input class="col-sm-6" label="First Name" name="firstname" [(ngModel)]="user.name" required minlength="5" maxlength="20"></my-input>
źródło
innerNgModel
jest zdefiniowane wngAfterViewInit
Jest to dość łatwe do zrobienia
ControlValueAccessor
NG_VALUE_ACCESSOR
.Możesz przeczytać ten artykuł, aby utworzyć proste niestandardowe pole Utwórz niestandardowy komponent pola wejściowego za pomocą Angular
źródło