在 angular 中的 formArray 中使用 ngx-mat-select-search 时动态设置 mat-select-options 8
Dynamically set mat-select-options when using ngx-mat-select-search in formArray in angular 8
我有一个包含两个 mat-select 的 formArray。其中之一使用 ngx-mat-select-search 从 mat-select 的选项中的值进行搜索。我的问题是,当表单数组包含多个元素时,我从这些元素中搜索其中一个 mat-select,当我搜索时,所有 mat-select 的值都消失了,它会在值被 selected 后重新出现。
这是我的模板文件的一部分:
<ng-container formArrayName='cpUserAccounts'
*ngFor='let account of cpUserAccounts.controls; let i=index'>
<div class="d-flex align-items-center flex-wrap col-md-12" [formGroupName]='i'>
<div class="col-sm-6">
<mat-form-field class="task-array">
<mat-label>Client</mat-label>
<mat-select formControlName="clientId" required>
<mat-option>
<ngx-mat-select-search [formControl]="bankFilterCtrl"
placeholderLabel='Search' noEntriesFoundLabel="'No match found'">
</ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let client of filteredOptions | async" [value]="client.id">
{{client.companyName}}
</mat-option>
</mat-select>
<mat-hint class="error"
*ngIf="findDuplicate(account.controls.clientId.value, 'cpUserAccounts')">
{{constants.errorMessage.duplicateClientLabel}}
</mat-hint>
</mat-form-field>
</div>
<div class="col-sm-4">
<mat-form-field class="task-array">
<mat-label>Role</mat-label>
<mat-select formControlName="roleId" required>
<mat-option *ngFor="let role of accountRoles" [value]="role.id">
{{role.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-sm-2 d-flex justify-content-end">
<mat-icon class="remove-task-button" title="Remove" (click)='removeAccount(i)'
matSuffix>
remove
</mat-icon>
</div>
</div>
</ng-container>
.ts 文件包含以下代码:
filteredOptions: ReplaySubject<ClientModel[]> = new ReplaySubject<ClientModel[]>(1);
public bankFilterCtrl: FormControl = new FormControl();
protected onDestroy = new Subject<void>();
ngOnInit() {
this.bankFilterCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(() => {
this.filterBanks();
});
}
getClients() {
const subscription = this.clientContactService.getClients().subscribe(clients => {
this.clients = clients.data;
this.filterBanks();
});
this.subscription.push(subscription);
}
protected filterBanks() {
if (!this.clients) {
return;
}
// get the search keyword
let search = this.bankFilterCtrl.value;
if (!search) {
this.filteredOptions.next(this.clients.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the clients
this.filteredOptions.next(
this.clients.filter(client => client.companyName.toLowerCase().indexOf(search) > -1)
);
}
以下是正在发生的事情的流程图像:
一开始,它是这样的:
然后:
问题出在这里:
因为您使用的是 *ngFor
中的 filteredOptions | async
元素,相同的选项将用于显示的每个“client”-select 元素。因此在过滤的时候,会过滤掉所有客户端select框的选项,这样原来selected的值就没有了
要解决此问题,您可以将最外层 *ngFor
的内容移动到一个单独的组件,该组件应实现 ControlValueAccessor 接口 (https://angular.io/api/forms/ControlValueAccessor#description)。
我也被困在这个问题上,找不到任何解决方案,然后自己想出了这个办法。最简单的方法是使用一个以数组作为值的备份 FormControl。并在要使用该数组的模板数组中访问它的值。尝试理解这个简单的代码示例 -
export class FormExampleComponent {
public formGroup: FormGroup;
public itemCodesArray: ItemCode[] = [
{ id: 1, name: apple },
{ id: 2, name: banna },
{ id: 3, name: pineapple },
];
constructor(fb: FormBuilder) {}
public ngOnInit(): void {
this.createForm();
}
private createForm() {
this.formGroup = this.fb.group({
formItems: this.fb.array([this.initFormGroup()]),
});
}
private initFormGroup(): FormGroup {
return this.fb.group({
description: [null, []],
formItemsGroup: this.initFormItemsGroup(),
});
}
private initFormItemsGroup(): FormGroup {
const fg = this.fb.group({
// id is just to recognize the form array elemnts by trackByfunction
id: [new Date().getTime() + Math.floor(Math.random() * 100)],
itemCodeSearch: [null, []],
// temp array
filteredItemCodes: [this.itemCodesArray, []],
itemCodeId: [null, [Validators.required]],
itemDescription: [null, [Validators.required]],
});
fg.get('itemCodeSearch')
.valueChanges
.subscribe((searchText) => {
this.itemCodes$.subscribe((itemCode) => {
if (searchText) {
fg.get('filteredItemCodes').setValue(
itemCode.filter((item) => item.name.toLowerCase().includes(searchText.toLowerCase())),
);
} else {
fg.get('filteredItemCodes').setValue(itemCode);
}
});
});
return fg;
}
public get formItems(): AbstractControl[] {
return (this.formGroup.get('formItems') as FormArray).controls;
}
public trackByFunction(index: number, item: any): number {
return item.value.formItemsGroup.id;
}
}
<form [formGroup]="formGroup">
<div formArrayName="formItems">
<div *ngFor="let formItem of formItems; let i = index; trackBy: trackByFunction">
<div [formGroupName]="i">
<div>
<div formGroupName="poItemsGroup">
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<input type="text" matInput formControlName="description" required />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Item Code</mat-label>
<mat-select formControlName="itemCodeId" required>
<mat-option>
<ngx-mat-select-search
formControlName="itemCodeSearch"
placeholderLabel="Search item"
noEntriesFoundLabel="No item found"
></ngx-mat-select-search>
</mat-option>
<mat-option
*ngFor="
let item of formItem.get('formItemsGroup').get('filteredItemCodes').value;
trackBy: 'id' | trackByKey
"
[value]="item.id"
>
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Item Description</mat-label>
<input type="text" matInput formControlName="itemDescription" required />
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</form>
由于 valueChanges 触发过于频繁,您可以使用 debounceTime 来阻止它在用户输入的每个字符上触发。
您还可以使用 OnDestroy 生命周期钩子和 takeUntil 函数来取消订阅每个订阅,以提高效率。
我有一个包含两个 mat-select 的 formArray。其中之一使用 ngx-mat-select-search 从 mat-select 的选项中的值进行搜索。我的问题是,当表单数组包含多个元素时,我从这些元素中搜索其中一个 mat-select,当我搜索时,所有 mat-select 的值都消失了,它会在值被 selected 后重新出现。 这是我的模板文件的一部分:
<ng-container formArrayName='cpUserAccounts'
*ngFor='let account of cpUserAccounts.controls; let i=index'>
<div class="d-flex align-items-center flex-wrap col-md-12" [formGroupName]='i'>
<div class="col-sm-6">
<mat-form-field class="task-array">
<mat-label>Client</mat-label>
<mat-select formControlName="clientId" required>
<mat-option>
<ngx-mat-select-search [formControl]="bankFilterCtrl"
placeholderLabel='Search' noEntriesFoundLabel="'No match found'">
</ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let client of filteredOptions | async" [value]="client.id">
{{client.companyName}}
</mat-option>
</mat-select>
<mat-hint class="error"
*ngIf="findDuplicate(account.controls.clientId.value, 'cpUserAccounts')">
{{constants.errorMessage.duplicateClientLabel}}
</mat-hint>
</mat-form-field>
</div>
<div class="col-sm-4">
<mat-form-field class="task-array">
<mat-label>Role</mat-label>
<mat-select formControlName="roleId" required>
<mat-option *ngFor="let role of accountRoles" [value]="role.id">
{{role.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-sm-2 d-flex justify-content-end">
<mat-icon class="remove-task-button" title="Remove" (click)='removeAccount(i)'
matSuffix>
remove
</mat-icon>
</div>
</div>
</ng-container>
.ts 文件包含以下代码:
filteredOptions: ReplaySubject<ClientModel[]> = new ReplaySubject<ClientModel[]>(1);
public bankFilterCtrl: FormControl = new FormControl();
protected onDestroy = new Subject<void>();
ngOnInit() {
this.bankFilterCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(() => {
this.filterBanks();
});
}
getClients() {
const subscription = this.clientContactService.getClients().subscribe(clients => {
this.clients = clients.data;
this.filterBanks();
});
this.subscription.push(subscription);
}
protected filterBanks() {
if (!this.clients) {
return;
}
// get the search keyword
let search = this.bankFilterCtrl.value;
if (!search) {
this.filteredOptions.next(this.clients.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the clients
this.filteredOptions.next(
this.clients.filter(client => client.companyName.toLowerCase().indexOf(search) > -1)
);
}
以下是正在发生的事情的流程图像:
一开始,它是这样的:
然后:
问题出在这里:
因为您使用的是 *ngFor
中的 filteredOptions | async
元素,相同的选项将用于显示的每个“client”-select 元素。因此在过滤的时候,会过滤掉所有客户端select框的选项,这样原来selected的值就没有了
要解决此问题,您可以将最外层 *ngFor
的内容移动到一个单独的组件,该组件应实现 ControlValueAccessor 接口 (https://angular.io/api/forms/ControlValueAccessor#description)。
我也被困在这个问题上,找不到任何解决方案,然后自己想出了这个办法。最简单的方法是使用一个以数组作为值的备份 FormControl。并在要使用该数组的模板数组中访问它的值。尝试理解这个简单的代码示例 -
export class FormExampleComponent {
public formGroup: FormGroup;
public itemCodesArray: ItemCode[] = [
{ id: 1, name: apple },
{ id: 2, name: banna },
{ id: 3, name: pineapple },
];
constructor(fb: FormBuilder) {}
public ngOnInit(): void {
this.createForm();
}
private createForm() {
this.formGroup = this.fb.group({
formItems: this.fb.array([this.initFormGroup()]),
});
}
private initFormGroup(): FormGroup {
return this.fb.group({
description: [null, []],
formItemsGroup: this.initFormItemsGroup(),
});
}
private initFormItemsGroup(): FormGroup {
const fg = this.fb.group({
// id is just to recognize the form array elemnts by trackByfunction
id: [new Date().getTime() + Math.floor(Math.random() * 100)],
itemCodeSearch: [null, []],
// temp array
filteredItemCodes: [this.itemCodesArray, []],
itemCodeId: [null, [Validators.required]],
itemDescription: [null, [Validators.required]],
});
fg.get('itemCodeSearch')
.valueChanges
.subscribe((searchText) => {
this.itemCodes$.subscribe((itemCode) => {
if (searchText) {
fg.get('filteredItemCodes').setValue(
itemCode.filter((item) => item.name.toLowerCase().includes(searchText.toLowerCase())),
);
} else {
fg.get('filteredItemCodes').setValue(itemCode);
}
});
});
return fg;
}
public get formItems(): AbstractControl[] {
return (this.formGroup.get('formItems') as FormArray).controls;
}
public trackByFunction(index: number, item: any): number {
return item.value.formItemsGroup.id;
}
}
<form [formGroup]="formGroup">
<div formArrayName="formItems">
<div *ngFor="let formItem of formItems; let i = index; trackBy: trackByFunction">
<div [formGroupName]="i">
<div>
<div formGroupName="poItemsGroup">
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<input type="text" matInput formControlName="description" required />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Item Code</mat-label>
<mat-select formControlName="itemCodeId" required>
<mat-option>
<ngx-mat-select-search
formControlName="itemCodeSearch"
placeholderLabel="Search item"
noEntriesFoundLabel="No item found"
></ngx-mat-select-search>
</mat-option>
<mat-option
*ngFor="
let item of formItem.get('formItemsGroup').get('filteredItemCodes').value;
trackBy: 'id' | trackByKey
"
[value]="item.id"
>
{{ item.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Item Description</mat-label>
<input type="text" matInput formControlName="itemDescription" required />
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</form>
由于 valueChanges 触发过于频繁,您可以使用 debounceTime 来阻止它在用户输入的每个字符上触发。
您还可以使用 OnDestroy 生命周期钩子和 takeUntil 函数来取消订阅每个订阅,以提高效率。