Input bentuk kustom sudut 2

93

Bagaimana cara membuat komponen kustom yang berfungsi seperti <input>tag asli ? Saya ingin membuat kontrol formulir kustom saya dapat mendukung ngControl, ngForm, [(ngModel)].

Seperti yang saya pahami, saya perlu menerapkan beberapa antarmuka agar kontrol formulir saya berfungsi seperti yang asli.

Juga, sepertinya direktif ngForm hanya mengikat untuk <input>tag, apakah ini benar? Bagaimana saya bisa mengatasinya?


Izinkan saya menjelaskan mengapa saya membutuhkan ini sama sekali. Saya ingin membungkus beberapa elemen masukan agar dapat bekerja bersama sebagai satu masukan. Apakah ada cara lain untuk mengatasinya? Sekali lagi: Saya ingin membuat kontrol ini seperti yang asli. Validasi, ngForm, ngModel mengikat dua arah dan lainnya.

ps: Saya menggunakan Typecript.

Maksim Fomin
sumber
1
Sebagian besar jawaban sudah usang terkait versi Angular saat ini. Lihat stackoverflow.com/a/41353306/2176962
hgoebl

Jawaban:

85

Sebenarnya, ada dua hal yang harus diterapkan:

  • Komponen yang menyediakan logika komponen formulir Anda. Ini tidak memerlukan masukan karena akan disediakan dengan ngModelsendirinya
  • Sebuah kebiasaan ControlValueAccessoryang akan mengimplementasikan jembatan antara komponen ini dan ngModel/ngControl

Mari kita ambil contoh. Saya ingin menerapkan komponen yang mengelola daftar tag untuk perusahaan. Komponen akan memungkinkan untuk menambah dan menghapus tag. Saya ingin menambahkan validasi untuk memastikan bahwa daftar tag tidak kosong. Saya akan mendefinisikannya di komponen saya seperti yang dijelaskan di bawah ini:

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

The TagsComponentkomponen mendefinisikan logika untuk menambah dan menghapus elemen dalam tagsdaftar.

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

Seperti yang Anda lihat, tidak ada input di komponen ini kecuali setValuesatu (namanya tidak penting di sini). Kami menggunakannya nanti untuk memberikan nilai dari ngModelke komponen. Komponen ini mendefinisikan peristiwa untuk diberitahukan ketika status komponen (daftar tag) diperbarui.

Mari kita implementasikan sekarang tautan antara komponen ini dan ngModel/ ngControl. Ini sesuai dengan arahan yang mengimplementasikan ControlValueAccessorantarmuka. Penyedia harus ditentukan untuk pengakses nilai ini terhadap NG_VALUE_ACCESSORtoken (jangan lupa untuk menggunakan forwardRefkarena direktif ditentukan setelahnya).

Direktif akan melampirkan event listener pada tagsChangeacara host (yaitu komponen direktif dilampirkan, yaitu TagsComponent). The onChangemetode akan dipanggil saat peristiwa itu terjadi. Metode ini sesuai dengan yang didaftarkan oleh Angular2. Dengan cara ini ia akan mengetahui perubahan dan pembaruan yang sesuai dengan kontrol formulir yang terkait.

The writeValuedisebut ketika nilai terikat dalam ngFormdiperbarui. Setelah memasukkan komponen yang dilampirkan (mis. TagsComponent), kita akan dapat memanggilnya untuk meneruskan nilai ini (lihat setValuemetode sebelumnya ).

Jangan lupa untuk memberikan CUSTOM_VALUE_ACCESSORdi binding dari direktif tersebut.

Berikut ini kode lengkap custom 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; }
}

Dengan cara ini ketika saya menghapus semua tagsperusahaan, validatribut companyForm.controls.tagskontrol menjadi falsesecara otomatis.

Lihat artikel ini (bagian "komponen yang kompatibel dengan NgModel") untuk detail selengkapnya:

Thierry Templier
sumber
Terima kasih! Anda luar biasa! Bagaimana menurut Anda - apakah cara ini sebenarnya baik-baik saja? Maksud saya: jangan gunakan elemen input dan buat kontrol sendiri seperti: <textfield>, <dropdown>? Apakah ini cara "bersudut"?
Maksim Fomin
1
Saya akan mengatakan jika Anda ingin menerapkan bidang Anda sendiri dalam bentuk (sesuatu yang khusus), gunakan pendekatan ini. Jika tidak, gunakan elemen HTML asli. Artinya, jika Anda ingin memodulasi cara untuk menampilkan input / textarea / select (misalnya dengan Bootstrap3), Anda dapat memanfaatkan ng-content. Lihat jawaban ini: stackoverflow.com/questions/34950950/…
Thierry Templier
3
Kode di atas tidak ada dan memiliki beberapa perbedaan, seperti 'removeLabel', bukan 'removeLabel'. Lihat di sini untuk contoh kerja lengkap. Terima kasih Thierry karena telah meletakkan contoh awal di luar sana!
Biru
1
Menemukannya, mengimpor dari @ angular / forms daripada @ angular / common dan berhasil. impor {NG_VALUE_ACCESSOR, ControlValueAccessor} dari '@ angular / forms';
Cagatay Civici
1
link ini juga akan membantu ..
refactor
110

Saya tidak mengerti mengapa setiap contoh yang saya temukan di internet harus begitu rumit. Saat menjelaskan konsep baru, saya pikir yang terbaik adalah memiliki contoh yang paling sederhana dan berfungsi. Saya telah menyaringnya sedikit:

HTML untuk formulir eksternal menggunakan komponen yang mengimplementasikan ngModel:

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

Komponen mandiri (tidak ada kelas 'pengakses' terpisah - mungkin saya melewatkan intinya):

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

Sebenarnya, saya baru saja mengabstraksi semua hal ini ke kelas abstrak yang sekarang saya perluas dengan setiap komponen yang saya perlukan untuk menggunakan ngModel. Bagi saya ini adalah banyak kode overhead dan boilerplate yang dapat saya lakukan tanpanya.

Edit: Ini dia:

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

Berikut komponen yang menggunakannya: (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>
David
sumber
1
Menariknya, jawaban yang diterima tampaknya telah berhenti berfungsi sejak RC2, saya mencoba pendekatan ini dan berhasil, meskipun tidak yakin mengapa.
3urdoch
1
@ 3urdoch Tentu, satu detik
David
6
Untuk membuatnya bekerja dengan yang baru @angular/formsperbarui impor: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk
6
Provider () tidak didukung di Angular2 Final. Sebaliknya, buat MakeProvider () return {sediakan: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa
2
Anda tidak perlu mengimpor CORE_DIRECTIVESdan menambahkannya @Componentlagi karena sudah disediakan secara default sekarang sejak final Angular2. Namun, menurut IDE saya, "Konstruktor untuk kelas turunan harus berisi panggilan 'super'.", Jadi saya harus menambahkan super();ke konstruktor komponen saya.
Joseph Webber
16

Ada contoh di tautan ini untuk versi 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;
    }

}

Kami kemudian dapat menggunakan kontrol kustom ini sebagai berikut:

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>
Dániel Kis
sumber
4
Meskipun tautan ini mungkin menjawab pertanyaan, lebih baik menyertakan bagian penting dari jawaban di sini dan menyediakan tautan untuk referensi. Jawaban link saja bisa menjadi tidak valid jika halaman tertaut berubah.
Maximilian Ast
5

Teladan Thierry sangat membantu. Berikut adalah impor yang diperlukan untuk TagsValueAccessor untuk menjalankan ...

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';
Biru
sumber
1

Saya menulis sebuah perpustakaan yang membantu mengurangi beberapa boilerplate untuk kasus ini: s-ng-utils. Beberapa jawaban lainnya memberikan contoh pembungkusan kontrol bentuk tunggal . Menggunakannya s-ng-utilsdapat dilakukan dengan sangat sederhana menggunakan 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);
  }
}

Di posting Anda, Anda menyebutkan bahwa Anda ingin menggabungkan beberapa kontrol formulir menjadi satu komponen. Berikut adalah contoh lengkap melakukan itu dengan 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);
  }
}

Anda kemudian dapat menggunakan <app-location>dengan [(ngModel)],, [formControl]validator khusus - semua yang dapat Anda lakukan dengan kontrol yang didukung Angular di luar kotak.

Eric Simonton
sumber
-1

Mengapa membuat pengakses nilai baru jika Anda dapat menggunakan ngModel bagian dalam. Setiap kali Anda membuat komponen kustom yang memiliki input [ngModel] di dalamnya, kami sudah membuat instance ControlValueAccessor. Dan itulah aksesor yang kami butuhkan.

template:

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

Komponen:

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

Digunakan sebagai:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>
Nishant
sumber
Meskipun ini terlihat menjanjikan, karena Anda menelepon super, ada "perpanjangan" yang hilang
Dave Nottage
1
Yup, saya tidak menyalin seluruh kode saya di sini dan lupa menghapus super ().
Nishant
9
Juga, dari mana asal outerNgModel? Jawaban ini akan lebih baik disajikan dengan kode lengkap
Dave Nottage
Menurut angular.io/docs/ts/latest/api/core/index/… innerNgModel didefinisikan dalamngAfterViewInit
Matteo Suppo
2
Ini tidak bekerja sama sekali. innerNgModel tidak pernah diinisialisasi, outerNgModel tidak pernah dideklarasikan, dan ngModel yang diteruskan ke konstruktor tidak pernah digunakan.
pengguna2350838