2017-05-26 27 views
12

Chciałbym utworzyć niestandardowy element formularza z interfejsem ControlValueAccessor w Angular 2+. Ten element byłby opakowaniem nad <select>. Czy jest możliwe propagowanie właściwości formControl do owiniętego elementu? W moim przypadku stan sprawdzania poprawności nie jest propagowany do zagnieżdżonego zaznaczenia, jak widać na załączonym zrzucie ekranu.Czy mogę uzyskać dostęp do formControl mojego niestandardowego ControlValueAccessor w Angular 2+?

enter image description here

Mój składnik jest dostępna w następujący sposób:

const OPTIONS_VALUE_ACCESSOR: any = { 
    multi: true, 
    provide: NG_VALUE_ACCESSOR, 
    useExisting: forwardRef(() => OptionsComponent) 
    }; 

    @Component({ 
    providers: [OPTIONS_VALUE_ACCESSOR], 
    selector: 'inf-select[name]', 
    templateUrl: './options.component.html' 
    }) 
    export class OptionsComponent implements ControlValueAccessor, OnInit { 

    @Input() name: string; 
    @Input() disabled = false; 
    private propagateChange: Function; 
    private onTouched: Function; 

    private settingsService: SettingsService; 
    selectedValue: any; 

    constructor(settingsService: SettingsService) { 
    this.settingsService = settingsService; 
    } 

    ngOnInit(): void { 
    if (!this.name) { 
    throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>'); 
    } 
    } 

    writeValue(obj: any): void { 
    this.selectedValue = obj; 
    } 

    registerOnChange(fn: any): void { 
    this.propagateChange = fn; 
    } 

    registerOnTouched(fn: any): void { 
    this.onTouched = fn; 
    } 

    setDisabledState(isDisabled: boolean): void { 
    this.disabled = isDisabled; 
    } 
    } 

To mój szablon komponent:

<select class="form-control" 
    [disabled]="disabled" 
    [(ngModel)]="selectedValue" 
    (ngModelChange)="propagateChange($event)"> 
    <option value="">Select an option</option> 
    <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description"> 
    {{option.description}} 
    </option> 
    </select> 
+0

można odtworzyć go w plunker? – yurzui

Odpowiedz

5

SAMPLE PLUNKER

widzę dwie opcje:

  1. propagować błędów w składniku FormControl do <select>FormControl gdy wartość <select>FormControl zmienia
  2. propagować weryfikacje składnika FormControl do <select>FormControl

Poniżej następujących zmiennych są dostępne:

  • selectModel jest NgModel z <select>
  • formControl jest FormControl składnika otrzymanego jako argument

Wariant 1: propagacji błędów

ngAfterViewInit(): void { 
    this.selectModel.control.valueChanges.subscribe(() => { 
     this.selectModel.control.setErrors(this.formControl.errors); 
    }); 
    } 

Wariant 2: propagują Walidatorów

ngAfterViewInit(): void { 
    this.selectModel.control.setValidators(this.formControl.validator); 
    this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator); 
    } 

Różnica między nimi polega na tym, że propagowanie błędów oznacza posiadanie już błędów, natomiast opcja sekund polega na ponownym uruchomieniu walidatorów. Niektóre z nich, podobnie jak walidatory asynchroniczne, mogą być zbyt kosztowne do wykonania.

Propagowanie wszystkich właściwości?

Nie istnieje ogólne rozwiązanie do propagowania wszystkich właściwości. Różne właściwości są ustalane na podstawie różnych dyrektyw lub innych środków, a zatem mają inny cykl życia, co oznacza, że ​​wymagają szczególnej obsługi. Obecne rozwiązanie dotyczy propagowania błędów walidacji i walidatorów. Tam jest wiele dostępnych nieruchomości.

Należy zauważyć, że można uzyskać różne zmiany stanu z instancji FormControl, subskrybując numer FormControl.statusChanges(). W ten sposób można uzyskać informacje o tym, czy kontrola jest VALID,, DISABLED lub PENDING (sprawdzanie poprawności asynchronicznej jest nadal uruchomione).

W jaki sposób walidacja działa pod maską?

Pod maską weryfikatory są stosowane za pomocą dyrektyw (check the source code). Dyrektywy mają providers: [REQUIRED_VALIDATOR] co oznacza, że ​​własne hierarchiczne wtryskiwacz jest używany do rejestracji tej instancji walidatora. Tak więc w zależności od atrybutów zastosowanych w elemencie, dyrektywy doda instancje validatora na wtryskiwacza powiązanego elementu docelowego.

Następnie te weryfikatory są pobierane przez NgModel i FormControlDirective.

Validators oraz akcesorów wartości są pobierane jak:

constructor(@Optional() @Host() parent: ControlContainer, 
       @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>, 
       @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>, 
       @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) 

i odpowiednio:

constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>, 
       @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>, 
       @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) 
       valueAccessors: ControlValueAccessor[]) 

Zauważ, że @Self() jest używany, więc własnego wtryskiwacza (elementu, do którego dyrektywa jest Stosowana) stosuje się w celu otrzymania zależności.

NgModel i FormControlDirective mają instancję FormControl, która faktycznie aktualizuje wartość i wykonuje weryfikatory.

Dlatego głównym punktem interakcji jest instancja FormControl.

Również wszystkie zatwierdzającym lub Akcesory wartości są zarejestrowane w wtryskiwacza elementu, do których są stosowane. Oznacza to, że rodzic nie powinien uzyskać dostęp do wtryskiwacza. Więc byłoby to złe praktyki w celu uzyskania dostępu z bieżącego składnika wtryskiwacz dostarczonego przez <select>.

Kod próbki dla wariantu 1 (łatwo wymieniony przez opcja 2)

Poniższy przykład ma dwa weryfikatorów: jedną, która jest wymagana, a inny, który to wzór, który wymusza na możliwość, aby dopasować opcję „3”.

The PLUNKER

options.component.ts

import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core'; 
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms'; 
import {SettingsService} from '../settings.service'; 

const OPTIONS_VALUE_ACCESSOR: any = { 
    multi: true, 
    provide: NG_VALUE_ACCESSOR, 
    useExisting: forwardRef(() => OptionsComponent) 
}; 

@Component({ 
    providers: [OPTIONS_VALUE_ACCESSOR], 
    selector: 'inf-select[name]', 
    templateUrl: './options.component.html', 
    styleUrls: ['./options.component.scss'] 
}) 
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit { 

    @ViewChild('selectModel') selectModel: NgModel; 
    @Input() formControl: FormControl; 

    @Input() name: string; 
    @Input() disabled = false; 

    private propagateChange: Function; 
    private onTouched: Function; 

    private settingsService: SettingsService; 

    selectedValue: any; 

    constructor(settingsService: SettingsService) { 
    this.settingsService = settingsService; 
    } 

    ngOnInit(): void { 
    if (!this.name) { 
     throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>'); 
    } 
    } 

    ngAfterViewInit(): void { 
    this.selectModel.control.valueChanges.subscribe(() => { 
     this.selectModel.control.setErrors(this.formControl.errors); 
    }); 
    } 

    writeValue(obj: any): void { 
    this.selectedValue = obj; 
    } 

    registerOnChange(fn: any): void { 
    this.propagateChange = fn; 
    } 

    registerOnTouched(fn: any): void { 
    this.onTouched = fn; 
    } 

    setDisabledState(isDisabled: boolean): void { 
    this.disabled = isDisabled; 
    } 
} 

options.component.html

<select #selectModel="ngModel" 
     class="form-control" 
     [disabled]="disabled" 
     [(ngModel)]="selectedValue" 
     (ngModelChange)="propagateChange($event)"> 
    <option value="">Select an option</option> 
    <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description"> 
    {{option.description}} 
    </option> 
</select> 

options.component.SCSS

:host { 
    display: inline-block; 
    border: 5px solid transparent; 

    &.ng-invalid { 
    border-color: purple; 
    } 

    select { 
    border: 5px solid transparent; 

    &.ng-invalid { 
     border-color: red; 
    } 
    } 
} 

Zastosowanie

Określenie wystąpienie FormControl:

export class AppComponent implements OnInit { 

    public control: FormControl; 

    constructor() { 
    this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required])); 
    } 
... 

Bind wystąpienie składowej FormControl:

<inf-select name="myName" [formControl]="control"></inf-select> 

Dummy SettingsService

/** 
* TODO remove this class, added just to make injection work 
*/ 
export class SettingsService { 

    public getOption(name: string): [{ description: string }] { 
    return [ 
     { description: 'option 1' }, 
     { description: 'option 2' }, 
     { description: 'option 3' }, 
     { description: 'option 4' }, 
     { description: 'option 5' }, 
    ]; 
    } 
} 
+0

Hello! Dziękuję za Twoją odpowiedź. Jak jednak uzyskać dostęp do "FormControl", gdy formularz jest budowany za pomocą FormBuilder? W twoim przykładzie komponent byłby wywoływany w ten sposób: ''. –

+0

@SlavaFominII powiedzmy, że tworzymy 'FormGroup' używając' FormBuilder'. Następnie możesz uzyskać kontrolę za pomocą 'formGroup.controls ['someControl']' lub 'formGroup.get ('someControl')'. – andreim

+0

Tak, wiem to, dziękuję. Wyjaśniłem tutaj moje pytanie: https://stackoverflow.com/questions/44731894/get-access-to-formcontrol-from-the-custom-form-component-in-angular –