Angular 2自定义表单输入

Mak*_*min 81 typescript angular

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

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

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


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

ps:我使用的是Typescript.

Dav*_*vid 90

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

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

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>
Run Code Online (Sandbox Code Playgroud)

自包含的组件(没有单独的'访问者'类 - 也许我错过了这一点):

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; }
}
Run Code Online (Sandbox Code Playgroud)

事实上,我刚刚将所有这些内容抽象为一个抽象类,我现在使用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
  };
}
Run Code Online (Sandbox Code Playgroud)

这是一个使用它的组件:(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;
}
Run Code Online (Sandbox Code Playgroud)

HTML:

<div class="form-group">
  <label class="control-label" >{{displaytext}}</label>
  <input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md">
</div>
Run Code Online (Sandbox Code Playgroud)

  • 为了使它与新的`@ angular/forms`一起使用,只需更新导入:`import {ControlValueAccessor,NG_VALUE_ACCESSOR}来自'@ angular/forms'` (6认同)
  • Angular2 Final不支持Provider().相反,让MakeProvider()返回{provide:NG_VALUE_ACCESSOR,useExisting:forwardRef(()=> type),multi:true}; (6认同)
  • 您不再需要导入 `CORE_DIRECTIVES` 并将它们添加到 `@Component` 中,因为它们是自 Angular2 final 以来默认提供的。但是,根据我的 IDE,“派生类的构造函数必须包含一个 'super' 调用。”,所以我不得不将 `super();` 添加到我的组件的构造函数中。 (3认同)

Thi*_*ier 79

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

  • 提供表单组件逻辑的组件.它不是输入,因为它将由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]
    });
  }
}
Run Code Online (Sandbox Code Playgroud)

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 = '';
  }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,此组件中没有输入,只有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; }
}
Run Code Online (Sandbox Code Playgroud)

这样当我删除所有tags公司时,控件的valid属性companyForm.controls.tags就会false自动变为.

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

  • 以上是缺少代码并且存在一些差异,例如'removeLabel'而不是'removeLabel'.有关完整的工作示例,请参见[here](https://github.com/baloodevil/angular2-custom-form-control).感谢Thierry把最初的例子放在那里! (3认同)

小智 17

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

然后我们可以使用此自定义控件,如下所示:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
Run Code Online (Sandbox Code Playgroud)

  • 虽然此链接可能会回答这个问题,但最好在此处包含答案的基本部分并提供参考链接.如果链接的页面发生更改,则仅链接的答案可能会无效. (4认同)

Blu*_*lue 5

蒂埃里的例子很有帮助.以下是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';
Run Code Online (Sandbox Code Playgroud)