Angular 2 自定义表单输入

Posted

技术标签:

【中文标题】Angular 2 自定义表单输入【英文标题】:Angular 2 custom form input 【发布时间】:2016-04-29 04:28:29 【问题描述】:

如何创建与原生 <input> 标签一样的自定义组件?我想让我的自定义表单控件能够支持 ngControl、ngForm、[(ngModel)]。

据我了解,我需要实现一些接口以使我自己的表单控件像原生控件一样工作。

另外,ngForm 指令似乎只绑定<input> 标签,对吗?我该如何处理?


让我解释一下为什么我需要这个。我想包装几个输入元素,使它们能够作为一个输入一起工作。有没有其他方法来处理它? 再来一次:我想让这个控件就像原生控件一样。验证、ngForm、ngModel 双向绑定等。

ps:我使用 Typescript。

【问题讨论】:

关于当前 Angular 版本的大多数答案都已过时。看看***.com/a/41353306/2176962 【参考方案1】:

我不明白为什么我在互联网上找到的每个示例都必须如此复杂。在解释一个新概念时,我认为最好有最简单、最有效的例子。我把它提炼了一点:

使用实现 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>

【讨论】:

有趣的是,自 RC2 以来,接受的答案似乎已经停止工作,我尝试了这种方法并且它有效,但不知道为什么。 @3urdoch 当然,一秒 要使其与新的 @angular/forms 一起使用,只需更新导入:import ControlValueAccessor, NG_VALUE_ACCESSOR from '@angular/forms' Provider() 在 Angular2 Final 中不受支持。相反,让 MakeProvider() return provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true ; 您不再需要导入 CORE_DIRECTIVES 并将它们添加到 @Component 中,因为它们是自 Angular2 最终版以来默认提供的。但是,根据我的 IDE,“派生类的构造函数必须包含 'super' 调用。”,所以我必须将 super(); 添加到组件的构造函数中。【参考方案2】:

其实有两件事要实现:

提供表单组件逻辑的组件。它不需要输入,因为它将由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 !== -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 = '';
  

如您所见,此组件中没有输入,而是一个 setValue (名称在这里并不重要)。我们稍后使用它来将来自ngModel 的值提供给组件。该组件定义了一个事件,当组件的状态(标签列表)更新时进行通知。

现在让我们实现这个组件和ngModel/ngControl之间的链接。这对应于实现ControlValueAccessor 接口的指令。必须针对 NG_VALUE_ACCESSOR 令牌为此值访问器定义提供程序(不要忘记使用 forwardRef,因为该指令是在之后定义的)。

该指令将在主机的tagsChange 事件上附加一个事件侦听器(即附加该指令的组件,即TagsComponent)。事件发生时将调用onChange 方法。此方法对应于 Angular2 注册的方法。这样它就会知道相关的表单控件的变化并相应地更新。

writeValuengForm 中绑定的值更新时被调用。在注入附加的组件(即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 兼容组件”部分):

http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/

【讨论】:

谢谢!你真棒!你怎么想 - 这种方式真的好吗?我的意思是:不要使用输入元素并制作自己的控件,例如:&lt;textfield&gt;&lt;dropdown&gt;?这是“棱角分明”的方式吗? 我想说如果你想在表单中实现自己的字段(自定义),请使用这种方法。否则使用原生 HTML 元素。也就是说,如果您想模块化显示输入/文本区域/选择的方式(例如使用 Bootstrap3),您可以利用 ng-content。看到这个答案:***.com/questions/34950950/… 上面缺少代码并且有一些差异,比如'removeLabel'而不是'removeLabel'。有关完整的工作示例,请参阅here。感谢蒂埃里把最初的例子放在那里! 找到它,从@angular/forms 导入而不是@angular/common 并且它可以工作。从“@angular/forms”导入 NG_VALUE_ACCESSOR, ControlValueAccessor; this 链接也应该有帮助..【参考方案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】:

我编写了一个库来帮助减少这种情况下的一些样板:s-ng-utils。其他一些答案给出了包装 single 表单控件的示例。使用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);
  

然后,您可以将 &lt;app-location&gt;[(ngModel)][formControl]、自定义验证器一起使用 - 您可以使用 Angular 开箱即用的控件进行的所有操作。

【讨论】:

【参考方案6】:

当您可以使用内部 ngModel 时,为什么要创建新的值访问器。每当您创建一个包含 input[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>

【讨论】:

虽然这看起来很有希望,但由于您调用的是 super,因此缺少“扩展” 是的,我没有在这里复制我的整个代码并且忘记删除 super()。 另外,outerNgModel 是从哪里来的?完整的代码会更好地提供这个答案 根据angular.io/docs/ts/latest/api/core/index/…innerNgModel定义在ngAfterViewInit 这根本不起作用。 innerNgModel 永远不会被初始化,outerNgModel 永远不会被声明,传递给构造函数的 ngModel 永远不会被使用。【参考方案7】:

使用ControlValueAccessor NG_VALUE_ACCESSOR 很容易做到这一点。

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

【讨论】:

以上是关于Angular 2 自定义表单输入的主要内容,如果未能解决你的问题,请参考以下文章

自定义 angular2 表单输入组件,在组件内具有两种方式绑定和验证

Angular 2 - 单元测试绑定到嵌套的自定义表单控件

如何使用 Angular 重置自定义表单控件

Angular2- RC6 自定义表单控件不起作用

数字输入的角度自定义指令需要访问父反应表单 - FormGroup 和 FormContols

使用自定义验证和动态值的 Angular 表单