Кутовий 2 введення користувацької форми


93

Як я можу створити власний компонент, який би працював так само, як власний <input>тег? Я хочу, щоб мій власний контроль форми міг підтримувати ngControl, ngForm, [(ngModel)].

Як я розумію, мені потрібно впровадити деякі інтерфейси, щоб моє власне управління формою працювало так само, як рідне.

Крім того, схоже, директива ngForm прив'язується лише до <input>тегу, чи не так? Як я можу з цим боротися?


Дозвольте пояснити, навіщо мені це взагалі потрібно. Я хочу обернути декілька вхідних елементів, щоб зробити їх здатними працювати разом як єдиний вхід. Чи є інший спосіб впоратися з цим? Ще раз: я хочу зробити цей контроль таким же, як рідний. Перевірка, ngForm, ngModel двостороння прив'язка та інші.

ps: я використовую Typescript.


1
Більшість відповідей застаріли щодо поточних версій Angular. Подивіться stackoverflow.com/a/41353306/2176962
hgoebl

Відповіді:


85

Насправді є дві речі для реалізації:

  • Компонент, який забезпечує логіку компонента форми. Він не потребує введення, оскільки він буде наданий 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.tagsконтролю стає falseавтоматично.

Детальніше див. У цій статті (розділ "Компонент, сумісний з NgModel"):


Дякую! Ти неймовірний! Як ви думаєте - чи насправді цей спосіб чудовий? Я маю на увазі: не використовуйте елементи введення і не створюйте власні елементи керування, такі як: <textfield>, <dropdown>? Це "кутовий" спосіб?
Максим Фомін

1
Я б сказав, якщо ви хочете реалізувати своє власне поле у ​​формі (щось на замовлення), використовуйте цей підхід. В іншому випадку використовуйте власні елементи HTML. Тим не менш, якщо ви хочете модулювати спосіб відображення вводу / тексту / вибір (наприклад, з Bootstrap3), ви можете використовувати ng-вміст. Дивіться цю відповідь: stackoverflow.com/questions/34950950/…
Тьєррі Темплієр

3
Вище не вказано коду та є деякі розбіжності, наприклад, "removeLabel" замість "removeLabel". Дивіться тут повний робочий приклад. Дякую Тьєррі за те, що подав там початковий приклад!
Blue

1
Знайшов, імпортуйте з @ angular / forms замість @ angular / common, і він працює. імпортувати {NG_VALUE_ACCESSOR, ControlValueAccessor} з '@ angular / forms';
Cagatay Civici

1
це посилання також має бути корисним ..
рефактор

110

Я не розумію, чому кожен приклад, який я знаходжу в Інтернеті, повинен бути таким складним. Пояснюючи нову концепцію, я думаю, що завжди найкраще мати максимально простий, робочий приклад. Я трохи перегнав його:

HTML для зовнішньої форми з використанням компонента, що реалізує ngModel:

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

Автономний компонент (немає окремого класу "accessor" - можливо, я втрачаю суть):

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 Звичайно, одна секунда
Девід

6
Щоб це працювало з новим, @angular/formsпросто оновіть імпорт: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
Постачальник () не підтримується в Angular2 Final. Натомість, MakeProvider () return {provide: 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
Хоча це посилання може відповісти на питання, краще включити сюди основні частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться.
Максиміліан Аст

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

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], призначені для користувача валідатори - все , що можна зробити за допомогою кнопок управління кутові опори з коробки.



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

Хоча це виглядає багатообіцяючим, оскільки, коли ви телефонуєте супер, є відсутність "extends"
Дейв Ноттедж

1
Так, я не скопіював тут весь свій код і забув видалити super ().
Нішант

9
Крім того, звідки походить externalNgModel? Цю відповідь краще подати із повним кодом
Dave Nottage

Відповідно до angular.io/docs/ts/latest/api/core/index/… innerNgModel визначено вngAfterViewInit
Маттео Суппо

2
Це взагалі не працює. innerNgModel ніколи не ініціалізується, externalNgModel ніколи не оголошується, а ngModel, переданий конструктору, ніколи не використовується.
користувач2350838

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.