Since I migrate a custom component to standalone this error occurs.
I had an eye on this similar question but cannot find what is my issue : ERROR Error: No value accessor for form control with unspecified name attribute on switch
I expect a Module to be missing because of the standalone migration but I do not find which one. Or else I don’t know Why it did not occurs before migration.
Any help would be really appreciated.
ts :
import {
forwardRef,
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnChanges,
OnDestroy,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent,
MatAutocompleteTrigger,
MatAutocompleteModule,
} from '@angular/material/autocomplete';
import { MatChipGrid, MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
import isNil from 'lodash-es/isNil';
import { Observable, Subject } from 'rxjs';
import { map, startWith, takeUntil } from 'rxjs/operators';
import { MatOptionModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { NgFor, AsyncPipe } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { switchItemFromArray } from '../../../utils/switch-array';
import { sortAlphabeticalAscendingCaseInsensitive } from '../../../utils/string-array-sorting';
import { getKeysFromSelectedValues } from '../../../utils/keys-extractor';
import { sortAlphabeticalValueAscending } from '../../../utils/key-value-pair-sorting';
import { filterContainsCaseInvariant } from '../../../utils/filter-array';
import { KeyValuePair } from '../../../data/models/key-value-pair.model';
@Component({
selector: 'atlas-chips-auto-complete-edition',
providers: [
{ provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => ChipsAutoCompleteEditionComponent) },
],
templateUrl: './chips-auto-complete-edition.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatFormFieldModule,
MatChipsModule,
NgFor,
MatIconModule,
FormsModule,
MatAutocompleteModule,
ReactiveFormsModule,
MatOptionModule,
AsyncPipe,
],
})
export class ChipsAutoCompleteEditionComponent implements OnDestroy, OnChanges, ControlValueAccessor {
@Input() public fullWidth = true;
@Input() public allItems: KeyValuePair[] = [];
@Input() public invertSelection = false;
@Input() public required = false;
@Input() public selectionMessage?: string;
public selectedKeys: string[] = [];
public inputFormControl: FormControl = new FormControl();
@ViewChild('chipGrid', { static: false }) public chipGrid?: MatChipGrid;
@ViewChild('itemInput', { static: false }) public itemInput?: ElementRef<HTMLInputElement>;
@ViewChild('auto', { static: false }) public matAutocomplete?: MatAutocomplete;
@ViewChild('autoTrigger', { read: MatAutocompleteTrigger, static: true })
public autoTrigger?: MatAutocompleteTrigger;
public filteredItems$?: Observable<string[]>;
public selectedItems: string[] = [];
public disabled?: boolean;
private unselectedItems: string[] = [];
private readonly unsubscribe$: Subject<void> = new Subject<void>();
public onChanged = (val: any) => {
return;
};
public onTouched = () => {
return;
};
public writeValue(val: string[]): void {
this.selectedKeys = val;
this.updateComponentWithData();
}
public ngOnChanges(changes: SimpleChanges): void {
if (!isNil(changes['allItems']) && !isNil(this.selectedItems)) {
this.updateComponentWithData();
}
}
public ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
public registerOnChange(fn: any): void {
this.onChanged = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
public selectedFromInput(event: MatChipInputEvent): void {
if (this.matAutocomplete?.isOpen) {
return;
}
const input = event.input;
const value = event.value;
// Select our item
this.moveItem(value, this.unselectedItems, this.selectedItems);
// Reset the input value
if (input) {
input.value = '';
}
}
public focus(): void {
this.itemInput?.nativeElement.focus();
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.moveItem(event.option.viewValue, this.unselectedItems, this.selectedItems);
if (this.itemInput) {
this.itemInput.nativeElement.value = '';
}
}
public unselected(item: string): void {
this.moveItem(item, this.selectedItems, this.unselectedItems);
}
public checkErrorState(): void {
if (this.required && this.chipGrid) {
this.chipGrid.errorState = this.selectedItems.length < 1;
}
}
public openPanel(evt: Event): void {
if (!this.matAutocomplete?.isOpen && this.autoTrigger) {
evt.stopPropagation();
this.autoTrigger.openPanel();
return;
}
}
public trackByFn(_index: number, item: string): string {
return item;
}
private updateComponentWithData() {
this.initListsFromInputs();
this.subscribeTextFilterChange();
}
private initListsFromInputs(): void {
const unselectedValues: string[] = [];
const selectedValues: string[] = [];
sortAlphabeticalValueAscending(this.allItems).forEach((kvp: KeyValuePair) => {
const isSelected: boolean = this.selectedKeys.includes(kvp.key);
if (isSelected) {
selectedValues.push(kvp.value);
} else {
unselectedValues.push(kvp.value);
}
});
if (this.invertSelection) {
this.unselectedItems = selectedValues;
this.selectedItems = unselectedValues;
} else {
this.unselectedItems = unselectedValues;
this.selectedItems = selectedValues;
}
}
private subscribeTextFilterChange() {
this.filteredItems$ = this.inputFormControl.valueChanges.pipe(
startWith(''),
map((filterValue: string) =>
filterValue ? filterContainsCaseInvariant(this.unselectedItems, filterValue) : this.unselectedItems,
),
takeUntil(this.unsubscribe$),
);
}
private moveItem(item: string, sourceArray: string[], destinationArray: string[]): void {
if (!switchItemFromArray(item, sourceArray, destinationArray)) {
return;
}
sourceArray.sort(sortAlphabeticalAscendingCaseInsensitive);
destinationArray.sort(sortAlphabeticalAscendingCaseInsensitive);
this.inputFormControl.setValue('');
this.checkErrorState();
const items = this.invertSelection ? this.unselectedItems : this.selectedItems;
if (this.onChanged && this.onTouched) {
this.onChanged(getKeysFromSelectedValues(items, this.allItems));
this.onTouched();
}
}
protected readonly MatChipGrid = MatChipGrid;
}
html :
<mat-form-field [class.full-width]="fullWidth">
<mat-label>{{ selectionMessage }}</mat-label>
<mat-chip-grid #chipGrid [required]="required">
<mat-chip-row *ngFor="let item of selectedItems; trackBy: trackByFn" (removed)="unselected(item)">
{{ item }}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="Type to filter list..."
(focusout)="checkErrorState()"
[formControl]="inputFormControl"
#autoTrigger="matAutocompleteTrigger"
#itemInput
[matAutocomplete]="auto"
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="selectedFromInput($event)"
(blur)="onTouched()"
(click)="openPanel($event)"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let item of filteredItems$ | async; trackBy: trackByFn" [value]="item">
{{ item }}
</mat-option>
</mat-autocomplete>
<mat-error>At least one element must be selected.</mat-error>