web-dev-qa-db-ja.com

構成方法Angular Material form control components

以下のコードは、米国の州を選択できるオートコンプリートフォームコントロールを示しています。

  <mat-form-field class="example-full-width">
    <input matInput placeholder="State" aria-label="State" [matAutocomplete]="auto" [formControl]="stateCtrl">
    <mat-autocomplete #auto="matAutocomplete">
      <mat-option *ngFor="let state of filteredStates | async" [value]="state.name">
        <img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" />
        <span>{{ state.name }}</span> |
        <small>Population: {{state.population}}</small>
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>

ただし、アプリケーションでこのタイプの入力が必要な場所が多数ある場合、これをすべてのボイラープレートを繰り返す必要のないコンポーネント(ディレクティブ?)に変えることは理にかなっています。ただし、テンプレート駆動型またはモデル駆動型のいずれかでこれを使用し、プレースホルダー、検証などをコンテナコンポーネントごとに変更できるようにしたいと思っています。

これを達成するためのシンプルで堅牢な方法は何ですか?

Angularに推奨される一般的なアプローチを試しましたが、Angular Materialのさまざまな要件を考慮していません。たとえば、MatFormFieldControlを実装する必要があります。 Angularマテリアルは、既存のAngularマテリアルフォームコントロールを利用/ラップするのではなく、プリミティブ要素を使用して新しいフォームコントロールを作成することに重点を置いています。

目的は、次のようなことをフォームで実行できるようにすることです。

<mat-form-field>
    <lookup-state placeholder="State of Residence" required="true" formControlName="resState">
    </lookup-state>
</mat-form-field>
17
Phil Degenhardt

Angular Material。を使用して、コンポーネントの例を貼り付けます。カスタム入力コンポーネントを作成しました(2つの場合:単純な入力またはオートコンプリート):

これは私のInput.component.html

<mat-form-field color="accent" [hideRequiredMarker]="true" [class.mat-form-field-invalid]="hasErrors">
  <ng-container *ngIf="autocomplete">
    <input matInput [matAutocomplete]="auto" [type]="type" [placeholder]="placeholder" [disabled]="isDisabled" [value]="innerValue" (input)="autocompleteHandler($event)" (blur)="autocompleteBlur($event)">
    <mat-autocomplete #auto [displayWith]="displayText" (optionSelected)="updateOption($event)">
      <mat-option *ngFor="let choice of autocompleteChoices | async" [value]="choice">{{ choice.text }}</mat-option>
    </mat-autocomplete>
  </ng-container>
  <input *ngIf="!autocomplete" matInput [type]="type" [placeholder]="placeholder" [disabled]="isDisabled" [value]="innerValue" (input)="inputHandler($event)" (blur)="setTouched()">
</mat-form-field>

これは私のInput.component.ts

import { Component, Input, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgModel } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material';

import { ChoiceList } from '../../../../models/choice-list';
import { ChoiceSource } from '../../../../models/choice-source';
import { getFlagAttribute } from '../../../../utils';
import { HintComponent } from '../hint/hint.component';
import { ErrorsComponent } from '../errors/errors.component';
import { FormField } from '../form-field';
import { ChoiceModel } from '../../../../models/choice-model';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/toPromise';

@Component({
  selector: 'my-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => InputComponent),
    multi: true
  }]
})
export class InputComponent extends FormField implements ControlValueAccessor {
  @Input() type = 'text';
  @Input() placeholder: string;
  @Input() autocomplete: ChoiceSource;

  autocompleteChoices: ChoiceList;

  @Input() set value(value: string) {
    this.innerValue = value == null ? '' : String(value);
  }
  get value() {
    return this.innerValue;
  }

  @Input() set disabled(value: any) {
    this.setDisabledState(getFlagAttribute(value));
  }
  get disabled() {
    return this.isDisabled;
  }

  private changeCallback: Function;
  private touchedCallback: Function;

  isDisabled = false;
  innerValue = '';

  displayText(value: ChoiceModel): string {
    return value.text;
  }

  writeValue(value: any) {
    if (!this.autocomplete) {
      this.value = value;
    }
  }
  registerOnChange(fn: Function) {
    this.changeCallback = fn;
  }
  registerOnTouched(fn: Function) {
    this.touchedCallback = fn;
  }
  setDisabledState(isDisabled: boolean) {
    this.isDisabled = isDisabled;
  }

  inputHandler(event: Event) {
    this.value = (<HTMLInputElement>event.target).value;
    if (this.changeCallback) {
      this.changeCallback(this.value);
    }
  }

  autocompleteHandler(event: Event) {
    const text = (<HTMLInputElement>event.target).value;
    if (this.autocomplete) {
      if (text) {
        this.autocompleteChoices = this.autocomplete(text);
      } else if (this.changeCallback) {
        this.innerValue = '';
        this.changeCallback(null);
      }
    }
  }

  autocompleteBlur(event: Event) {
    (<HTMLInputElement>event.target).value = this.innerValue;
    this.setTouched();
  }

  updateOption(event: MatAutocompleteSelectedEvent) {
    if (this.changeCallback) {
      const { value, text } = event.option.value;
      this.value = text;
      this.changeCallback(value);
    }
  }

  setTouched() {
    if (this.touchedCallback) {
      this.touchedCallback();
    }
  }
}

次に、両方の使用例を示します。

シンプル入力ケース

<my-input type="text" name="myInputName" [(ngModel)]="myNgModel" placeholder="---" required pattern="[a-zA-Zàèìòù\'\s0-9\.]+">
</my-input>

オートコンプリート入力ケース

export myClass implements OnInit, AfterViewInit, ControlValueAccessor, AfterViewChecked {

@ViewChild('BirthTown') BirthTown: InputComponent; //from import

public autocompleteSourceBirthTown: Function;

this.autocompleteSourceBirthTown = (async function(input: string) {
      if (input.trim().length > 2) {
        const towns = await this.generalService.getListBirthTowns(input.trim());
        return towns;
      }
      return [];
    }).bind(this);
    
    // only for text of town
ngAfterViewChecked() {
    if (this.BirthTown && this.BirthTownNgModel) {
      const textTown = this.stateService.getDataBirthTown(this.BirthTownNgModel);
      if (textTown) {
        this.textBirthTown = textTown;
      }
    }
<seg-input #BirthTown [(ngModel)]="BirthTownNgModel" placeholder="BirthTown"  [autocomplete]="autocompleteSourceBirthTown" [value]="textBirthTown" required>
</seg-input>

希望が役立つ

7
Luca Taccagni

オートコンプリート用のラッパーコンポーネントを作成するときにも同じ問題が発生しました。以下は、リアクティブおよびテンプレート駆動型で動作する私の実装です。これを実現するには、ControlValueAccessorを実装する必要があります。コンポーネントに移動したい検証もある場合は、Validatorインターフェイスを実装することもできます。

フォームコントロールが実際に無効であっても、mat-form-fieldが無効としてマークされないという問題に遭遇しました。 This 問題に関するコメント "FormFieldがカスタムコンポーネントでラップされている場合、スタイルは適用されません"および this 関連するプランカー私はそれを修正するのに役立ちました。

autocomplete.component.html:

<mat-form-field>
  <input #input matInput type="text" class="form-control" [matAutocomplete]="autocomplete" (input)="valueChanged($event)" [readonly]="readonly"
    (focus)="$event.target.select()" (blur)="onTouched()">
  <mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="displayFunction" (optionSelected)="onOptionSelected($event)">
    <mat-option *ngFor="let option of filteredOptions" [value]="option">
        {{ displayFunction(option) }}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

autocomplete.component.ts:

import { MatAutocompleteTrigger, MatInput } from '@angular/material';
import {
  Component,
  Input,
  AfterViewInit,
  ViewChild,
  OnChanges,
  SimpleChanges,
  forwardRef,
  Injector
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
import { forbiddenAutocompleteValue } from 'app/shared/directives/validators/autocomplete-validator.directive';

@Component({
  selector: 'pp-autocomplete',
  templateUrl: './autocomplete.component.html',
  styleUrls: ['./autocomplete.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AutocompleteComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => AutocompleteComponent),
      multi: true
    }
  ]
})
export class AutocompleteComponent implements AfterViewInit, OnChanges, ControlValueAccessor, Validator {
  @Input() options: any[] = [];
  @Input() readonly = false;
  @Input() displayFunction: (value: any) => string = this.defaultDisplayFn;
  @Input() filterFunction: (value: any) => any[] = this.defaultFilterFn;

  @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger;
  @ViewChild(MatInput) matInput: MatInput;

  filteredOptions: any[];
  optionSelected = '';
  onChange = (val: any) => {};
  onTouched = () => {};

  constructor(
    private injector: Injector
  ) { }

  ngAfterViewInit() {
    this.trigger.panelClosingActions
      .subscribe(
        e => {
          if (this.trigger.activeOption) {
            const value = this.trigger.activeOption.value;
            this.writeValue(value);
            this.onChange(value);
          }
        }
      );

    // this is needed in order for the mat-form-field to be marked as invalid when the control is invalid
    setTimeout(() => {
      this.matInput.ngControl = this.injector.get(NgControl, null);
    });
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.options) {
      this.filterOptions(this.optionSelected);
    }
  }

  writeValue(obj: any): void {
    if (obj) {
      this.trigger.writeValue(obj);
      this.optionSelected = obj;
      this.filterOptions(obj);
    }
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.matInput.disabled = isDisabled;
    this.trigger.setDisabledState(isDisabled);
  }

  validate(c: AbstractControl): { [key: string]: any; } {
    return forbiddenAutocompleteValue()(c);
  }

  valueChanged(event) {
    const value = event.target.value;
    this.optionSelected = value;
    this.onChange(value);
    this.filterOptions(value);
  }

  onOptionSelected(event) {
    const value = event.option.value;
    this.optionSelected = value;
    this.onChange(value);
    this.filterOptions(value);
  }

  filterOptions(value) {
    this.filteredOptions = this.filterFunction(value);
  }

  private defaultFilterFn(value) {
    let name = value;

    if (value && typeof value === 'object') {
      name = value.name;
    }

    return this.options.filter(
      o => o.name.toLowerCase().indexOf(name ? name.toLowerCase() : '') !== -1
    );
  }

  defaultDisplayFn(value) {
    return value ? value.name : value;
  }
}
3
andbjer