Nhập biểu mẫu tùy chỉnh Angular 2


89

Làm cách nào để tạo thành phần tùy chỉnh hoạt động giống như <input>thẻ gốc ? Tôi muốn điều khiển biểu mẫu tùy chỉnh của mình có thể hỗ trợ ngControl, ngForm, [(ngModel)].

Như tôi hiểu, tôi cần triển khai một số giao diện để điều khiển biểu mẫu của riêng tôi hoạt động giống như bản gốc.

Ngoài ra, có vẻ như chỉ thị ngForm chỉ liên kết với <input>thẻ, điều này có đúng không? Làm thế nào tôi có thể đối phó với điều đó?


Hãy để tôi giải thích tại sao tôi cần cái này. Tôi muốn kết hợp một số yếu tố đầu vào để làm cho chúng có thể hoạt động cùng nhau như một đầu vào duy nhất. Có cách nào khác để đối phó với điều đó? Một lần nữa: Tôi muốn đặt điều khiển này giống như điều khiển gốc. Validation, ngForm, ngModel ràng buộc hai chiều và khác.

ps: Tôi sử dụng Typescript.


1
Hầu hết các câu trả lời đã lỗi thời về các phiên bản Angular hiện tại. Có một cái nhìn tại stackoverflow.com/a/41353306/2176962
hgoebl

Câu trả lời:


82

Trên thực tế, có hai điều cần thực hiện:

  • Một thành phần cung cấp logic của thành phần biểu mẫu của bạn. Nó không phải là đầu vào vì nó sẽ được cung cấp bởi ngModelchính nó
  • Tùy chỉnh ControlValueAccessorsẽ triển khai cầu nối giữa thành phần này và ngModel/ngControl

Hãy lấy một mẫu. Tôi muốn triển khai một thành phần quản lý danh sách các thẻ cho một công ty. Thành phần sẽ cho phép thêm và bớt các thẻ. Tôi muốn thêm xác thực để đảm bảo rằng danh sách thẻ không trống. Tôi sẽ xác định nó trong thành phần của mình như được mô tả bên dưới:

(...)
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]
    });
  }
}

Thành TagsComponentphần xác định logic để thêm và bớt các phần tử trong tagsdanh sách.

@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 = '';
  }
}

Như bạn thấy, không có đầu vào trong thành phần này nhưng một setValuemột (tên không quan trọng ở đây). Chúng tôi sử dụng nó sau này để cung cấp giá trị từ ngModelthành phần. Thành phần này xác định một sự kiện để thông báo khi trạng thái của thành phần (danh sách thẻ) được cập nhật.

Bây giờ hãy triển khai liên kết giữa thành phần này và ngModel/ ngControl. Điều này tương ứng với một chỉ thị thực hiện ControlValueAccessorgiao diện. Một nhà cung cấp phải được xác định cho trình truy cập giá trị này đối với NG_VALUE_ACCESSORmã thông báo (đừng quên sử dụng forwardRefvì lệnh được xác định sau).

Chỉ thị sẽ đính kèm một bộ nghe sự kiện trên tagsChangesự kiện của máy chủ (tức là thành phần mà chỉ thị được đính kèm, tức là TagsComponent). Các onChangephương pháp sẽ được gọi khi sự kiện xảy ra. Phương thức này tương ứng với phương thức được Angular2 đăng ký. Bằng cách này, nó sẽ nhận biết được các thay đổi và cập nhật tương ứng với kiểm soát biểu mẫu liên quan.

Các writeValueđược gọi khi giá trị ràng buộc trong ngFormđược cập nhật. Sau khi đã tiêm thành phần được đính kèm vào (tức là TagsComponent), chúng ta sẽ có thể gọi nó để chuyển giá trị này (xem setValuephương pháp trước ).

Đừng quên cung cấp CUSTOM_VALUE_ACCESSORcác ràng buộc của chỉ thị.

Đây là mã hoàn chỉnh của tùy chỉnh 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; }
}

Bằng cách này khi tôi xóa tất cả các thành viên tagscủa công ty, validthuộc tính của companyForm.controls.tagsquyền kiểm soát sẽ falsetự động trở thành .

Xem bài viết này (phần "Thành phần tương thích với NgModel") để biết thêm chi tiết:


Cảm ơn! Bạn thật tuyệt vời! Bạn nghĩ thế nào - cách này có thực sự ổn không? Ý tôi là: không sử dụng các yếu tố đầu vào và tạo các điều khiển riêng như: <textfield>, <dropdown>? Đây có phải là cách "góc cạnh"?
Maksim Fomin

1
Tôi sẽ nói nếu bạn muốn triển khai trường của riêng mình trong biểu mẫu (một cái gì đó tùy chỉnh), hãy sử dụng cách tiếp cận này. Nếu không, hãy sử dụng các phần tử HTML gốc. Điều đó nói rằng nếu bạn muốn mô-đun hóa cách hiển thị input / textarea / select (ví dụ với Bootstrap3), bạn có thể tận dụng ng-content. Xem câu trả lời này: stackoverflow.com/questions/34950950/…
Thierry Templier,

3
Ở trên thiếu mã và có một số khác biệt, như 'removeLabel' thay vì 'removeLabel'. Xem ở đây để biết một ví dụ làm việc hoàn chỉnh. Cảm ơn Thierry đã đưa ra ví dụ ban đầu!
Blue

1
Tìm thấy nó, nhập từ @ angle / form thay vì @ angle / common và nó hoạt động. nhập {NG_VALUE_ACCESSOR, ControlValueAccessor} từ '@ angle / form';
Cagatay Civici

1
này liên kết cũng sẽ hữu ích ..
Refactor

109

Tôi không hiểu tại sao mọi ví dụ tôi tìm thấy trên internet đều phải phức tạp như vậy. Khi giải thích một khái niệm mới, tôi nghĩ tốt nhất nên có một ví dụ đơn giản nhất có thể. Tôi đã chắt lọc nó xuống một chút:

HTML cho biểu mẫu bên ngoài sử dụng thành phần triển khai ngModel:

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>

Thành phần độc lập (không có lớp 'trình truy cập' riêng biệt - có thể tôi đang thiếu điểm):

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

Trên thực tế, tôi vừa trừu tượng hóa tất cả những thứ này thành một lớp trừu tượng mà bây giờ tôi mở rộng với mọi thành phần tôi cần sử dụng ngModel. Đối với tôi, đây là một tấn mã trên không và mã soạn sẵn mà tôi có thể làm mà không có.

Chỉnh sửa: Đây rồi:

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

Đây là một thành phần sử dụng nó: (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
Thật thú vị, câu trả lời được chấp nhận dường như đã ngừng hoạt động kể từ RC2, tôi đã thử cách tiếp cận này và nó hoạt động, mặc dù vậy, không chắc tại sao.
3urdoch

1
@ 3urdoch Chắc chắn, một giây
David

6
Để làm cho nó làm việc với mới @angular/formschỉ cập nhật hàng nhập khẩu: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
Nhà cung cấp () không được hỗ trợ trong Angular2 Final. Thay vào đó, hãy có MakeProvider () return {cung cấp: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa

2
Bạn không cần phải nhập CORE_DIRECTIVESvà thêm chúng vào @Componentnữa vì chúng được cung cấp theo mặc định ngay bây giờ kể từ phiên bản cuối cùng của Angular2. Tuy nhiên, theo IDE của tôi, "Các hàm tạo cho các lớp dẫn xuất phải chứa một lệnh gọi 'siêu'.", Vì vậy tôi phải thêm super();vào hàm tạo của thành phần của mình.
Joseph Webber

16

Có một ví dụ trong liên kết này cho phiên bản 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;
    }

}

Sau đó, chúng tôi có thể sử dụng điều khiển tùy chỉnh này như sau:

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

4
Mặc dù liên kết này có thể trả lời câu hỏi, nhưng tốt hơn hết bạn nên đưa các phần thiết yếu của câu trả lời vào đây và cung cấp liên kết để tham khảo. Các câu trả lời chỉ có liên kết có thể trở nên không hợp lệ nếu trang được liên kết thay đổi.
Maximilian Ast

5

Ví dụ của Thierry rất hữu ích. Dưới đây là các lần nhập cần thiết để TagsValueAccessor chạy ...

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

Tôi đã viết một thư viện mà giúp giảm một số soạn sẵn cho trường hợp này: s-ng-utils. Một số câu trả lời khác đang đưa ra ví dụ về việc gói một điều khiển biểu mẫu duy nhất . Việc sử dụng s-ng-utilscó thể được thực hiện rất đơn giản bằng cách sử dụng 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);
  }
}

Trong bài đăng của bạn, bạn đề cập rằng bạn muốn kết hợp nhiều điều khiển biểu mẫu vào một thành phần duy nhất. Đây là một ví dụ đầy đủ làm điều đó với 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);
  }
}

Sau đó, bạn có thể sử dụng <app-location>với [(ngModel)],, [formControl]các trình xác thực tùy chỉnh - mọi thứ bạn có thể làm với các điều khiển mà Angular hỗ trợ ngoài hộp.



-1

Tại sao phải tạo trình truy cập giá trị mới khi bạn có thể sử dụng ngModel bên trong. Bất cứ khi nào bạn tạo một thành phần tùy chỉnh có đầu vào [ngModel] trong đó, chúng tôi đã khởi tạo một ControlValueAccessor. Và đó là công cụ truy cập chúng tôi cần.

bản mẫu:

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

Thành phần:

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

Sử dụng như là:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>

Mặc dù điều này có vẻ hứa hẹn, vì bạn đang gọi là siêu, nên còn thiếu một từ "mở rộng"
Dave Nottage

1
Đúng, tôi đã không sao chép toàn bộ mã của mình ở đây và quên xóa dấu siêu ().
Nishant

9
Ngoài ra, ngoàiNgModel đến từ đâu? Câu trả lời này sẽ được phục vụ tốt hơn với mã hoàn chỉnh
Dave Nottage

Theo angle.io/docs/ts/latest/api/core/index/… innerNgModel được định nghĩa trongngAfterViewInit
Matteo Suppo

2
Điều này không hoạt động ở tất cả. internalNgModel không bao giờ được khởi tạo, bên ngoàiNgModel không bao giờ được khai báo và ngModel được truyền cho hàm tạo không bao giờ được sử dụng.
user2350838

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.