首页 文章

Angular 2自定义表单输入

提问于
浏览
73

如何创建自定义组件,就像本机 <input> 标签一样?我想让我的自定义表单控件能够支持ngControl,ngForm,[(ngModel)] .

据我所知,我需要实现一些接口,使我自己的表单控件工作就像本机一样 .

此外,似乎ngForm指令仅绑定 <input> 标签,这是对的吗?我该如何处理?


让我解释为什么我需要这个 . 我想包装几个输入元素,使它们能够作为一个单独的输入一起工作 . 还有其他方法可以解决这个问题吗?还有一次:我想让这个控件像原生控件一样 . 验证,ngForm,ngModel双向绑定等 .

ps:我使用的是Typescript .

7 回答

  • 0

    实际上,有两件事要实现:

    • 提供表单组件逻辑的组件 . 它不是输入,因为它将由 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>&nbsp;|&nbsp;</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注册的方法 . 这样,它将意识到相应的表单控件的更改和更新 .

    更新 ngForm 中的值时将调用 writeValue . 在注入附加的组件(即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 时, companyForm.controls.tags 控件的 valid 属性自动变为 false .

    有关更多详细信息,请参阅此文章(“与NgModel兼容的组件”一节):

  • 71

    使用 ControlValueAccessor NG_VALUE_ACCESSOR 非常容易 .

    您可以阅读这篇文章来制作一个简单的自定义字段Create Custom Input Field Component with Angular

  • 71

    蒂埃里的例子很有帮助 . 以下是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指令解决此问题 . 这使父级可以完全访问注入子级的所有成员变量和函数 .

    见:How to access input fields of injected form component

  • 13

    我不明白为什么我在互联网上找到的每个例子都必须如此复杂 . 在解释一个新概念时,我认为最简单的工作示例总是最好的 . 我把它蒸馏了一下:

    使用实现ngModel的组件的外部表单的HTML:

    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>
    
  • 0

    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>
    
  • -1

    为什么要在使用内部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>
    

相关问题