Как я могу создать собственный компонент, который будет работать как собственный <input>
тег? Я хочу, чтобы мой настраиваемый элемент управления формой мог поддерживать ngControl, ngForm, [(ngModel)].
Насколько я понимаю, мне нужно реализовать некоторые интерфейсы, чтобы мой собственный элемент управления формой работал так же, как собственный.
Кроме того, похоже, что директива ngForm привязана только к <input>
тегу, это правильно? Как я могу с этим справиться?
Позвольте мне объяснить, зачем мне это вообще нужно. Я хочу обернуть несколько элементов ввода, чтобы они могли работать вместе как один ввод. Есть ли другой способ справиться с этим? Еще раз: я хочу сделать этот элемент управления таким же, как родной. Проверка, двусторонняя привязка ngForm, ngModel и другие.
ps: я использую Typescript.
источник
Ответы:
На самом деле нужно реализовать две вещи:
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 = ''; } }
Как видите, в этом компоненте нет ввода, кроме
setValue
одного (имя здесь не важно). Мы используем его позже, чтобы передать значение из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») для более подробной информации:
источник
<textfield>
,<dropdown>
? Это «угловатый» способ?Я не понимаю, почему каждый пример, который я нахожу в Интернете, должен быть таким сложным. Я считаю, что при объяснении новой концепции всегда лучше иметь самый простой из возможных рабочий пример. Я немного его переработал:
HTML для внешней формы с использованием компонента, реализующего ngModel:
EmailExternal=<input [(ngModel)]="email"> <inputfield [(ngModel)]="email"></inputfield>
Автономный компонент (нет отдельного класса-аксессуара - возможно, я упускаю суть):
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>
источник
@angular/forms
импортом, просто обновите:import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
CORE_DIRECTIVES
и добавлять их в@Component
файл, поскольку они предоставляются по умолчанию, начиная с Angular2 final. Однако, согласно моей IDE, «Конструкторы для производных классов должны содержать вызов super», поэтому мне пришлось добавитьsuper();
в конструктор моего компонента.В этой ссылке есть пример для версии 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>
источник
Пример Тьерри полезен. Вот импорт, который необходим для запуска 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';
источник
Я написал библиотеку , которая помогает уменьшить некоторые шаблонные для этого случая:
s-ng-utils
. В некоторых других ответах приводится пример упаковки одного элемента управления формой. Используяs-ng-utils
это можно сделать очень просто с помощью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); } }
В своем сообщении вы упоминаете, что хотите объединить несколько элементов управления формой в один компонент. Вот полный пример этого с
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); } }
Затем вы можете использовать
<app-location>
с[(ngModel)]
,[formControl]
, пользовательские валидаторы - все , что можно сделать с помощью кнопок управления угловые опоры из коробки.источник
Вы также можете решить эту проблему с помощью директивы @ViewChild. Это дает родителю полный доступ ко всем переменным-членам и функциям внедренного потомка.
См .: Как получить доступ к полям ввода внедренного компонента формы
источник
Зачем создавать новый метод доступа к значениям, если вы можете использовать внутренний 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>
источник
innerNgModel
определено вngAfterViewInit
Сделать это довольно просто
ControlValueAccessor
NG_VALUE_ACCESSOR
.Вы можете прочитать эту статью, чтобы создать простое настраиваемое поле.Создайте компонент настраиваемого поля ввода с помощью Angular.
источник