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.
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 :
- Angular Material - Create custom form field control
- Stack Overflow - custom component with ng value accessor
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;
}
}