Bagaimana cara membuat komponen kustom yang berfungsi seperti <input>
tag asli ? Saya ingin membuat kontrol formulir kustom saya dapat mendukung ngControl, ngForm, [(ngModel)].
Seperti yang saya pahami, saya perlu menerapkan beberapa antarmuka agar kontrol formulir saya berfungsi seperti yang asli.
Juga, sepertinya direktif ngForm hanya mengikat untuk <input>
tag, apakah ini benar? Bagaimana saya bisa mengatasinya?
Izinkan saya menjelaskan mengapa saya membutuhkan ini sama sekali. Saya ingin membungkus beberapa elemen masukan agar dapat bekerja bersama sebagai satu masukan. Apakah ada cara lain untuk mengatasinya? Sekali lagi: Saya ingin membuat kontrol ini seperti yang asli. Validasi, ngForm, ngModel mengikat dua arah dan lainnya.
ps: Saya menggunakan Typecript.
sumber
Jawaban:
Sebenarnya, ada dua hal yang harus diterapkan:
ngModel
sendirinyaControlValueAccessor
yang akan mengimplementasikan jembatan antara komponen ini danngModel
/ngControl
Mari kita ambil contoh. Saya ingin menerapkan komponen yang mengelola daftar tag untuk perusahaan. Komponen akan memungkinkan untuk menambah dan menghapus tag. Saya ingin menambahkan validasi untuk memastikan bahwa daftar tag tidak kosong. Saya akan mendefinisikannya di komponen saya seperti yang dijelaskan di bawah ini:
(...) 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] }); } }
The
TagsComponent
komponen mendefinisikan logika untuk menambah dan menghapus elemen dalamtags
daftar.@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 = ''; } }
Seperti yang Anda lihat, tidak ada input di komponen ini kecuali
setValue
satu (namanya tidak penting di sini). Kami menggunakannya nanti untuk memberikan nilai daringModel
ke komponen. Komponen ini mendefinisikan peristiwa untuk diberitahukan ketika status komponen (daftar tag) diperbarui.Mari kita implementasikan sekarang tautan antara komponen ini dan
ngModel
/ngControl
. Ini sesuai dengan arahan yang mengimplementasikanControlValueAccessor
antarmuka. Penyedia harus ditentukan untuk pengakses nilai ini terhadapNG_VALUE_ACCESSOR
token (jangan lupa untuk menggunakanforwardRef
karena direktif ditentukan setelahnya).Direktif akan melampirkan event listener pada
tagsChange
acara host (yaitu komponen direktif dilampirkan, yaituTagsComponent
). TheonChange
metode akan dipanggil saat peristiwa itu terjadi. Metode ini sesuai dengan yang didaftarkan oleh Angular2. Dengan cara ini ia akan mengetahui perubahan dan pembaruan yang sesuai dengan kontrol formulir yang terkait.The
writeValue
disebut ketika nilai terikat dalamngForm
diperbarui. Setelah memasukkan komponen yang dilampirkan (mis. TagsComponent), kita akan dapat memanggilnya untuk meneruskan nilai ini (lihatsetValue
metode sebelumnya ).Jangan lupa untuk memberikan
CUSTOM_VALUE_ACCESSOR
di binding dari direktif tersebut.Berikut ini kode lengkap 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; } }
Dengan cara ini ketika saya menghapus semua
tags
perusahaan,valid
atributcompanyForm.controls.tags
kontrol menjadifalse
secara otomatis.Lihat artikel ini (bagian "komponen yang kompatibel dengan NgModel") untuk detail selengkapnya:
sumber
<textfield>
,<dropdown>
? Apakah ini cara "bersudut"?Saya tidak mengerti mengapa setiap contoh yang saya temukan di internet harus begitu rumit. Saat menjelaskan konsep baru, saya pikir yang terbaik adalah memiliki contoh yang paling sederhana dan berfungsi. Saya telah menyaringnya sedikit:
HTML untuk formulir eksternal menggunakan komponen yang mengimplementasikan ngModel:
EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield>
Komponen mandiri (tidak ada kelas 'pengakses' terpisah - mungkin saya melewatkan intinya):
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; } }
Sebenarnya, saya baru saja mengabstraksi semua hal ini ke kelas abstrak yang sekarang saya perluas dengan setiap komponen yang saya perlukan untuk menggunakan ngModel. Bagi saya ini adalah banyak kode overhead dan boilerplate yang dapat saya lakukan tanpanya.
Edit: Ini dia:
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 }; }
Berikut komponen yang menggunakannya: (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>
sumber
@angular/forms
perbarui impor:import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
CORE_DIRECTIVES
dan menambahkannya@Component
lagi karena sudah disediakan secara default sekarang sejak final Angular2. Namun, menurut IDE saya, "Konstruktor untuk kelas turunan harus berisi panggilan 'super'.", Jadi saya harus menambahkansuper();
ke konstruktor komponen saya.Ada contoh di tautan ini untuk versi 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; } }
Kami kemudian dapat menggunakan kontrol kustom ini sebagai berikut:
<form> <custom-input name="someValue" [(ngModel)]="dataModel"> Enter data: </custom-input> </form>
sumber
Teladan Thierry sangat membantu. Berikut adalah impor yang diperlukan untuk TagsValueAccessor untuk menjalankan ...
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';
sumber
Saya menulis sebuah perpustakaan yang membantu mengurangi beberapa boilerplate untuk kasus ini:
s-ng-utils
. Beberapa jawaban lainnya memberikan contoh pembungkusan kontrol bentuk tunggal . Menggunakannyas-ng-utils
dapat dilakukan dengan sangat sederhana menggunakanWrappedFormControlSuperclass
:@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); } }
Di posting Anda, Anda menyebutkan bahwa Anda ingin menggabungkan beberapa kontrol formulir menjadi satu komponen. Berikut adalah contoh lengkap melakukan itu dengan
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); } }
Anda kemudian dapat menggunakan
<app-location>
dengan[(ngModel)]
,,[formControl]
validator khusus - semua yang dapat Anda lakukan dengan kontrol yang didukung Angular di luar kotak.sumber
Anda juga bisa menyelesaikan ini dengan petunjuk @ViewChild. Ini memberi induk akses penuh ke semua variabel anggota dan fungsi turunan yang diinjeksi.
Lihat: Cara mengakses bidang masukan dari komponen formulir yang diinjeksi
sumber
Mengapa membuat pengakses nilai baru jika Anda dapat menggunakan ngModel bagian dalam. Setiap kali Anda membuat komponen kustom yang memiliki input [ngModel] di dalamnya, kami sudah membuat instance ControlValueAccessor. Dan itulah aksesor yang kami butuhkan.
template:
<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>
Komponen:
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; } }
Digunakan sebagai:
<my-input class="col-sm-6" label="First Name" name="firstname" [(ngModel)]="user.name" required minlength="5" maxlength="20"></my-input>
sumber
innerNgModel
didefinisikan dalamngAfterViewInit
Ini cukup mudah dilakukan
ControlValueAccessor
NG_VALUE_ACCESSOR
.Anda dapat membaca artikel ini untuk membuat custom field sederhana Membuat Komponen Custom Input Field dengan Angular
sumber