6

In my use case when the user clicks on the edit button, Angular makes an HTTP call to the backend and retrieves the object, then populates those values on EDIT form. The user can update or leave the fields untouched. When clicked update button, Angular should take all those values present in form and send them to the backend. So, here is the problem, after loading the values into edit page form and updating some fields and leaving some fields untouched makes untouched values empty. This is really strange

product-edit.component.html

<div *ngIf="productDataAvailable()">
  <h2>Update Product</h2>
  <br/>
  <form [formGroup]="productForm">
    <div class="form-group">
      <label for="id">Product Id</label>
      <input class="form-control" formControlName="id" id="id" type="text" value="{{product.id}}">

      <small class="form-text text-muted" id="emailHelp"></small>
    </div>
    <div class="form-group">
      <label for="name">Name</label>
      <input class="form-control" formControlName="name" id="name" type="text" value="{{product.name}}">
    </div>
    <div class="form-group">
      <label for="description">Description</label>
      <input class="form-control" formControlName="description" height="100" id="description" required type="text"
             [value]="product.description" width="200">
    </div>
    <div class="form-group">
      <label for="currency">Price</label> <br/>
      <label>
        <select (load)="loadCurrencies()" class="form-control"  [value]="product.price.currency.symbol" formControlName="currency" id="currency" name="currency">
          <option *ngFor="let currency of currencies" value={{currency.id}}>
            {{currency.name}}
          </option>
        </select>
      </label>
      <input formControlName="price" id="price" required style="margin: 10px; padding: 5px" type="text" [value]="product.price.amount">
    </div>

    <div class="form-group">
      <label>Category:
        <select (load)="loadCategories()" class="form-control" formControlName="category" name="category">
          <option  [value]="category.id" *ngFor="let category of categories">
            {{category.name}}
          </option>
        </select>
      </label>
    </div>

    <div class="form-group">
      <label>Manufacturer:
        <select (load)="loadManufacturers()" class="form-control" [value]="product.manufacturer.name" formControlName="manufacturer" name="manufacturer">
          <option [value]="manufacturer.id" *ngFor="let manufacturer of manufacturers" >
            {{manufacturer.name}}
          </option>
        </select>
      </label>
    </div>

    <button (click)="updateProduct()" class="btn btn-primary" type="submit">Submit</button>
    <button (click)="goBack()" class="btn btn-primary" style="margin-left: 30px" type="button">Cancel</button>

  </form>

</div>

ProductEditComponent

import {Component, OnInit} from '@angular/core';
import {Product} from '../model/product';
import {ProductService} from '../service/product.service';
import {ActivatedRoute, Router} from '@angular/router';
import {CATEGORY_API_URL, CURRENCY_API_URL, MANUFACTURER_API_URL, PRODUCT_API_URL, SERVER_URL} from '../../../app.constants';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {Price} from '../model/price';
import {Currency} from '../model/currency';
import {Category} from '../../category/model/category';
import {Manufacturer} from '../model/manufacturer';
import {CategoryService} from '../../category/service/category.service';

@Component( {
              selector: 'app-product-edit',
              templateUrl: './product-edit.component.html',
              styleUrls: ['./product-edit.component.css']
            } )
export class ProductEditComponent implements OnInit
{
  product: Product;
  categories: Array<Category>;
  currencies: Array<Currency>;
  manufacturers: Array<Manufacturer>;

  productForm=new FormGroup( {
                               id: new FormControl( {value: '', disabled: true}, Validators.minLength( 2 ) ),
                               name: new FormControl( '' ),
                               description: new FormControl( '' ),
                               price: new FormControl( '' ),
                               category: new FormControl( '' ),
                               currency: new FormControl( '' ),
                               manufacturer: new FormControl( '' )
                             } );

  constructor(private productService: ProductService,
              private categoryService: CategoryService,
              private route: ActivatedRoute,
              private router: Router)
  {
  }

  ngOnInit()
  {

    this.getProduct();
    this.loadCategories();
    this.loadCurrencies();
    this.loadManufacturers();
  }

  productDataAvailable(): boolean
  {
    return this.product!==undefined;
  }

  goBack()
  {
    this.router.navigate( ['/product'] );
  }

  private getProduct()
  {
    const id=this.route.snapshot.paramMap.get( 'id' );
    const url=SERVER_URL+PRODUCT_API_URL+'find/'+id;
    this.productService.getProductDetails( url ).pipe()
        .subscribe(
          data =>
          {
            this.product=data;
          },
          error =>
          {
            console.log( error );
          },
          () => console.log( 'getProduct() success' ) );
  }

  private updateProduct()
  {
    const id=this.route.snapshot.paramMap.get( 'id' );
    const url=SERVER_URL+PRODUCT_API_URL+'update';

    const product=new Product();
    product.id=Number( id );
    product.name=this.productForm.get( 'name' ).value;
    product.description=this.productForm.get( 'description' ).value;
    const currency=new Currency( this.productForm.get( 'currency' ).value, 'USD', '$' );
    product.price=new Price(currency , this.productForm.get( 'price' ).value );
    product.category=new Category( this.productForm.get( 'category' ).value );
    product.manufacturer=new Manufacturer( this.productForm.get( 'manufacturer' ).value );
    product.lastModifiedBy='Admin';
    product.lastModifiedDate='Admin';

    this.productService.updateProduct( url, product ).subscribe(
      value =>
      {
        console.log( 'Successfully updated product' );
      }, error1 =>
      {
        console.log( 'Failed to update product' );
      },
      () =>
      {
        this.router.navigate( ['/product/list'] );
      } );
  }

  private loadCategories()
  {
    const url=SERVER_URL+CATEGORY_API_URL+'list';

    this.categoryService.getCategories( url ).subscribe(
      categories =>
      {
        // @ts-ignore
        this.categories=categories;
        console.log( 'Successfully loaded categories' );
      },
      error1 =>
      {
        console.log( 'Failed to load categories' );
      },
      () =>
      {
      } );
  }

  private loadCurrencies()
  {
    const url=SERVER_URL+CURRENCY_API_URL+'list';

    this.productService.getCurrencies( url ).subscribe(
      currencies =>
      {
        this.currencies=currencies;
      },
      error1 =>
      {
        console.log( 'Failed to load currencies' );
      },
      () =>
      {
      } );
  }

  private loadManufacturers()
  {
    const url=SERVER_URL+MANUFACTURER_API_URL+'list';

    this.productService.getManufacturers( url ).subscribe(
      manufacturers =>
      {
        this.manufacturers=manufacturers;
        console.log( 'Successfully loaded manufacturers' );
      },
      error1 =>
      {
        console.log( 'Failed to load manufacturers' );
      },
      () =>
      {
      } );
  }
}

Angular Versions

Angular CLI: 7.3.8
Node: 10.15.0
OS: darwin x64
Angular: 7.2.12
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.13.8
@angular-devkit/build-angular     0.13.8
@angular-devkit/build-optimizer   0.13.8
@angular-devkit/build-webpack     0.13.8
@angular-devkit/core              7.3.8
@angular-devkit/schematics        7.3.8
@angular/cli                      7.3.8
@ngtools/webpack                  7.3.8
@schematics/angular               7.3.8
@schematics/update                0.13.8
rxjs                              6.4.0
typescript                        3.2.4
webpack                           4.29.0
Pavan Jadda
  • 4,306
  • 9
  • 47
  • 79
  • 1
    You're missing the whole point of how reactive forms, and Angular i general, works. The truth is in the model, not in the view. If you want a form control to have a value, you store the value in the model of this form control (i.e. you set the value of the FormControl object). You don't use `value="{{product.id}}"`. Modifying the model modifies the view. Entering something in the input modifies the model. See https://angular.io/guide/reactive-forms#replacing-a-form-control-value (I linked to a specific section, but you'd better read the whole guide) – JB Nizet Apr 13 '19 at 06:02
  • 1
    any reason not to use `ngModel` two-way binding? I'm new to angular so forgive if this is obvious / not helpful – Andrew Allen Apr 13 '19 at 06:03
  • 3
    @AndrewAllen ngModel is for template-driven forms. You don't use it when using reactive forms. – JB Nizet Apr 13 '19 at 06:04

1 Answers1

9

As far as I can see, you have made the HTTP request to get the data from your servers, but you did not populate your productForm FormGroup the right way. Since you are using reactive forms, I would highly recommend you to use patchValue or setValue to update your FormControls.

For your case, I would recommend patchValue, as it is more flexible than setValue. patchValue do not require all FormControls to be specified within the parameters in order to update/set the value of your Form Controls.

This is how you can use patchValue. On your getProduct() method, you can pass the properties in your data response from getProductDetails() into your FormGroup by doing this;

getProduct() {
  const id = this.route.snapshot.paramMap.get('id');
  const url = SERVER_URL + PRODUCT_API_URL + 'find/' + id;
  this.productService.getProductDetails(url).pipe()
    .subscribe(
      data => {
        this.productForm.patchValue({
          id: data.id
          name: data.name
          description: data.description
          // other form fields
        })
      },
      error => {
        console.log(error);
      },
      () => console.log('getProduct() success'));
}

In addition, on your template html, there is no need to bind your value attributes on each <input> or <select>. You can remove all of them. This is because, you are already updating the values using patchValue.

<div class="form-group">
  <label for="name">Name</label>
  <input class="form-control" formControlName="name" id="name" type="text">
</div>
<div class="form-group">
  <label for="description">Description</label>
  <input class="form-control" formControlName="description" height="100" id="description" required type="text" width="200">
</div>

When you need to get data from your productForm, you can use the value property which is exposed on your FormGroup and FormControls.

updateProduct() {
  const id = this.route.snapshot.paramMap.get('id');
  const url = SERVER_URL + PRODUCT_API_URL + 'update';

  //console.log(this.productFrom.value) 
  const product = this.productForm.value

  this.productService.updateProduct(url, product).subscribe(
    value => {
      console.log('Successfully updated product');
    }, error1 => {
      console.log('Failed to update product');
    },
    () => {
      this.router.navigate(['/product/list']);
    });
}
wentjun
  • 40,384
  • 10
  • 95
  • 107
  • 2
    @JBNizet sorry, I copy pasta the OP's code because I am lazy AF. Will remove it – wentjun Apr 13 '19 at 06:30
  • @JBNizet And thanks for pointing that out. Also, correct me if I am wrong, but will it be safer to return the observables from calling `loadCategories()` using `forkJoin` first, before making the request from` getProduct`? Since the above methods are called asynchronously, and the OP did not include any safe operators to guard it from null values – wentjun Apr 13 '19 at 06:33
  • 1
    I think I tried similar version of your solution. Let me try again. – Pavan Jadda Apr 13 '19 at 06:34
  • @Jadda okey. Hmm, so you did use patchValue to update the form and .value to get the form data? – wentjun Apr 13 '19 at 06:37
  • I have not. I will try this now – Pavan Jadda Apr 13 '19 at 06:38
  • 1
    How do we deal with select drop downs? `` I need to iterate through options and set `currency.id` as an option value in this case. Where should I do this? in HTML or component? I don't think it's possible to put this in HTML. Please let me know – Pavan Jadda Apr 13 '19 at 06:49
  • 2
    1. It should be `[value]="currency.id"`. 2. The FormControl for this select box should contain the selected value, i.e. the ID of the crrency that should be selected. – JB Nizet Apr 13 '19 at 06:57
  • @wentjun: It worked. Thanks. Updating form with PatchValue seems to be working fine – Pavan Jadda Apr 13 '19 at 07:01
  • 1
    @Jadda Glad to help! And yes, JB Nizet is correct. You will need to bind it to the `value` attribute, which can be a string or number. If you need to bind it to an object, you will need to use `ngValue` instead of `value` – wentjun Apr 13 '19 at 10:16
  • 1
    @wentjun: I simplified my select class `` and select drop down shows options. But when I submit I get empty object for category `product.category=this.productForm.value.category;` Any suggestions? Please see [Github](https://github.com/pavankjadda/SpringSecurity-SpringData-UI/tree/development/src/app/order/product/product-new) for complete code base – Pavan Jadda Apr 13 '19 at 22:25
  • @Jadda Hmm, the github link seems to be broken! ` – wentjun Apr 14 '19 at 04:08
  • Also, is that question related to this hmm https://stackoverflow.com/questions/55670665/angular-7-select-dropdown-object-is-empty-when-not-touched – wentjun Apr 14 '19 at 04:38