Search code examples
angulartypescriptangular-forms

Angular FormGroup patchValue/setValue doesn't trigger property decorator's getter


I'm trying to use a custom property decorator called @Amount() that would eventually format the property value into a specific format that I need.

amount.decorator.ts

export const Amount = () => 
  (target: any, key: string) => {
    let _value: number | string = target[key];

    if (delete target[key]) {
      return Object.defineProperty(target, key, {
        configurable: false,
        enumerable: true,
        get: () => {
          console.log("returning", "$" + _value);
          return "$" + _value;
        },
        set: (val) => {
          console.log("setting val", val);
          _value = val;
        },
      });
    }
  }

summary.model.ts

export class Summary {
  regularField!: string;
  @Amount() amountField!: number | string;
} 

form.component.ts

export class SummaryComponent implements OnInit {
  summary: Summary;
  form: FormGroup = new FormGroup({
    regularField: new FormControl(''),
    amountField: new FormControl('Initial value'),
  });

  ngOnInit() {
    this.summary = this.service.getSummary();
    // summary.regularField is "Hello world!"
    // summary.amountField is 123.00
    
    this.summary.amountField = 456.00; <-- triggers "setting val" comment as expected

    console.log(this.summary.amountField); <-- triggers "returning" comment from @Amount() as expected 

    console.log(this.form.controls['amountField'].value); <-- shows "Initial value"

    this.form.patchValue(this.summary || {}); <-- expectation is the getter is called in order to patch it to the formcontrol, but it is not is called

    console.log(this.form.controls['amountField'].value); <-- still shows "Initial value"
  }
}

Is there something I'm missing, or some internal logic I'm misunderstanding about patchValue that making it not retrieve the amountField property?


Solution

  • To understand what's happening, you need to see the implementation of patchvalue/setValue() :

     (Object.keys(value) as Array<keyof TControl>).forEach(name => {
          assertControlPresent(this, true, name as any);
          (this.controls as any)[name].setValue(
              (value as any)[name], {onlySelf: true, emitEvent: options.emitEvent});
        });
    

    As you can see it iterates over the keys of the value to update the controls.

    Here is a decorator that fixes your issue :

    export const Amount = () => (target: any, key: string) => {
      let _value: number | string = target[key];
    
      Object.defineProperty(target, key, {
        get: () => '',
        set: function (v: string) {
          var val = '';
          Object.defineProperty(this, key, {
            get: () => {
              console.log('returning', '$' + _value);
              return '$' + _value;
            },
            set: (val) => {
              console.log('setting val', val);
              _value = val;
            },
            enumerable: true,
          });
          this[key] = v;
        },
      });
    };
    

    See this answer for the reason why.