Açısal 2 özel form girişi


93

Yerel <input>etiket gibi çalışacak özel bileşeni nasıl oluşturabilirim ? Özel form kontrolümün ngControl, ngForm, [(ngModel)] 'i destekleyebilmesini istiyorum.

Anladığım kadarıyla, kendi form kontrolümün yerel olan gibi çalışmasını sağlamak için bazı arayüzler uygulamam gerekiyor.

Ayrıca, ngForm yönergesi yalnızca <input>etiket için bağlanıyor gibi görünüyor , bu doğru mu? Bununla nasıl başa çıkabilirim?


Buna neden ihtiyacım olduğunu açıklayayım. Tek bir girdi olarak birlikte çalışabilmelerini sağlamak için birkaç girdi elemanını kaydırmak istiyorum. Bununla başa çıkmanın başka yolu var mı? Bir kez daha: Bu kontrolü tıpkı yerli kontrol gibi yapmak istiyorum. Doğrulama, ngForm, ngModel iki yönlü bağlama ve diğer.

ps: Typescript kullanıyorum.


1
Mevcut Angular sürümlerle ilgili çoğu yanıtın süresi geçmiştir. Göz at stackoverflow.com/a/41353306/2176962
hgoebl

Yanıtlar:


85

Aslında uygulanacak iki şey vardır:

  • Form bileşeninizin mantığını sağlayan bir bileşen. ngModelKendisi tarafından sağlanacağı için bir girdiye ihtiyaç duymaz.
  • ControlValueAccessorBu bileşen ve ngModel/ veya arasındaki köprüyü uygulayacak bir gelenekngControl

Bir örnek alalım. Bir şirket için bir etiket listesini yöneten bir bileşen uygulamak istiyorum. Bileşen, etiketlerin eklenmesine ve kaldırılmasına izin verecektir. Etiket listesinin boş olmadığından emin olmak için bir doğrulama eklemek istiyorum. Bunu bileşenimde aşağıda açıklandığı gibi tanımlayacağım:

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

TagsComponentBileşen ekleyip öğeleri kaldırmak için mantık tanımlar tagslistede.

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

Gördüğünüz gibi, bu bileşende bir girdi dışında bir girdi var setValue(isim burada önemli değil). Daha sonra ngModelbileşenden bileşene değer sağlamak için kullanırız . Bu bileşen, bileşenin (etiketler listesi) durumu güncellendiğinde bildirilecek bir olay tanımlar.

Şimdi bu bileşen ve ngModel/ arasındaki bağlantıyı uygulayalım ngControl. Bu, ControlValueAccessorarayüzü uygulayan bir yönergeye karşılık gelir . NG_VALUE_ACCESSORBelirteç karşısında bu değer erişimcisi için bir sağlayıcı tanımlanmalıdır ( forwardRefyönerge sonradan tanımlandığı için kullanmayı unutmayın ).

Yönerge tagsChange, ana bilgisayar olayına bir olay dinleyicisi ekleyecektir (yani yönergenin eklendiği bileşen, yani TagsComponent). onChangeOlay meydana geldiğinde yöntemi çağrılır. Bu yöntem, Angular2 tarafından kaydedilene karşılık gelir. Bu şekilde, ilgili form kontrolüne göre değişikliklerin ve güncellemelerin farkında olacaktır.

İçindeki writeValuedeğerin değeri ngFormgüncellendiğinde çağrılır . Eklenen bileşeni (yani, TagsComponent) enjekte ettikten sonra, bu değeri iletmek için onu çağırabileceğiz (önceki setValueyönteme bakın).

CUSTOM_VALUE_ACCESSORDirektifin bağlayıcılarını sağlamayı unutmayın .

İşte özelliğin tam kodu 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öylelikle tagsşirketin tamamını kaldırdığımda valid, companyForm.controls.tagskontrolün niteliği falseotomatik olarak oluyor.

Daha fazla ayrıntı için bu makaleye bakın ("NgModel uyumlu bileşen" bölümü):


Teşekkürler! Harikasın! Nasıl düşünüyorsun - bu yol gerçekten iyi mi? Demek istediğim: giriş öğelerini kullanmayın ve aşağıdaki gibi kendi kontrollerinizi yapmayın: <textfield>, <dropdown>? Bu "köşeli" yol mu?
Maksim Fomin

1
Formda kendi alanınızı uygulamak istiyorsanız (özel bir şey), bu yaklaşımı kullanın derim. Aksi takdirde yerel HTML öğelerini kullanın. Bununla birlikte, input / textarea / select (örneğin Bootstrap3 ile) görüntüleme şeklini modülerleştirmek istiyorsanız, ng-içeriğinden yararlanabilirsiniz. Bu yanıtı görün: stackoverflow.com/questions/34950950/…
Thierry Templier

3
Yukarıdaki kod eksik ve "removeLabel" yerine "removeLabel" gibi bazı tutarsızlıklar var. Tam bir çalışma örneği için buraya bakın . İlk örneği ortaya koyduğu için teşekkürler Thierry!
Mavi

1
Buldum, @ angular / common yerine @ angular / formlarından içe aktarın ve işe yarıyor. {NG_VALUE_ACCESSOR, ControlValueAccessor} öğesini '@ angular / formlar'dan içe aktarın;
Çağatay Civici

1
bu bağlantı da yardımcı olabilir ..
refactor

112

İnternette bulduğum her örneğin neden bu kadar karmaşık olduğunu anlamıyorum. Yeni bir kavramı açıklarken, mümkün olan en basit, işe yarayan örneğe sahip olmanın her zaman en iyisi olduğunu düşünüyorum. Onu biraz damıttım:

NgModel'i uygulayan bileşen kullanan harici form için HTML:

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

Kendi kendine yeten bileşen (ayrı 'erişimci' sınıfı yok - belki de noktayı kaçırıyorum):

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

Aslında, tüm bunları, ngModel'i kullanmak için ihtiyacım olan her bileşenle genişlettiğim soyut bir sınıfa soyutladım. Benim için bu, onsuz yapabileceğim bir ton ek yük ve standart kod.

Düzenleme: İşte:

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

İşte onu kullanan bir bileşen: (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
İlginç bir şekilde, kabul edilen cevap RC2'den beri çalışmayı bırakmış gibi görünüyor, bu yaklaşımı denedim ve işe yarıyor, yine de neden olduğundan emin değilim.
3urdoch

1
@ 3urdoch Tabii, bir saniye
David

6
Yeni @angular/formsgüncelleme içe import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
aktarmalarıyla

6
Provider (), Angular2 Final'de desteklenmiyor. Bunun yerine, MakeProvider () return {sağlayın: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa

2
Angular2 finalinden bu yana varsayılan olarak sağlandığından artık CORE_DIRECTIVESbunları içe aktarmanıza ve eklemenize gerek @Componentyok. Bununla birlikte, IDE'me göre, "Türetilmiş sınıflar için super();kurucular bir 'süper' çağrı içermelidir.", Bu yüzden bileşenimin kurucusuna eklemem gerekiyordu.
Joseph Webber

16

Bu bağlantıda RC5 sürümü için bir örnek var: 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;
    }

}

Daha sonra bu özel kontrolü aşağıdaki gibi kullanabiliriz:

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

4
Bu bağlantı soruyu cevaplayabilirken, cevabın temel kısımlarını buraya eklemek ve referans için bağlantıyı sağlamak daha iyidir. Bağlantılı sayfa değişirse yalnızca bağlantı yanıtları geçersiz hale gelebilir.
Maximilian Ast

5

Thierry'nin örneği faydalıdır. İşte TagsValueAccessor'ın çalışması için gerekli olan içe aktarmalar ...

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

Bu durum için bazı klişe azaltmaya yardımcı olan bir kitaplık yazdı: s-ng-utils. Diğer cevaplardan bazıları, tek bir form kontrolünü kaydırmaya örnek veriyor . s-ng-utilsBunu kullanmak çok basit bir şekilde yapılabilir 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);
  }
}

Gönderinizde birden çok form denetimini tek bir bileşene sarmak istediğinizi belirtmişsiniz. İşte bunu yapmanın tam bir örneğ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);
  }
}

Daha sonra kullanabilirsiniz <app-location>ile [(ngModel)], [formControl]özel doğrulayıcıları, - her şey kutunun dışında kontrolleri ile Açısal destekleri yapabilirsiniz.



-1

İç ngModel'i kullanabildiğinizde neden yeni bir değer erişimcisi yaratmalısınız? İçinde [ngModel] girdisi olan özel bir bileşen oluşturduğunuzda, zaten bir ControlValueAccessor başlatıyoruz. Ve ihtiyacımız olan erişimci bu.

şablon:

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

Bileşen:

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

Kullanım şekli:

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

Bu umut verici görünse de, süper aradığınız için, eksik bir "
uzatmalar

1
Evet, kodumun tamamını buraya kopyalamadım ve super () öğesini kaldırmayı unuttum.
Nishant

9
Ayrıca, OuterNgModel nereden geliyor? Bu yanıt tam kodla sunulması daha iyi olur
Dave Nottage

Göre angular.io/docs/ts/latest/api/core/index/... innerNgModel tanımlanırngAfterViewInit
Matteo Suppo

2
Bu hiç çalışmıyor. innerNgModel asla başlatılmaz, OuterNgModel asla bildirilmez ve yapıcıya geçirilen ngModel asla kullanılmaz.
user2350838

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.