问题
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>
回答1:
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
来源:https://stackoverflow.com/questions/61503425/angular-9-formarray-search-operation-executing-for-only-first-dynamic-control