Angular 2 пользовательских ввода формы
Как создать пользовательский компонент, который будет работать как родной тег <input>
? Я хочу, чтобы мой пользовательский элемент управления формы мог поддерживать ngControl, ngForm, [(ngModel)].
Как я понимаю, мне нужно реализовать некоторые интерфейсы, чтобы моя собственная обработка формы работала так же, как и родная.
Кроме того, похоже, что директива ngForm связывается только с тегом <input>
, это правильно? Как я могу справиться с этим?
Позвольте мне объяснить, зачем мне это вообще нужно. Я хочу обернуть несколько элементов ввода, чтобы они могли работать вместе как один вход. Есть ли другой способ справиться с этим?
Еще один раз: я хочу сделать этот контроль так же, как и собственный. Validation, ngForm, ngModel двусторонняя привязка и другие.
ps: Я использую Typescript.
Ответы
Ответ 1
На самом деле есть две вещи для реализации:
- Компонент, который обеспечивает логику вашего компонента формы. Это не вход, поскольку он будет предоставлен самой
ngModel
- Пользовательский
ControlValueAccessor
, который реализует мост между этим компонентом и ngModel
/ngControl
Возьмем образец. Я хочу реализовать компонент, который управляет списком тегов для компании. Компонент позволит добавлять и удалять теги. Я хочу добавить проверку, чтобы список тегов не был пустым. Я буду определять его в моем компоненте, как описано ниже:
(...)
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
определяет логику добавления и удаления элементов в списке tags
.
@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 = '';
}
}
Как вы можете видеть, в этом компоненте нет ввода, а < <28 > (имя здесь не важно). Мы используем его позже, чтобы предоставить значение от ngModel
к компоненту. Этот компонент определяет событие для уведомления, когда обновляется состояние компонента (список тегов).
Теперь реализуем связь между этим компонентом и ngModel
/ngControl
. Это соответствует директиве, реализующей интерфейс ControlValueAccessor
. Поставщик должен быть определен для этого атрибута доступа к токену NG_VALUE_ACCESSOR
(не забудьте использовать forwardRef
, поскольку директива определена после).
Директива будет прикреплять прослушиватель событий к событию tagsChange
узла (то есть к компоненту, на который прикреплена директива, т.е. TagsComponent
). При возникновении события вызывается метод onChange
. Этот метод соответствует тому, который зарегистрирован Angular2. Таким образом, он будет знать об изменениях и обновлениях, соответствующих соответствующему контролю формы.
Вызывается writeValue
при обновлении значения в ngForm
. После того, как вы добавили компонент, прикрепленный (т.е. TagsComponent), мы сможем вызвать его для передачи этого значения (см. Предыдущий метод setValue
).
Не забудьте указать CUSTOM_VALUE_ACCESSOR
в привязках директивы.
Вот полный код пользовательского 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; }
}
Таким образом, когда я удаляю все tags
компании, атрибут valid
элемента управления companyForm.controls.tags
автоматически становится false
.
Подробнее см. в этой статье (раздел "Компонент, совместимый с NgModel" ):
Ответ 2
Я не понимаю, почему каждый пример, который я нахожу в Интернете, должен быть настолько сложным. Объясняя новую концепцию, я считаю, что всегда лучше иметь самый простой, рабочий пример. Я немного переделал это:
HTML для внешней формы с использованием компонента, реализующего ngModel:
EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>
Автономный компонент (нет отдельного класса "accessor" - может быть, я пропущу точку):
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; }
}
Фактически, я только что отвлек весь этот материал на абстрактный класс, который я теперь распространяю с каждым компонентом, который мне нужен, чтобы использовать ngModel. Для меня это тонна верхнего и стандартного кода, с которым я могу обойтись.
Изменить: Здесь это:
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
};
}
Здесь используется компонент, который использует его: (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>
Ответ 3
Вот пример в этой ссылке для версии 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;
}
}
Затем мы можем использовать этот настраиваемый элемент управления следующим образом:
<form>
<custom-input name="someValue"
[(ngModel)]="dataModel">
Enter data:
</custom-input>
</form>
Ответ 4
Пример Тьерри полезен. Вот импорт, который необходим для запуска функции 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';
Ответ 5
Вы также можете решить эту проблему с помощью директивы @ViewChild. Это дает родительскому полному доступу ко всем переменным-членам и функциям инъецируемого ребенка.
Смотрите: Как получить доступ к полям вводимых компонентов формы
Ответ 6
Зачем создавать новый атрибут доступа, когда вы можете использовать внутренний ngModel. Всякий раз, когда вы создаете пользовательский компонент, который имеет вход [ngModel] в нем, мы уже создаем экземпляр ControlValueAccessor. И что нам нужен аксессор.
шаблон:
<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>
компонент:
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;
}
}
Использовать как:
<my-input class="col-sm-6" label="First Name" name="firstname"
[(ngModel)]="user.name" required
minlength="5" maxlength="20"></my-input>