如何组成Angular Material表单控件组件

Phi*_*rdt 18 angular-material angular-material2 angular

下面的代码显示了一个允许选择美国州的自动填充表单控件.

  <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>
Run Code Online (Sandbox Code Playgroud)

但是,如果在我的应用程序中我有许多需要这种类型输入的位置,那么将它转换为一个组件(指令?)是有意义的,其中所有样板都不需要重复.但是,我仍然希望能够在模板驱动或模型驱动的表单中使用它,并允许容器组件改变占位符,验证等.

实现这一目标的简单而有力的方法是什么?

我尝试过为Angular推荐的一般方法,但它们没有考虑Angular Material的各种要求.例如,需要实现MatFormFieldControl.Angular Material提供的指导更多地是使用原始元素创建新的表单控件,而不是利用/包装现有的Angular Material表单控件.

目标是能够以这样的形式做这样的事情:

<mat-form-field>
    <lookup-state placeholder="State of Residence" required="true" formControlName="resState">
    </lookup-state>
</mat-form-field>
Run Code Online (Sandbox Code Playgroud)

Luc*_*gni 7

我将使用Angular Material粘贴我的组件示例.我创建了一个自定义输入组件(两种情况:简单输入或自动完成):

这是我的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>
Run Code Online (Sandbox Code Playgroud)

这是我的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();
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我将举一个使用它们的例子:

简单的输入案例

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

自动完成输入案例

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;
      }
    }
Run Code Online (Sandbox Code Playgroud)
<seg-input #BirthTown [(ngModel)]="BirthTownNgModel" placeholder="BirthTown"  [autocomplete]="autocompleteSourceBirthTown" [value]="textBirthTown" required>
</seg-input>
Run Code Online (Sandbox Code Playgroud)

希望会有所帮助


and*_*jer 5

我想为自动完成创建包装器组件时遇到了同样的问题。下面是我在反应式和模板驱动形式中工作的实现。为了实现这一点,您需要实施ControlValueAccessor. 如果您还有一些验证要移到组件中,您也可以实现该Validator接口。

我确实遇到了一个问题,mat-form-field即使表单控件实际上是无效的,也没有被标记为无效。“对这个问题的评论,如果FormField通过自定义组件包不被应用的样式和”相关plunker帮我解决这个问题。

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>
Run Code Online (Sandbox Code Playgroud)

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;
  }
}
Run Code Online (Sandbox Code Playgroud)