I have a custom control component that is used to show a required validation error for mat-chip-lists. On the form that I'm using this component, when I first open this form the required validation message shows. I only want it to show when the field is not populated with any data. Can someone please provide a reason why and a solution to what I need to do to get the validation to work properly.
I could be overcomplicating this and it's much simpler than I thought.
html where mat-error element for required show:
<mat-form-field [floatLabel]="floatLabel">
<mat-label>{{ label }}</mat-label>
<mat-chip-list #optionList aria-label="label" required>
<mat-chip *ngFor="let item of selectedOptions" (removed)="removed(item)"
[removable]="!item.disabled" [disabled]="item.disabled">
{{ item.text }}
<mat-icon *ngIf="!item.disabled" matChipRemove>cancel</mat-icon>
</mat-chip>
<input
#optionInput
type="text"
[placeholder]="placeholder"
[formControl]="formControl"
[matAutocomplete]="optionAutoComplete"
[matChipInputFor]="optionList"
[required]="required"
/>
</mat-chip-list>
<mat-autocomplete #optionAutoComplete="matAutocomplete"
(optionSelected)="selected($event.option.value)">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option.text }}
</mat-option>
</mat-autocomplete>
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
</mat-form-field>
<mat-error *ngIf="required === true && hasValue === false &&
isInitialized === true"> {{ label }} is
<strong>required</strong> </mat-error>
Typescript for the custom control The removed function fires when you remove selected items from the list. It turns the check back on and sends the update to the parent component.
Component({
selector: 'app-chips',
templateUrl: './chips.component.html',
})
export class ChipsComponent implements OnInit, DoCheck {
@Input() label = '';
@Input() placeholder = '';
@Input() options: Options[] = [];
@Input() selectedOptions: Options[] = [];
@Input() floatLabel: FloatLabelType = 'auto';
@Input() hint!: string | undefined;
@Input() required = true;
hasValue: boolean = this.selectedOptions.length > 0;
isInitialized: boolean | undefined;
@ViewChild('optionInput') optionInput: ElementRef | undefined;
@Output() onRemoved = new EventEmitter<Options>();
@Output() selectedOptionsChanged = new EventEmitter<Options[]>();
formControl = new FormControl('');
filteredOptions: Observable<Options[]> | undefined;
iterableDiffer: IterableDiffer<Options>;
constructor(private readonly iterableDiffers: IterableDiffers) {
this.iterableDiffer = this.iterableDiffers.find([]).create();
}
ngDoCheck(): void {
const optionChanges = this.iterableDiffer.diff(this.options);
if (optionChanges) {
this.filteredOptions = of(this.options);
}
if (this.required === undefined) {
this.required = false;
}
}
ngOnInit(): void {
this.subscribeFilterOptions();
this.isInitialized = true;
}
ngAfterViewInit(): void {
this.isInitialized = true;
}
selected(value: Options): void {
if (this.optionInput) {
this.optionInput.nativeElement.value = '';
}
if (!this.selectedOptions.find((x) => x.text === value.text)) {
this.selectedOptions.push(value);
this.selectedOptionsChanged.emit(this.selectedOptions);
}
this.hasValue = this.selectedOptions.length > 0;
}
private subscribeFilterOptions() {
this.filteredOptions = this.formControl.valueChanges.pipe(
startWith(''),
map((value: string | Options) =>
value && typeof value === 'string' ? this.options.filter((o) =>
o.text.toLowerCase().includes(value.toLowerCase())) :
this.options.slice()
)
);
}
removed(value: Options): void {
this.onRemoved.emit(value);
this.hasValue = this.selectedOptions.length > 0;
}
}
Mat-chip-list component on the form
<div class="col-md-12">
<app-linq-chips
label="Entities"
placeholder="Add Entity..."
[options]="entityOptions"
[selectedOptions]="selectedEntities"
[hint]="
entityListHasDisabledOptions === true
? 'Please remove this contact from any roles for an entity prior to removing their
association to that entity.'
: undefined
"
(onRemoved)="removeEntity($event)"
>
</app-linq-chips>
</div>