web-dev-qa-db-fra.com

Comment composer Angular Material control control components

Le code ci-dessous montre un contrôle de formulaire de saisie semi-automatique qui permet la sélection d'un état américain.

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

Cependant, si dans mon application j'ai de nombreux endroits où ce type d'entrée est requis, il serait logique de le transformer en composant (directive?) Où tout le passe-partout n'a pas besoin d'être répété. Cependant, j'aimerais toujours pouvoir l'utiliser dans des formulaires basés sur un modèle ou sur un modèle et permettre aux espaces réservés, aux validations, etc. d'être modifiés par le composant conteneur.

Quel est un moyen simple et robuste d'y parvenir?

J'ai essayé les approches générales recommandées pour Angular mais elles ne prennent pas en compte les diverses exigences de Angular Material. Par exemple, la nécessité d'implémenter MatFormFieldControl. Les conseils fournis par Angular Material vise davantage à créer un nouveau contrôle de formulaire en utilisant des éléments primitifs plutôt qu'à utiliser/envelopper les contrôles de formulaire Angular Material).

L'objectif est de pouvoir faire quelque chose comme ça sous une forme:

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

je vais coller mon exemple de composant en utilisant Angular Material. J'ai créé un composant Input personnalisé (deux cas: entrée simple ou saisie semi-automatique):

c'est mon 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>

c'est mon 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();
    }
  }
}

Maintenant, je vais mettre un exemple d'utilisation des deux:

simple cas d'entrée

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

cas de saisie semi-automatique

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>

l'espoir aidera

7
Luca Taccagni

J'ai eu le même problème lorsque je voulais créer un composant wrapper pour une saisie semi-automatique. Ci-dessous est mon implémentation qui fonctionne sous des formes réactives et basées sur des modèles. Pour ce faire, vous devez implémenter ControlValueAccessor. Si vous avez également une certaine validation que vous souhaitez déplacer dans le composant, vous pouvez également implémenter l'interface Validator.

J'ai rencontré un problème avec le mat-form-field n'est pas marqué comme invalide même si le contrôle de formulaire n'était pas valide. This commentez le problème " Les styles ne sont pas appliqués si FormField est enveloppé par un composant personnalisé" et this plunker associé m'a aidé à résoudre ce problème il.

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