Search code examples
javascriptangularjsangularjs-scopewatch

Deep $watch a collection in AngularJS


Here is my object:

$scope.steps = {
        service: {
            selected_service: '',
            selected_month: '',
            selected_year: '',
            selected_day: ''
        },
        payment: {
            selected_dd_type: '',
            cc: {
                selected_card_type: '',
                credit_card_number: '',
                name_on_card: '',
                expiry_month: '',
                expiry_year: ''
            },
            bank_acc: {
                bsb_number: '',
                account_number: '',
                account_holder_name: ''
            }
        },
        agreement: {
            agreement_acceptance: false
        }
    }

Here are the $watchCollection:

$scope.$watchCollection('steps.service', function(newVal, oldVal) {
        $scope.canExit = true;
    });

    $scope.$watchCollection('steps.payment', function(newVal, oldVal) {
        $scope.canExit = true;
    }, true);

    $scope.$watchCollection('steps.payment.cc', function(newVal, oldVal) {
            $scope.canExit = true;
    }, true);

    $scope.$watchCollection('steps.payment.bank_acc', function(newVal, oldVal) {
            $scope.canExit = true;
    }, true);

    $scope.$watchCollection('steps.agreement', function(newVal, oldVal) {
            $scope.canExit = true;
    }, true);

Everything is working fine.

There must be a better way to $watch the $scope.steps object.

If I $watch steps it is not going to work. I guess Angular can't watch deep enough

 $scope.$watchCollection('steps', function(newVal, oldVal) {
                $scope.canExit = true;
        }, true);

Thank you for your guidance.


Solution

  • Here you go. Pass the value true as the third option in the $watch:

    $scope.$watch('steps', function(newValue, oldValue) {
         // callback on deep watch
         $scope.canExit = true;
    }, true);
    

    Also, make sure you add an if condition inside the watch since watch will also be triggered when you assign the value to $scope.steps.

    Working example:

    var app = angular.module("sa", []);
    
    app.controller("FooController", function($scope) {
      $scope.canExit = false;
    
      $scope.steps = {
        service: {
          selected_service: '',
          selected_month: '',
          selected_year: '',
          selected_day: ''
        },
        payment: {
          selected_dd_type: '',
          cc: {
            selected_card_type: '',
            credit_card_number: '',
            name_on_card: '',
            expiry_month: '',
            expiry_year: ''
          },
          bank_acc: {
            bsb_number: '',
            account_number: '',
            account_holder_name: ''
          }
        },
        agreement: {
          agreement_acceptance: false
        }
      };
    
      $scope.reset = function() {
        $scope.canExit = false;
      }
    
      // Using Math.random() just for demo so that the watch can be invoked
      $scope.changeService = function() {
        $scope.steps.service.selected_service = Math.random();
      }
    
      $scope.changeDate = function() {
        $scope.steps.payment.cc.selected_card_type = Math.random();
      }
    
      $scope.changeAgreement = function() {
        $scope.steps.agreement.agreement_acceptance = Math.random();
      }
    
      $scope.$watch('steps', function(newValue, oldValue) {
        if (newValue && (newValue !== oldValue)) {
          // callback on deep watch
          $scope.canExit = true;
        }
      }, true);
    });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    
    <div ng-app="sa" ng-controller="FooController">
      <button ng-click="changeService()">Change service</button>
      <button ng-click="changeDate()">Change expiry date</button>
      <button ng-click="changeAgreement()">Change agreement</button>
      <br>
      <br>
      <br>Can exit: {{canExit}}
      <button ng-click="reset()">Reset</button>
    </div>