typescript - Angular 2 özel form girişi
Yerel <input> etiketi gibi çalışacak özel bir bileşeni nasıl oluşturabilirim? Özel form kontrolümün ngControl, ngForm, [(ngModel)]'i destekleyebilmesini istiyorum.
Anladığım kadarıyla, kendi form kontrolümün yerel bir kontrol gibi çalışmasını sağlamak için bazı arayüzleri uygulamam gerekiyor.
Ayrıca, ngForm yönergesinin sadece <input> etiketi için bağlandığı görünüyor, doğru mu? Bununla nasıl başa çıkabilirim?
Buna neden ihtiyaç duyduğumu açıklamama izin verin. Birkaç girdi öğesini bir arada tek bir girdi gibi çalışabilecek şekilde sarmak istiyorum. Bununla başa çıkmanın başka bir yolu var mı? Bir kez daha: Bu kontrolü tıpkı yerel kontrol gibi yapmak istiyorum. Doğrulama, ngForm, ngModel iki yönlü bağlama ve diğerleri.
Not: Typescript kullanıyorum.
Cevap #1
İnternette bulduğum her örneğin neden bu kadar karmaşık olması gerektiğini anlamıyorum. Yeni bir kavramı açıklarken, bence her zaman mümkün olan en basit, çalışan örneğe sahip olmak en iyisidir. Bunu biraz basitleştirdim:
ngModel uygulayan bir bileşen kullanarak harici form için HTML:
EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>
Bağımsız bileşen (ayrı bir 'erişimci' sınıfı yok - belki de konuyu kaçırıyorum):
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; }
}
Aslında, tüm bu şeyleri sadece bir soyut sınıfa soyutladım ve şimdi ngModel kullanmam gereken her bileşenle bunu genişletiyorum. Benim için bu çok fazla yük ve gereksiz kod, ki buna gerek duymam.
Düzenle: İşte bu:
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
};
}
İşte bunu kullanan bir bileşen: (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>
Cevap #2
Aslında uygulanacak iki şey var:
- Form bileşeninizin mantığını sağlayan bir bileşen. Bir girişe ihtiyacı yoktur çünkü bu,
ngModeltarafından kendisi sağlanacaktır - Bu bileşen ile
ngModel/ngControlarasında köprüyü uygulayacak özel birControlValueAccessor
Bir örnek alalım. Bir şirket için etiket listesi yöneten bir bileşen uygulamak istiyorum. Bu bileşen etiket eklemeye ve çıkarmaya izin verecek. Etiketler listesinin boş olmadığını garanti etmek için bir doğrulama eklemek istiyorum. Bunu bileşenimde aşağıda açıklandığı şekilde tanımlayacağım:
(...)
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 bileşeni, tags listesindeki öğeleri eklemek ve kaldırmak için mantığı tanımlar.
@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 !== -1) {
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 = '';
}
}
Gördüğünüz gibi, bu bileşende bir girdi yok ama bir setValue bileşeni var (isim burada önemli değil). Daha sonra ngModel'den bileşene değeri sağlamak için bunu kullanıyoruz. Bu bileşen, bileşenin durumu (etiketler listesi) güncellendiğinde bildirim yapmak için bir olay tanımlar.
Şimdi bu bileşen ile ngModel/ngControl arasındaki bağlantıyı uygulayalım. Bu, ControlValueAccessor arayüzünü uygulayan bir direktife karşılık gelir. Bu değer erişici için NG_VALUE_ACCESSOR token'ına karşı bir sağlayıcı tanımlanmalıdır (direktif daha sonra tanımlandığı için forwardRef kullanmayı unutmayın).
Yönerge, ana öğenin (yani yönergenin eklendiği bileşen, yani TagsComponent) tagsChange olayına bir olay dinleyicisi ekleyecektir. Olay gerçekleştiğinde onChange metodu çağrılacaktır. Bu metod, Angular2 tarafından kaydedilen metoda karşılık gelir. Bu şekilde, değişikliklerden haberdar olacak ve ilgili form kontrolünü buna göre güncelleyecektir.
writeValue, ngForm içinde bağlı olan değer güncellendiğinde çağrılır. Eklendiği bileşeni (örn. TagsComponent) enjekte ettikten sonra, bu değeri geçirmek için onu çağırabileceğiz (önceki setValue yöntemine bakınız).
Directive'in bağlamlarında CUSTOM_VALUE_ACCESSOR sağlamayı unutmayın.
İşte özel ControlValueAccessor'ın tam kodu:
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; }
}
Bu şekilde, şirketin tüm etiketlerini kaldırdığımda, companyForm.controls.tags kontrolünün valid özniteliği otomatik olarak false olur.
Daha fazla detay için bu makaleye ("NgModel-uyumlu bileşen" bölümü) bakın:
- http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/
Cevap #3
RC5 sürümü için bu bağlantıda bir örnek var: 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;
}
}
Böylece bu özel kontrolü şu şekilde kullanabiliriz:
<form>
<custom-input name="someValue"
[(ngModel)]="dataModel">
Enter data:
</custom-input>
</form>
