首页 文章

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

提问于
浏览
15

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

<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 Material提供的指导更多地是使用原始元素创建新的表单控件,而不是利用/包装现有的Angular Material表单控件 .

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

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

2 回答

  • 3

    我将使用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>
    

    这是我的 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();
        }
      }
    }
    

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

    简单 input case

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

    autocomplete input case

    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

    想要为自动完成创建包装器组件时遇到了同样的问题 . 下面是我的实现,它以反应式和模板驱动的形式工作 . 为了实现这一目标,您需要实现 ControlValueAccessor . 如果您还要进行某些验证,则还可以实现 Validator 接口 .

    即使表单控件实际上无效,我确实遇到了 mat-form-field 未被标记为无效的问题 . This评论问题“如果FormField被自定义组件包装,则不应用样式”和this相关的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>
    

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

相关问题