Angular Material - Custom Autocomplete component

时间:2018-02-26 18:02:18

标签: angular autocomplete angular-material

I'm trying to create my own custom angular material component that would be able to work with a mat-form-field control.

Added to that, I'd like the control to use the mat-autocomplete directive.

My aim is simply to create a better-looking mat-autocomplete component with an integrated clear-button and custom css arrow like the following image. I have succesfully obtained it by using the standard component and added what I wanted but now I want to export it into a generic component.

material component aim

I'm using the official angular material documentation to create my own form field control and also another SO post about it which already helped me a lot :

I am currently facing several problems which I believe are linked :

  • My form is not valid even when the value is selected correctly.
  • The placeholder is not setting itself correctly after an option is selected.
  • The auto-complete filter option doesn't work at all
  • The focus does not trigger correctly if I don't click on the input specifically.

I believe my first three issues are caused by the auto-complete value that is not linked to my reactive form correctly.


Here is a direct link to a personnal public repository with the project (since the issue is a bit big to be displayed here) : Git Repository : https://github.com/Tenmak/material.


Basically, the idea is to transform this :

  <mat-form-field>
    <div fxLayout="row">
      <input matInput placeholder="Thématique" [matAutocomplete]="thematicAutoComplete" formControlName="thematique" tabindex="1">

      <div class="mat-select-arrow-wrapper">
        <div class="mat-select-arrow" [ngClass]="{'mat-select-arrow-down': !thematicAutoComplete.isOpen, 'mat-select-arrow-up': thematicAutoComplete.isOpen}"></div>
      </div>
    </div>
    <button mat-button *ngIf="formDossier.get('thematique').value" matSuffix mat-icon-button aria-label="Clear" (click)="formDossier.get('thematique').setValue('')">
      <mat-icon>close</mat-icon>
    </button>

    <mat-hint class="material-hint-error" *ngIf="!formDossier.get('thematique').hasError('required') && formDossier.get('thematique').touched && formDossier.get('thematique').hasError('thematiqueNotFound')">
      <strong>
        Veuillez sélectionner un des choix parmi les options possibles.
      </strong>
    </mat-hint>
  </mat-form-field>

  <mat-autocomplete #thematicAutoComplete="matAutocomplete" [displayWith]="displayThematique">
    <mat-option *ngFor="let thematique of filteredThematiques | async" [value]="thematique">
      <span> {{thematique.code}} </span>
      <span> - </span>
      <span> {{thematique.libelle}} </span>
    </mat-option>
  </mat-autocomplete>

into this :

  <mat-form-field>
    <siga-auto-complete placeholder="Thématique" [tabIndex]="1" [autoCompleteControl]="thematicAutoComplete" formControlName="thematique">
    </siga-auto-complete>

    <mat-hint class="material-hint-error" *ngIf="!formDossier.get('thematique').hasError('required') && formDossier.get('thematique').touched && formDossier.get('thematique').hasError('thematiqueNotFound')">
      <strong>
        Veuillez sélectionner un des choix parmi les options possibles.
      </strong>
    </mat-hint>
  </mat-form-field>

  <mat-autocomplete #thematicAutoComplete="matAutocomplete" [displayWith]="displayThematique">
    <mat-option *ngFor="let thematique of filteredThematiques | async" [value]="thematique">
      <span> {{thematique.code}} </span>
      <span> - </span>
      <span> {{thematique.libelle}} </span>
    </mat-option>
  </mat-autocomplete>

I'm currently working in the "dossiers" folder which displays my initial reactive form. And I'm using my custom component autocomplete.component.ts inside this form directly to replace the first field.

Here is my attempt at the code of the generic component (simplified):

class AutoCompleteInput {
    constructor(public testValue: string) {
    }
}

@Component({
    selector: 'siga-auto-complete',
    templateUrl: './autocomplete.component.html',
    styleUrls: ['./autocomplete.component.scss'],
    providers: [
        {
            provide: MatFormFieldControl,
            useExisting: SigaAutoCompleteComponent
        },
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => SigaAutoCompleteComponent),
            multi: true
        }
    ],
})
export class SigaAutoCompleteComponent implements MatFormFieldControl<AutoCompleteInput>, AfterViewInit, OnDestroy, ControlValueAccessor {
    ...
    parts: FormGroup;
    ngControl = null;

    ...

    @Input()
    get value(): AutoCompleteInput | null {
        const n = this.parts.value as AutoCompleteInput;
        if (n.testValue) {
            return new AutoCompleteInput(n.testValue);
        }
        return null;
    }
    set value(value: AutoCompleteInput | null) {
        // Should set the value in the form through this.writeValue() ??
        console.log(value);
        this.writeValue(value.testValue);
        this.stateChanges.next();
    }

    @Input()
    set formControlName(formName) {
        this._formControlName = formName;
    }
    private _formControlName: string;

    // ADDITIONNAL
    @Input() autoCompleteControl: MatAutocomplete;
    @Input() tabIndex: string;

    private subs: Subscription[] = [];

    constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef) {
        this.subs.push(
            fm.monitor(elRef.nativeElement, true).subscribe((origin) => {
                this.focused = !!origin;
                this.stateChanges.next();
            })
        );

        this.parts = fb.group({
            'singleValue': '',
        });

        this.subs.push(this.parts.valueChanges.subscribe((value: string) => {
            this.propagateChange(value);
        }));
    }

    ngAfterViewInit() {
        // Wrong approach but some idea ?
        console.log(this.autoCompleteControl);
        this.autoCompleteControl.optionSelected.subscribe((event: MatAutocompleteSelectedEvent) => {
            console.log(event.option.value);
            this.value = event.option.value;
        })
    }

    ngOnDestroy() {
        this.stateChanges.complete();
        this.subs.forEach(s => s.unsubscribe());
        this.fm.stopMonitoring(this.elRef.nativeElement);
    }

    ...

    // CONTROL VALUE ACCESSOR
    private propagateChange = (_: any) => { };

    public writeValue(a: string) {
        console.log('wtf');

        if (a && a !== '') {
            console.log('value => ', a);
            this.parts.setValue({
                'singleValue': a
            });
        }
    }
    public registerOnChange(fn: any) {
        this.propagateChange = fn;
    }

    public registerOnTouched(fn: any): void {
        return;
    }

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

1 个答案:

答案 0 :(得分:15)

  

终于解决了!!!

enter image description here

  1. 这里的问题是在子节点[SigaAutoCompleteComponent]中创建输入字段时,父节点必须知道 在子[CreateDossierComponent]中填充的值, 那部分是 缺少这就是它认为不能转向有效的原因 未触及输入字段[保持无效] - 通过发出来解决 值为父,然后根据需要操作表单控件。
  2. 通过将mat-form-field元素移动到子元素来解决mat-form-field和输入引起的问题 - 其他代码保持不变 这解决了占位符重叠和单击箭头图标以显示
  3. 这可以通过将服务注入子组件并执行自动完成功能来完成 - [通过重新设计的一种方式] 那边[我没有实现这个,但这将只是部门字段的副本]

    在create-doiser.component.html

                

          <!-- </div> -->
          <mat-autocomplete #thematicAutoComplete="matAutocomplete" [displayWith]="displayThematique">
            <mat-option *ngFor="let thematique of filteredThematiques | async" [value]="thematique">
              <span> {{thematique.code}} </span>
              <span> - </span>
              <span> {{thematique.libelle}} </span>
            </mat-option>
          </mat-autocomplete>
    
      

    在autocomplete.component.html

    <mat-form-field style="display:block;transition:none ">
    <div fxLayout="row">
      <input  matInput   placeholder="Thématique" [matAutocomplete]="autoCompleteControl" (optionSelected)="test($event)" tabindex="{{tabIndex}}">
      <div class="mat-select-arrow-wrapper" (click)="focus()">
        <div class="mat-select-arrow" [ngClass]="{'mat-select-arrow-down': !autoCompleteControl.isOpen, 'mat-select-arrow-up': autoCompleteControl.isOpen}"></div>
      </div>
    </div>
    
    </mat-form-field>
    
      

    在autocomplete.component.ts

    in set value emit the value to parent
    this.em.emit(value);
    
      

    创建-dosier.component.ts

      this.thematique = new FormControl( ['', [Validators.required, this.thematiqueValidator]]
    
        ); 
    
    this.formDossier.addControl('thematique',this.thematique);
    call(event){
    
        this.thematique.setValue(event);
        this.thematique.validator=this.thematiqueValidator();
        this.thematique.markAsTouched();
        this.thematique.markAsDirty();
    
      }
    }
    

    这将解决所有问题,如果您希望我推送到github请告诉我。 希望这可以帮助 !!!! 谢谢!!

    <强>更新 自动完成,暗示现在一切正常......

    据我所知,您不希望输入和mat-form-field在一起

      

    但是如果只是为了动态显示mat-h​​int [这取决于   上         formcontrol values],我们可以将formcontrol从parent传递给child       这甚至消除了从中发出输入值的必要性         孩子到父母       设置父组件中的值,[mat-h​​int字段保留在父组件本身]

    enter image description here