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
3
  • 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 angular.io/guide/reactive-forms#replacing-a-form-control-value (I linked to a specific section, but you'd better read the whole guide) Commented Apr 13, 2019 at 6:02
  • 1
    any reason not to use ngModel two-way binding? I'm new to angular so forgive if this is obvious / not helpful Commented Apr 13, 2019 at 6:03
  • 3
    @AndrewAllen ngModel is for template-driven forms. You don't use it when using reactive forms. Commented Apr 13, 2019 at 6:04

1 Answer 1

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']);
    });
}
Sign up to request clarification or add additional context in comments.

12 Comments

@JBNizet sorry, I copy pasta the OP's code because I am lazy AF. Will remove it
I think I tried similar version of your solution. Let me try again.
How do we deal with select drop downs? <select class="form-control" formControlName="currency" id="currency" name="currency"> <option *ngFor="let currency of currencies" value={{currency.id}}> {{currency.name}} </option> </select> 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
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.
@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
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.