Search code examples
angularangular2-formsangular-reactive-formsangular-forms

Disabled controls from FormGroup (part of form custom form control) are excluded by .getRawValue() in parent


Having a component that implements ControlValueAccessor, with internal FormGroup to maintain the state of custom form control. When any field, that's part of that FormGroup is disabled, the field isn't visible when calling .getRawValue() method in parent form .

By specification, .getRawValue() should return the raw object, including disabled fields.

I've checked the code of .getRawValue() and here's what I found:

 getRawValue(): any {
    return this._reduceChildren(
        {}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => {
          acc[name] = control instanceof FormControl ? control.value : (<any>control).getRawValue();
          return acc;
        });
  }

So, basically, when form control is instance of FormControl (that's the case when using custom form controls, correct?), it retrieves .value instead of .getRawValue() and that's why the disabled controls of nested form are not included in the final object.

Stackblitz demo

Steps to reproduce:

1) Click on "Disable year" button on any of three custom form controls displayed in the UI.

2) Examine the output below => .getRawValue() and .value responses are identical.

Do you have any idea how I can overcome this? I'm looking for a way to retrieve the disabled controls as well in the parent form.


Solution

  • Kav, in your custom form control you has

      registerOnChange(fn: (v: any) => void) {
            this.formGroup.valueChanges.subscribe(fn);
        }
    

    so, your component return the "value" of formGroup. As a control is disabled, the value not return this field. You can change your customControl to return the rawValue of the formGroup, for this you need create a onChangeFunction, and in a ngOnInit subscribe to changes and send the rawValues. As we subscribe it's good unsubscribe using a takeWhile and a variable

    export class DetailsFields implements ControlValueAccessor,OnInit,OnDestroy {
        ...
        onChange: (v:any) => void = () => {}; //<--define a function
        isAlive:boolean=true; //<--use to unsubscribe, see below
    
        registerOnChange(fn: (v: any) => void) {
          this.onChange = fn; //<--equal to function
        }
    
        //In ngOnInit
        ngOnInit()
        {
          this.formGroup.valueChanges.pipe(takeWhile(()=>this.isAlive))
            .subscribe(v=>{
            //return this.formGroup.getRawValue()
            this.onChange(this.formGroup.getRawValue())
          })
        }
        //In ngOnDestroy
        ngOnDestroy() {  //make isAlive=False to unsubscribe
          this.isAlive=false;
        }
    

    But in this case, you received the year always is enabled or not

    There're another aproach, that is not have a custom form control, just a component to manage the make,year and color. For this, the first is change your app-component and create the form like another form with a formArray.

    <div id="cars" [formGroup]="form">
      <div formArrayName="cars">
      <div *ngFor="let car of form.get('cars').controls; let i = index;" 
       [formGroupName]="i">
        <app-details-fields [formGroup]="form.get('cars').at(i)" ></app-details-fields>
      </div>
      </div>
    </div>
    

    See that in a formArray we iterate over form.get('cars').controls and we need put a [formGroupName]="i". In the component simply pass as input [formGroup] form.get('cars').at(i)

    Of course, you need change your function "createCars" to return a formGroup. not a formControl that return an object type {make:..,color:..,year}

    createCar(car: any) {  //return a formGroup,not a formControl
        return this.builder.group({
          make: car.make,
          color: car.color,
          year: car.year
        });
      }
    

    Well, the details-fields becomes easer:

    details-fields.component.ts

    @Component({
      selector: 'app-details-fields',
      templateUrl: './details-fields.component.html',
      styleUrls: ['./details-fields.component.css']  ,
    })
    
    export class DetailsFields {
       @Input() formGroup:FormGroup
    
       disableYear() {
          this.formGroup.get('year').disable();
        }
    
        enableYear() {
          this.formGroup.get('year').enable();
        }
    }
    

    details-fields.component.html

    <div [formGroup]="formGroup">
      <div class="car-wrap">
          <div>
              <p class="title">This car is a {{formGroup.get('make').value}}</p>
          <div>
            <input type="text" formControlName="make">
            <input type="number" formControlName="year">
            <input type="text" formControlName="color">
          </div>
          <div>
            <button style="margin-top: 3px;" (click)="enableYear()">Enable year</button>
            <button style="margin-top: 3px;" (click)="disableYear()">Disable year</button>
          </div>
        </div>
      </div>
    </div>