Angular 2自定义表单输入


89

如何创建可以像本机<input>标记一样工作的自定义组件?我想让我的自定义窗体控件能够支持ngControl,ngForm,[(ngModel)]。

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

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


让我解释一下为什么我完全需要这个。我想包装几个输入元素,使它们能够作为一个输入一起工作。还有其他方法可以解决吗?再过一次:我想像本地控件一样进行此控件。验证时,ngForm,ngModel等两种方式绑定。

ps:我用打字稿。


1
对于当前的Angular版本,大多数答案已过时。看看stackoverflow.com/a/41353306/2176962
hgoebl

Answers:


82

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

  • 提供表单组件逻辑的组件。它不会因为它的输入将被提供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注册的方法。这样,它将知道更改并相应地更新关联的表单控件。

当中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.tagsfalse自动变为。

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


谢谢!你真棒!您的想法-这种方式真的可以吗?我的意思是:不要使用输入元素并使自己的控件如:<textfield><dropdown>?这是“成角度的”方式吗?
Maksim Fomin

1
我想说的是,如果您想以表单(自定义的形式)实现自己的字段,请使用此方法。否则,请使用本机HTML元素。就是说,如果您想模块化显示输入/文本区域/选择的方式(例如,使用Bootstrap3),则可以利用ng-content。看到这个答案:stackoverflow.com/questions/34950950/...
亨利TEMPLIER

3
上面缺少代码,并且有一些差异,例如“ removeLabel”而不是“ removeLabel”。有关完整的工作示例,请参见此处。感谢蒂埃里(Thierry)将最初的例子放在这里!
蓝色

1
找到它,从@ angular / forms而不是@ angular / common导入,它可以工作。从“ @ angular / forms”导入{NG_VALUE_ACCESSOR,ControlValueAccessor};
Cagatay Civici

1
链接也将有所帮助
。.–重构

109

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

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

1
有趣的是,自RC2以来,可接受的答案似乎已停止工作,我尝试了这种方法,但仍有效,但不确定为什么。
3urdoch

1
@ 3urdoch当然,一秒钟
David

6
为了使其与新的@angular/forms刚刚更新的进口一起使用: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
Angular2 Final不支持Provider()。而是让MakeProvider()返回{提供:NG_VALUE_ACCESSOR,useExisting:forwardRef(()=> type),multi:true};
DSoa

2
您不再需要导入CORE_DIRECTIVES和添加它们,@Component因为自Angular2 final起默认提供它们。但是,根据我的IDE,“派生类的构造函数必须包含一个'super'调用。”,因此我必须添加super();到组件的构造函数中。
约瑟夫·韦伯

16

此链接中有一个针对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
尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能会失效。
Maximilian Ast

5

蒂埃里(Thierry)的例子很有帮助。这是TagValuesValueAccessor运行所需的导入...

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

1

我写了一个库来帮助减少这种情况的样板: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]自定义验证器一起使用-Angular支持的控件可以直接使用。



-1

当可以使用内部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>

尽管这看起来很有希望,但由于您称之为“超级”,所以缺少一个“扩展”
Dave Nottage

1
是的,我没有在这里复制我的整个代码,却忘记了删除super()。
Nishant

9
另外,outerNgModel来自哪里?完整的代码可以更好地解决此问题
Dave Nottage '17

根据angular.io/docs/ts/latest/api/core/index/… innerNgModel定义于ngAfterViewInit
Matteo Suppo,

2
这根本不起作用。永远不会初始化innerNgModel,永远不会声明externalNgModel,并且永远不会使用传递给构造函数的ngModel。
user2350838

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.