Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
219 views
in Technique[技术] by (71.8m points)

Angular 9 Formarray search operation executing for only first dynamic control

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>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...