Angular 9 Formarray搜索操作仅针对第一个动态控制执行 [英] Angular 9 Formarray search operation executing for only first dynamic control
问题描述
我有一个包含某些字段的表单数组,其中一个是输入.在此字段中,根据用户输入,我们在 API 中进行搜索并返回一些数据.
问题是,它只适用于第一次动态控制.当我添加更多控件时,它对它们不起作用.
我猜这是因为我在 ngAfterViewInit()
中编写了搜索逻辑.
那还有什么选择呢.
我不知道如何解决这个问题.
提前谢谢
.ts
purchaseform = this.fb.group({vendor_mobile : ['', [Validators.required, Validators.minLength(10), Validators.maxLength(10), Validators.pattern("^[0-9]*$")]],产品:this.fb.array([this.addProductGroup()])})添加产品组(){返回 this.fb.group({product_name : ['', Validators.required ],product_quantity : ['', [Validators.required, Validators.pattern("^[0-9]*$") ]],product_Buyingprice : ['', [ Validators.required, Validators.pattern("^[0-9]*$") ]],})}获取产品(){将 this.purchaseform.get('product') 作为 FormArray 返回;}添加产品(){this.product.push(this.addProductGroup())}移除产品(索引){返回 this.product.removeAt(index)}ngAfterViewInit() {//服务端搜索fromEvent(this.input.nativeElement,'keyup').管道(过滤器(布尔),去抖动时间(500),distinctUntilChanged(),点击((事件:键盘事件)=> {控制台日志(事件)console.log(this.input.nativeElement.value)this.productService.search_Products(this.input.nativeElement.value).subscribe(data =>{如果(数据){this.product_list = 数据console.log(this.product_list)}})})).订阅();}
.html
</表单>
如果使用 @ViewChild
,viewChild 只会得到第一个元素.
如果您使用@ViewChildren,则需要为 QueryList 的每个元素创建如此多的事件
@ViewChildren('input') 输入:QueryListthis.inputs.forEach(input=>{fromEvent(input.nativeElement,'keyup')})
无论如何这不起作用 - 如果数组最初是固定元素则有效.好吧,你可以订阅 input.changes 和 bla-bla-bla
方法不是使用 fromEvents.这个想法来自 这个 Amir Tugendhaft 的条目博客.第一个是不使用productList",否则是 productList 和异步管道的可观察对象.由于我们有几个productList",我们需要一个可观察的数组
productList$:Observable[]=[];
.html 会像
<ng-container [formGroupName]="i"><mat-form-field class="example-full-width"><mat-label>输入产品名称</mat-label><input matInput #input咏叹调标签=产品名称"[matAutocomplete]="自动"formControlName ="product_name"><mat-autocomplete #auto="matAutocomplete"><ng-container *ngIf="product_list$[i] |async as results"><mat-option *ngFor="let state of results" [value]="state.state"><span>{{state.name}}</span></mat-option><mat-option *ngIf="prod.get('product_name').value &&结果?.length<=0" class="text-danger">此类产品不存在</mat-option><ng-容器></mat-autocomplete></mat-form-field></ng-容器>
看看我们如何使用 <ng-container *ngIf="product_list$[i] |async as results">
并迭代results".
好吧,下一步是更改函数 addProductGroup 以创建 observable 和 asing 到数组 productList$
方式是订阅控件的valueChanges,但是使用switchMap返回服务的响应
addProductGroup(index) {//我们使用辅助常量const group = this.fb.group({product_name: ["", Validators.required],产品数量:["",[Validators.required, Validators.pattern("^[0-9]*$")]],产品_购买价格:["",[Validators.required, Validators.pattern("^[0-9]*$")]]});//查看 observables 将如何成为product_name"的 valueChange//这个formGroup,使用管道和switchMapthis.productList$[index] = group.get("product_name").valueChanges.pipe(去抖动时间(300),switchMap(value => this.productService.search_Products(value)));//最后,我们返回组返回组;}
最后,当调用 addGroup 将索引"作为参数发送时要小心,所以,首先
this.purchaseform = this.fb.group({...product: this.fb.array([this.addProductGroup(0)])//<--查看0"});
还有
addproduct() {this.product.push(this.addProductGroup(this.product.length));}
你可以看到stackblitz(我模拟了使用的服务,显然你调用了一个 API)
注意:如果您想使用 ngbTypeHead,请参阅 这篇文章
更新[displayWith]="displayFn"
我用 mat-option 写的
这使得值是属性状态",如果我们想要整个对象,我们需要写
但我们也需要做一点改变,首先是在matAutocomplete中添加displaywith
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" >
我们的函数可以像,例如
displayFn(data: any): string {返回数据 ?data.name+'-'+data.state : '';}
第二个是当我们收到一个对象或一个字符串时,更改服务中的一些search_Products".我们可以用类似的函数替换
search_Products(name: any): Observable{//name 可以是字符串或对象,所以name=name.toLowerCase?name.toLowerCase():name.name//现在name是一个字符串,可以过滤或调用appireturn of(states.filter(x=>x && x.toLowerCase().indexOf(name)>=0)).pipe(map(result=>){return result.map(x=>({state:x,name:x}))}))}
我分叉了stackblitz 与此更改
I have a formarray with certain fields one of which is input. In this field, on user input, we search in the API's and return some data.
The problem is, it is working only for first dynamic control. When I add more controls, it is not working for them.
I guess this is happening because I have written search logic in ngAfterViewInit()
.
But what's the alternative then.
I can't get how to solve this problem.
Thank you In advance
.ts
purchaseform = this.fb.group({
vendor_mobile : ['', [Validators.required, Validators.minLength(10), Validators.maxLength(10), Validators.pattern("^[0-9]*$")]],
product : this.fb.array([this.addProductGroup()])
})
addProductGroup() {
return this.fb.group({
product_name : ['', Validators.required ],
product_quantity : ['', [Validators.required, Validators.pattern("^[0-9]*$") ]],
product_Buyingprice : ['', [ Validators.required, Validators.pattern("^[0-9]*$") ]],
})
}
get product() {
return this.purchaseform.get('product') as FormArray;
}
addproduct() {
this.product.push(this.addProductGroup())
}
remove_product(index) {
return this.product.removeAt(index)
}
ngAfterViewInit() {
// server-side search
fromEvent(this.input.nativeElement,'keyup')
.pipe(
filter(Boolean),
debounceTime(500),
distinctUntilChanged(),
tap((event:KeyboardEvent) => {
console.log(event)
console.log(this.input.nativeElement.value)
this.productService.search_Products(this.input.nativeElement.value).subscribe(data =>{
if(data){
this.product_list = data
console.log(this.product_list)
}
})
})
)
.subscribe();
}
.html
<form [formGroup]="purchaseform">
// other fields
<div formArrayName = "product" *ngFor="let prod of product?.controls; let i = index">
<ng-container [formGroupName]="i">
<mat-form-field class="example-full-width">
<mat-label>Enter product name</mat-label>
<input matInput #input
aria-label="product name"
[matAutocomplete]="auto"
formControlName ="product_name">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" >
<mat-option *ngFor="let state of product_list " [value]="state ">
<span>{{state.name}}</span>
</mat-option>
<mat-option *ngIf="!product_list || !product_list.length" class="text-danger">
Such product does not exists
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Enter product quantity</mat-label>
<input matInput formControlName="product_quantity" type="number" >
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Enter product price</mat-label>
<input matInput formControlName="product_Buyingprice" type="number">
</mat-form-field>
<button type="button" [disabled]="!purchaseform.valid" class="btn btn-primary" (click) = "addproduct()">Add product</button>
<button [disabled] = "i==0" type="button" class="btn btn-danger" (click) = "remove_product(i)">Delete product</button>
</ng-container>
</div>
</form>
if you use @ViewChild
, viewChild only get the first element.
if you're using @ViewChildren you need get create so many event for each Element of the QueryList
@ViewChildren('input') inputs:QueryList<ElementRef>
this.inputs.forEach(input=>{
fromEvent(input.nativeElement,'keyup')
})
Anyway this NOT work -works if the array was fixed elements at first. Well, you can subscribe to inputs.changes and bla-bla-bla
The way is NOT use fromEvents. The idea goes from this Amir Tugendhaft's entry blog. The first is not use a "productList" else an observable of productList and async pipe. As we has severals "productList, we need an array of observables
productList$:Observable<any>[]=[];
And the .html will be like
<div formArrayName = "product" *ngFor="let prod of product?.controls; let i = index">
<ng-container [formGroupName]="i">
<mat-form-field class="example-full-width">
<mat-label>Enter product name</mat-label>
<input matInput #input
aria-label="product name"
[matAutocomplete]="auto"
formControlName ="product_name">
<mat-autocomplete #auto="matAutocomplete">
<ng-container *ngIf="product_list$[i] |async as results">
<mat-option *ngFor="let state of results " [value]="state.state">
<span>{{state.name}}</span>
</mat-option>
<mat-option *ngIf="prod.get('product_name').value &&
results?.length<=0" class="text-danger">
Such product does not exists
</mat-option>
<ng-container>
</mat-autocomplete>
</mat-form-field>
</ng-container>
</div>
See how we use <ng-container *ngIf="product_list$[i] |async as results">
and iterate over "results".
Well, the next step is change the function addProductGroup to create the observable and asing to the array productList$
The way is subscribe to valueChanges of the control, but return the response of the service using switchMap
addProductGroup(index) {
//we use an auxiliar const
const group = this.fb.group({
product_name: ["", Validators.required],
product_quantity: [
"",
[Validators.required, Validators.pattern("^[0-9]*$")]
],
product_Buyingprice: [
"",
[Validators.required, Validators.pattern("^[0-9]*$")]
]
});
//See how the observables will be valueChange of the "product_name"
//of this formGroup, using a pipe and switchMap
this.productList$[index] = group.get("product_name").valueChanges.pipe(
debounceTime(300),
switchMap(value => this.productService.search_Products(value))
);
//finally, we return the group
return group;
}
At last, be carefull when call to addGroup to send as argument the "index", so, at first
this.purchaseform = this.fb.group({
...
product: this.fb.array([this.addProductGroup(0)]) //<--see the "0"
});
And
addproduct() {
this.product.push(this.addProductGroup(this.product.length));
}
You can see the stackblitz (I simulate the service using of, obviously you call to an API)
NOTE: If you want use ngbTypeHead see this post
Update use of [displayWith]="displayFn"
I wrote in mat-option
<mat-option *ngFor="let state of results " [value]="state.state">
This make that the value is the property "state", if we want the whole object we need write
<mat-option *ngFor="let state of results" [value]="state">
but also we need make a little change, the first is add in the matAutocomplete the displaywith
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" >
And our function can be like, e.g.
displayFn(data: any): string {
return data ?data.name+'-'+data.state : '';
}
The second is change a few the "search_Products" in the service taking account when we received an object or an string. We can replace the function with some like
search_Products(name: any): Observable<any> {
//name can be a string or an object, so
name=name.toLowerCase?name.toLowerCase():name.name
//now name is a string and can filter or call to the appi
return of(states.filter(x=>x && x.toLowerCase().indexOf(name)>=0)).pipe(map(result=>
{
return result.map(x=>({state:x,name:x}))
}))
}
I forked the stackblitz with this changes
这篇关于Angular 9 Formarray搜索操作仅针对第一个动态控制执行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!