0
votes

I am having a strange problem using reactive forms in Angular using material table and forms. I have a form with an autocomplete setup which fetches products from api. I am calling a function on onClick which adds that product in a formarray with default quantity as 1. If users clicks on that same product again I am incrementing the quantity, so that no product appears twice only quantity increases. I am also enabling the quantity as input so that if user wants to enter quantity manually he can just by entering value. The problem is that it is incremented in form's value but not in form control's value. The json output of form shows 2 but input field says 1. Have a look at the code and attached image to get better understanding.

enter image description here

enter image description here

add.deal.component.ts

import {Component, OnInit, ViewChild} from '@angular/core';
import {AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, NgForm, Validators} from "@angular/forms";
import {DealService} from "../deal.service";
import {Deal} from "../deal";
import {BehaviorSubject, Observable} from "rxjs/Rx";
import {Product} from "../../product/product.interface";
import {ProductService} from "../../product/product.service";
import {map, startWith} from "rxjs/operators";
import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
import {DealProducts} from "../deal-products";

const ELEMENT_DATA: DealProducts[] = [];

@Component({
  selector: 'app-add-deal',
  templateUrl: './add-deal.component.html',
  styleUrls: ['./add-deal.component.css']
})
export class AddDealComponent implements OnInit
{

  dealForm: FormGroup;

  filteredProducts: Observable<Product[]>;
  products: Product[] = [];
  dealProducts: FormArray = new FormArray([]);

  displayedColumns = ['name', 'description', 'quantity'];
  // dataSource = new MatTableDataSource(ELEMENT_DATA);
  // dataSource = new BehaviorSubject;
  dataSource = new BehaviorSubject<AbstractControl[]>([]);
  loading: boolean = false;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  constructor (private productService: ProductService, private formBuilder: FormBuilder, private dealService: DealService) {}

  ngOnInit ()
  {
    this.initForm();
    this.productService.getProducts().subscribe(
      (data: Product[]) =>
      {
        console.log(data);
        this.products = data;
        this.filteredProducts = this.dealForm.get('product').valueChanges
          .pipe(
            startWith<string | Product>(''),
            map(value => typeof value === 'string' ? value : value.name),
            map(name => name ? this._filterProducts(name) : this.products.slice())
          );
      }
    );
  }

  initForm ()
  {
    this.dealForm = this.formBuilder.group({
      'name': [null, Validators.required],
      'description': [null, Validators.required],
      'price': [null, Validators.required],
      'product': [null],
      'dealProducts': this.dealProducts
    });
    // this.dealProducts.push(
    //   new FormGroup({
    //     product: new FormControl('', Validators.required),
    //     quantity: new FormControl(1, Validators.required)
    //   })
    // );
  }

  createDealProduct (): FormGroup
  {
    return this.formBuilder.group({
      product: null,
      quantity: 1
    });
  }

  onProductSelect (product: Product)
  {
    this.dealForm.get('product').setValue('');
    if (this.dealForm.value.dealProducts.length > 0)
    {
      if (this.productExists(product.id))
      {
        for (const item of this.dealForm.value.dealProducts)
        {
          if (item.product.id === product.id)
          {
            item.quantity++;
          }
        }
      }
      else
      {
        (<FormArray>this.dealForm.controls['dealProducts']).push(
          new FormGroup({
            product: new FormControl(product),
            quantity: new FormControl(1, Validators.required)
          })
        );
      }

      // this.dataSource = (<MatTableDataSource>(<FormArray>this.dealForm.controls['dealProducts']).controls);
      this.dataSource.next((<FormArray>this.dealForm.controls['dealProducts']).controls);
      // this.dataSource.next(this.dealForm.value.dealProducts);

    }
    else
    {
      (<FormArray>this.dealForm.controls['dealProducts']).push(
        new FormGroup({
          product: new FormControl(product),
          quantity: new FormControl(1, Validators.required)
        })
      );
      // this.dataSource = (<MatTableDataSource>(<FormArray>this.dealForm.controls['dealProducts']).controls);

      // this.dataSource.next((<FormArray>this.dealForm.controls['dealProducts']).controls);
      // this.dataSource.next(this.dealForm.value.dealProducts);
      this.dataSource.next((<FormArray>this.dealForm.controls['dealProducts']).controls);

    }
    console.log(this.dealForm);
  }

  productExists (id)
  {
    for (const item of this.dealForm.value.dealProducts)
    {
      if (item.product.id === id)
      {
        return true;
      }
    }
    return false;
  }

  displayProduct (product ?: Product): string | undefined
  {
    return product ? product.name : undefined;
  }

  private
  _filterProducts (name: string): Product[]
  {
    const filterValue = name.toLowerCase();

    return this.products.filter(product => product.name.toLowerCase().indexOf(filterValue) === 0);
  }

  getErrorMessage ()
  {
    for (const field in this.dealForm.controls)
    {
      if (!this.dealForm.get(field).valid && this.dealForm.get(field).touched)
      {
        return 'Please enter a value';
      }
    }
  }

  onFormSubmit (form)
  {

    let myDeal: Deal;
    myDeal.name = form.name;
    myDeal.description = form.description;
    myDeal.price = form.price;
    myDeal.dealProducts = form.dealProducts;
    console.log(myDeal);
    // const jsonString = JSON.stringify(form);
    // const deal = <Deal>JSON.parse(jsonString);
    // console.log(deal);
    // this.dealService.createDeal(deal).subscribe(
    //   data => {
    //     console.log(data);
    //   },
    //   error => {
    //     console.log(error);
    //   }
    // );
  }
}

add.deal.component.html

<div fxLayout="row" fxLayoutWrap="wrap">
  <div fxFlex.gt-sm="100" fxFlex.gt-xs="100" fxFlex="100">
    <mat-card>
      <mat-toolbar color="primary">
        <span>Add Deal</span>
        <span fxFlex></span>
      </mat-toolbar>
    </mat-card>
  </div>
</div>
<div fxLayout="row" fxLayoutWrap="wrap">
  <div fxFlex.gt-sm="100" fxFlex.gt-xs="100" fxFlex="100">
    <mat-card>
      <mat-card-content class="mat-elevation-z8">
        <form [formGroup]="dealForm" (ngSubmit)="onFormSubmit(dealForm.value)">
          <div fxLayout="row" class="row" fxflexalign="center" fxLayoutWrap="wrap" ng-reflect-layout="row"
               ng-reflect-wrap="wrap" ng-reflect-align="center"
               style="flex-flow: row wrap; box-sizing: border-box; display: flex; align-self: center;">
            <div class="p-10" fxflex="100" fxflex.gt-sm="35" ng-reflect-flex="100" ng-reflect-flex-gt-sm="35"
                 style="flex: 1 1 35%; box-sizing: border-box; max-width: 35%;">
              <mat-form-field>
                <input matInput placeholder="Name" formControlName="name" required>
                <mat-error *ngIf="dealForm.get('name').invalid">{{getErrorMessage()}}</mat-error>
              </mat-form-field>
            </div>
            <div class="p-10" fxflex="100" fxflex.gt-sm="35" ng-reflect-flex="100" ng-reflect-flex-gt-sm="35"
                 style="flex: 1 1 35%; box-sizing: border-box; max-width: 35%;">
              <mat-form-field>
                <input matInput type="number" placeholder="Price" formControlName="price" required>
                <mat-error *ngIf="dealForm.get('price').invalid">{{getErrorMessage()}}</mat-error>
              </mat-form-field>
            </div>
            <div class="p-10" fxflex="100" fxflex.gt-sm="30" ng-reflect-flex="100" ng-reflect-flex-gt-sm="30"
                 style="flex: 1 1 30%; box-sizing: border-box; max-width: 30%;">
              <mat-form-field>
                <textarea matInput placeholder="Description" formControlName="description" required></textarea>
                <mat-error *ngIf="dealForm.get('description').invalid">{{getErrorMessage()}}</mat-error>
              </mat-form-field>
            </div>
            <div class="p-10" fxflex="100" fxflex.gt-sm="65" ng-reflect-flex="100" ng-reflect-flex-gt-sm="65"
                 style="flex: 1 1 65%; box-sizing: border-box; max-width: 65%;">
              <mat-form-field class="example-full-width">
                <input matInput placeholder="Search Product" aria-label="Products" [matAutocomplete]="autoProduct"
                       formControlName="product">
                <mat-autocomplete #autoProduct="matAutocomplete" [displayWith]="displayProduct">
                  <mat-option *ngFor="let product of filteredProducts | async" [value]="product"
                              (onSelectionChange)="onProductSelect(product)">
                    <!--<img class="example-option-img" aria-hidden [src]="state.flag" height="25">-->
                    <span>{{product.name}}</span>
                    <!--<small>Population: {{state.population}}</small>-->
                  </mat-option>
                </mat-autocomplete>
                <mat-error *ngIf="dealForm.get('product').invalid">{{getErrorMessage()}}</mat-error>
              </mat-form-field>
            </div>
            <div class="p-10" fxflex="100" fxflex.gt-sm="35" ng-reflect-flex="100" ng-reflect-flex-gt-sm="35"
                 style="flex: 1 1 35%; box-sizing: border-box; max-width: 35%;">
              <button class="btn-block" mat-raised-button color="primary" [disabled]="dealForm.invalid">
                Save
              </button>
            </div>
          </div>
          <div>
            <mat-table #table [dataSource]="dataSource" formArrayName="dealProducts">
              <ng-container matColumnDef="name">
                <mat-header-cell *matHeaderCellDef> Name</mat-header-cell>
                <mat-cell *matCellDef="let element; let i = index;" formGroupName="{{i}}"> {{ element.controls.product.value.name }}</mat-cell>
              </ng-container>
              <ng-container matColumnDef="description">
                <mat-header-cell *matHeaderCellDef> Description</mat-header-cell>
                <mat-cell *matCellDef="let element; let i = index;" formGroupName="{{i}}"> {{ element.controls.product.value.description }}</mat-cell>
              </ng-container>
              <ng-container matColumnDef="quantity">
                <mat-header-cell *matHeaderCellDef> Quantity</mat-header-cell>
                <mat-cell *matCellDef="let element; let i = index;" formGroupName="{{i}}">
                  <mat-form-field>
                    <input matInput type="number" formControlName="quantity" required>
                    <mat-error *ngIf="dealForm.get('price').invalid">{{getErrorMessage()}}</mat-error>
                  </mat-form-field>
                </mat-cell>
              </ng-container>
              <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
              <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
            </mat-table>
          </div>
        </form>
        <pre>
          {{dealForm.value |json}}
        </pre>
      </mat-card-content>
    </mat-card>
  </div>
</div>
1
could you create stackblitz example please?? - Akj

1 Answers

0
votes

I had a similar problem with form controls not updating in a mat-table. Looks like a bug, but I think you can workaround that by referencing the formControl with [formGroup] instead of formGroupName. In your example:

<input matInput type="number" [formControl]="dealForm.get('dealProducts').controls[i].get('quantity')" required>