Angular reactive form returns empty values when fields are untouched
Asked Answered
S

1

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
Shoebill answered 13/4, 2019 at 5:53 Comment(3)
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)Southeastward
any reason not to use ngModel two-way binding? I'm new to angular so forgive if this is obvious / not helpfulFerric
@AndrewAllen ngModel is for template-driven forms. You don't use it when using reactive forms.Southeastward
C
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']);
    });
}
Chamonix answered 13/4, 2019 at 6:6 Comment(12)
@JBNizet sorry, I copy pasta the OP's code because I am lazy AF. Will remove itChamonix
@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 valuesChamonix
I think I tried similar version of your solution. Let me try again.Shoebill
@Jadda okey. Hmm, so you did use patchValue to update the form and .value to get the form data?Chamonix
I have not. I will try this nowShoebill
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 knowShoebill
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.Southeastward
@wentjun: It worked. Thanks. Updating form with PatchValue seems to be working fineShoebill
@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 valueChamonix
@wentjun: I simplified my select class <label>Category: <select class="form-control" name="category" formControlName="category"> <option *ngFor="let category of categories"> {{category.name}} </option> </select> </label> 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 for complete code baseShoebill
@Jadda Hmm, the github link seems to be broken! <option *ngFor="let category of categories"> Hmm, did you try binding the value atrribute, as stated earlier?Chamonix
Also, is that question related to this hmm #55671165Chamonix

© 2022 - 2024 — McMap. All rights reserved.