10
votes

For an application I'm building, I'm retrieving data from an API and setting my form value directly with that response. For that I'm using the reactive forms module. I've built the form so that it matches the object I'm getting from the API. The problem is that some fields are supposed to be an object (FormGroup), but are NULL when empty. This results in the error "Cannot convert undefined or null to object".

FormGroup instance:

note: this.formBuilder.group({
   turnbook_file: [''],
   note: [''],
   additional_files: ['']
});

Response from API

note: NULL

My thought was that maybe it's possibile to put an OR statement or something where you define the FormGroup like this:

note: this.formBuilder.group({
   turnbook_file: [''],
   note: [''],
   additional_files: ['']
} | NULL);
7
it's not my best answer, but I like make a function to create a FormGroup that received or data or null and use conditional operator to create the form filled or empy, see stackoverflow.com/questions/48337798/… - Eliseo
@Eliseo I've seen a workaround like that. However, my groups sometimes go 3 layers deep. That means if i try to do it your way i would have 3 If statements in one line, which is (by my knowledge) not possible. IG: [data && data.note && data.note.author ? data.note.author : '']. This would still result in an error "cannot find 'author' of undefined", if data.note would be undefined. But please correct me if im wrong. - Thom
For me work, see stackblitz.com/edit/… - Eliseo
Please upvote the issue (Been open over a year) - github.com/angular/angular/issues/21021 - muttonUp
I don't really understand the question. The title is about patchValue but in the question is about building the form. What's wrong with building/patching a first level control and only build/patch the second level group only after a check If you have a data? - trungk18

7 Answers

2
votes

You can try using lodash pickBy method to wrap your api response before using it.

import * as _ from 'lodash';
...
//fetch data from API
note.patchValue(
    _.pickBy(apiResponse)
);
...
0
votes

You may define your form like this

note =new FormGroup({
   'turnbook_file': new FormControl(null),
   'note: new FormControl(null),
   'additional_files': new FormControl(null)
})
0
votes

If you define form and controls then you have to must pass values to that so if your response is null then set values of Form controls like this

note.setValue({
'turnbook_file': data.turnbook_file == null ? "": data.turnbook_file,
   'note: ,data.note == null ? "": data.note,
   'additional_files': data.additional_files == null ? "": 
    data.additional_files,
})
0
votes

You can simply filter out those values before patching the form

const obj = {
  foo: "bar",
  bar: null,
  cat: undefined,
  dog: "bark"
}

const truthy = Object.keys(obj).filter(item => obj[item] != undefined || obj[item] != null );
const newObj = {};
truthy.forEach(item =>Object.assign(newObj, { [item]: obj[item]}));
console.log(newObj) // { foo: "bar", dog: "bark" }

Once you filter out the falsy values, just use it to patch the form

0
votes

Two solutions come to mind

Modify FormGroup prototype

@NgModule({...})
export class AppModule {

  constructor() {
    const builtInFunction = FormGroup.prototype.patchValue;
    FormGroup.prototype.patchValue = function (value, options?) {
      builtInFunction.call(this, value || {}, options);
    }
  }

}

Extend FormGroup class

import {FormGroup, ValidatorFn, AbstractControlOptions, AsyncValidatorFn} from '@angular/forms';

export class MyFormGroup extends FormGroup {

  constructor(
    controls: {[key: string]: any},
    validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
    asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
  ) {
    super(controls, validatorOrOpts, asyncValidator);  
  }

  patchValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}) {
    super.patchValue(value || {}, options);
  }

}
0
votes

Short answer: In the current implementation of Angular Forms API - it's not possible to create/patch FormGroup with null or undefined value.

Reason: In all scenarios new FormGroup(), formBuilder.group(), patchValue() etc there is a call of a method Object.keys(), which throws an error Cannot convert undefined or null to object (according to JS specification) if we pass null/undefined as argument.

Proof:

  • FormBuilder's group() method (source code link):

      group(controlsConfig: {[key: string]: any}, ...) {
        const controls = this._reduceControls(controlsConfig);
        ...
      }
      _reduceControls(controlsConfig: {[k: string]: any}) {
        Object.keys(controlsConfig).forEach(...);
        ...
      }
    
  • FormGroup's patchValue (source code link):

    patchValue(value: {[key: string]: any}, ...) {
      Object.keys(value).forEach(...);
      ...
    }
    

Workarounds:

  • Change you API response in Backend side so it will send empty object {} instead of null;
  • Once you receive a response in Frontend side, you can do mapping: from null to {};
  • Extend FormGroup class and create a new one where you can override the constructor/patchValue: check if the argument is null/undefined - use empty object instead.
  • Extend FormBuilder service and create your own Singleton service (use it everywhere in your app) where you can override the group method (see prev point).
0
votes

improving bestbrain10 answer. You can use pickBy and isDefined together.

import { isEmpty, pickBy ,isUndefined,isNull} from 'lodash';
// Fetch data and remove all falsy  keys

this.formgroup.patchValue(
_.omitBy(formData, (v) => _.isUndefined(v) || _.isNull(v) || v === ''));