Angular 7 [disabled] not detecting the change when a nested object's property values gets changed

I have a need to write a generic function which will enable/disable buttons based on conditions inside a JSON.

JSON:

    {
        PURCHASE_LIEN: {
          LABEL: 'PURCHASE LIEN',
          DISABLE: true,
          CONDITION: [
            {
              ReviewPurchaseDecisionStatus: true,
              PurchaseDecisionStatus: false
            },
            {
              ReviewPurchaseDecisionStatus: true,
              'ReviewPurchaseDecision.Status': 'NOT QUALIFIED'
            }
          ]
        },
        NOT_QUALIFIED: {
          LABEL: 'NOT QUALIFIED',
          DISABLE: true,
          CONDITION: [
            {
              ReviewPurchaseDecisionStatus: true,
              PurchaseDecisionStatus: false
            },
            {
              ReviewPurchaseDecisionStatus: true,
              'ReviewPurchaseDecision.Status': 'PURCHASED'
            }
          ]
        }
  }

In the JSON, I have two buttons "PURCHASE_LIEN" and "NOT_QUALIFIED". Both functions has array of conditions based on the condition, the button should enable/disable using "DISABLE" property.

VALIDATION.SERVICE.TS

The below function will set the DISABLE property of the button based on the conditions against an object (selectedRow).

public disableButton(buttonContainer: any, buttonID: string, selectedRow: any) {
    let status = true;
    for (let i = 0; i < buttonContainer[buttonID]['CONDITION'].length; i++) {
      const condition = buttonContainer[buttonID]['CONDITION'][i];
      for (const conditionName in condition) {
        if (condition[conditionName] !== selectedRow[condition]) {
          status = false;
        }
      }
      if (status) {
        buttonContainer[buttonID].DISABLE = false;
        break;
      } else {
        buttonContainer[buttonID].DISABLE = true;
      }

    }

    return buttonContainer;
  }

app.component.html

 <div class="col-12 col-sm-6 col-md-4 col-lg-2">
        <button class="btn btn-primary btn-semi-circle" (click)="showModal('Purchase')"
         [disabled]="disableButton(buttonGroup, 'PURCHASE_LIEN', selectedRowData)">Purchase
          Lien</button>
          <!-- [disabled]="PURCHASE_LIEN_DISABLE" -->
      </div>
      <div class="col-12 col-sm-6 col-md-4 col-lg-2">
        <button class="btn btn-danger btn-semi-circle" (click)="showModal('Not Qualified')"
         [disabled]="disableButton(buttonGroup, 'NOT_QUALIFIED', selectedRowData)">Not
          Qualified</button>
          <!-- [disabled]="NOT_QUALIFIED_DISABLE" -->
      </div>

app.component.ts

export class ReviewPurchaseDecisionComponent implements OnInit {
  public buttonGroup: any = {
    PURCHASE_LIEN: {
      LABEL: 'PURCHASE LIEN',
      DISABLE: true,
      CONDITION: [
        {
          ReviewPurchaseDecisionStatus: true,
          PurchaseDecisionStatus: false
        },
        {
          ReviewPurchaseDecisionStatus: true,
          'ReviewPurchaseDecision.Status': 'NOT QUALIFIED'
        }
      ]
    },
    NOT_QUALIFIED: {
      LABEL: 'NOT QUALIFIED',
      DISABLE: true,
      CONDITION: [
        {
          ReviewPurchaseDecisionStatus: true,
          PurchaseDecisionStatus: false
        },
        {
          ReviewPurchaseDecisionStatus: true,
          'ReviewPurchaseDecision.Status': 'PURCHASED'
        }
      ]
    }
  };

  constructor(
    public router: Router,
    public validation: ValidationService,
    private fb: FormBuilder,
    private http: HttpHelperService,
    private myMonitoringService: MyMonitoringService,
    private authentication: AuthenticationService,
    private sessionService: SessionService,
    public dialogService: DialogServiceService,
    private caseService: CaseService,
    private cookieService: CookieService
  ) {}

  disableButton(buttonContainer: any, buttonID: string, selectedRow: any) {
    this.buttonGroup = this.validation.disableButton(
      buttonContainer,
      buttonID,
      selectedRow
    );
    return this.buttonGroup[buttonID].DISABLE;
  }
}

The disableButton method in validation service changes the DISABLE property value to true/false based on the conditions, but the button doesn't enable. It doesn't detect the changes

2 answers

  • answered 2019-01-11 05:53 Yanis-git

    change detection in angular is huge topic. Here you have very nested object where angular have to keep track to detect change.

    For that angular have no choice to recursively check each field of your object and compare it to previous state to detect if you have make any change. This step is call digest and mark as dirty. Is very ressource consuming is why angular do it in very particular case (list not complete is just for demonstration):

    • @Output is trigger
    • @Input is mutate
    • browser event is dispatch (click, hover, etc)
    • timeout
    • interval
    • ....

    here you mutate your object by calling function from [disable] html attribute. I suspect this case is not cover by the default changeDetectionStrategy.

    Anyway angular team not recommand to manipulate object like this. Is recommand to use any of this two approch : - Avoid mutate state, prefer create new object and replace previous one. Like this angular simple have to do myPreviousObject !== myNewObject instead of :

    if (
        myPreviousObject.prop1 !== myNewObject.prop1 ||
        myPreviousObject.prop2 !== myPreviousObject.prop2 ||
        ....
    )
    
    • Use Observable with immutable state.

    for my demonstration i have use second approach and here you can find simple implementation :

    my model :

    export interface disableState {
      PURCHASE_LIEN: {
        disable: boolean;
        disable$: BehaviorSubject<boolean>
      };
    
      NOT_QUALIFIED: {
        disable: boolean;
        disable$: BehaviorSubject<boolean>
      };
    }
    

    inside my component i have property like this :

    disableState: disableState = {
        NOT_QUALIFIED: { 
          disable: false, 
          disable$: new BehaviorSubject<boolean>(false),
          },
       PURCHASE_LIEN: { 
          disable: false, 
          disable$: new BehaviorSubject<boolean>(false),
          },
      }
    

    and when i want to change this value i can do it like this :

    /**
     * Call your service like :
     * this.validation.disableButton()
     */
    this.disableState['NOT_QUALIFIED'].disable = true;
    this.disableState['NOT_QUALIFIED'].disable$.next(true);
    

    live coding

    The full component :

    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: [ './app.component.css' ],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class AppComponent  {
    
      disableState: disableState = {
        NOT_QUALIFIED: { 
          disable: false, 
          disable$: new BehaviorSubject<boolean>(false),
          },
       PURCHASE_LIEN: { 
          disable: false, 
          disable$: new BehaviorSubject<boolean>(false),
          },
      }
    
      showModal(id: string) {
        console.log(`Open modal : ${id}`);
      }
    
    
      dummyPropertyChange() {
        /**
         * Call your service like :
         * this.validation.disableButton()
         */
        this.disableState['NOT_QUALIFIED'].disable = true;
        this.disableState['NOT_QUALIFIED'].disable$.next(true);
      }
    }
    

    Notice i have switch the ChangeDetectionStrategy to ask angular to only perform dirty checking by simple object comparaison (instead of nested one). From now if i want to update data i should :

    • change the whole variable data (instead of only nested property)
    • use observable to perform change on timeline.

  • answered 2019-01-11 09:08 Bear Nithi

    I have solved this issue, by creating a directive. The directive will access the nativeElement of the button, it enables / disables button based on the condition in the JSON

    btn-disable.directive.ts

    import {
      Directive,
      Renderer2,
      ElementRef,
      Input,
      OnChanges
    } from '@angular/core';
    
    
    @Directive({
      selector: '[appBtnDisable]'
    })
    export class BtnDisableDirective implements OnChanges {
      @Input() buttonContainer: any = {};
      @Input() buttonID = '';
      @Input() condition: any = {};
    
      constructor(public ele: ElementRef, public renderer: Renderer2) {
        if (this.buttonID) {
          this.disableButton(this.buttonContainer, this.buttonID, this.condition);
        }
      }
    
      ngOnChanges() {
        if (this.buttonID) {
          this.disableButton(this.buttonContainer, this.buttonID, this.condition);
        }
      }
    
      public disableButton(
        buttonContainer: any,
        buttonID: string,
        selectedRow: any
      ) {
    
        for (let i = 0; i < buttonContainer[buttonID]['CONDITION'].length; i++) {
          const condition = buttonContainer[buttonID]['CONDITION'][i];
          let status = true;
          for (const conditionName in condition) {
            if (
              this.convertNulltoUndefined(condition[conditionName]) !== this.evaluate(conditionName, selectedRow)
            ) {
              status = false;
            }
          }
    
          if (status) {
            this.ele.nativeElement.disabled = false;
            break;
          } else {
            this.ele.nativeElement.disabled = true;
          }
        }
      }
    
    
    
      evaluate(data: string, selectedRow: any): any {
        if (data.split('.').length > 1) {
          const value = 'selectedRow.' + data;
          try {
            return eval(value);
          } catch (error) {
            return undefined;
          }
        } else {
          return selectedRow[data];
        }
      }
    
      convertNulltoUndefined(data) {
        return (data === null) ? undefined : data;
      }
    }
    

    app.component.html

      <button class="btn btn-primary btn-semi-circle" (click)="showModal('Purchase')"
                appBtnDisable [condition]="selectedRowData" [buttonContainer]="buttonGroup" [buttonID]="'PURCHASE_LIEN'">Purchase
                  Lien</button>