Angularjs: 'controller as syntax' and $watch

JavascriptAngularjsAngularjs ScopeNg Controller

Javascript Problem Overview


How to subscribe on property change when using controller as syntax?

controller('TestCtrl', function ($scope) {
  this.name = 'Max';
  this.changeName = function () {
    this.name = new Date();
  }
  // not working       
  $scope.$watch("name",function(value){
    console.log(value)
  });
});

<div ng-controller="TestCtrl as test">
  <input type="text" ng-model="test.name" />
  <a ng-click="test.changeName()" href="#">Change Name</a>
</div>  

Javascript Solutions


Solution 1 - Javascript

Just bind the relevant context.

$scope.$watch(angular.bind(this, function () {
  return this.name;
}), function (newVal) {
  console.log('Name changed to ' + newVal);
});

Example: http://jsbin.com/yinadoce/1/edit

UPDATE:

Bogdan Gersak's answer is actually kind of equivalent, both answers try binding this with the right context. However, I found his answer cleaner.

Having that said, first and foremost, you have to understand the underlying idea behind it.

UPDATE 2:

For those who use ES6, by using arrow function you get a function with the right context OOTB.

$scope.$watch(() => this.name, function (newVal) {
  console.log('Name changed to ' + newVal);
});

Example

Solution 2 - Javascript

I usually do this:

controller('TestCtrl', function ($scope) {
    var self = this;

    this.name = 'Max';
    this.changeName = function () {
        this.name = new Date();
   }
   
   $scope.$watch(function () {
       return self.name;
   },function(value){
        console.log(value)
   });
});

Solution 3 - Javascript

You can use:

   $scope.$watch("test.name",function(value){
        console.log(value)
   });

This is working JSFiddle with your example.

Solution 4 - Javascript

Similar to using the "test" from "TestCtrl as test", as described in another answer, you can assign "self" your scope:

controller('TestCtrl', function($scope){
    var self = this;
    $scope.self = self;

    self.name = 'max';
    self.changeName = function(){
            self.name = new Date();
        }

    $scope.$watch("self.name",function(value){
            console.log(value)
        });
})

In this way, you are not tied to the name specified in the DOM ("TestCtrl as test") and you also avoid the need to .bind(this) to a function.

...for use with the original html specified:

<div ng-controller="TestCtrl as test">
    <input type="text" ng-model="test.name" />
    <a ng-click="test.changeName()" href="#">Change Name</a>
</div>

Solution 5 - Javascript

AngularJs 1.5 supports the default $ctrl for the ControllerAs structure.

$scope.$watch("$ctrl.name", (value) => {
    console.log(value)
});

Solution 6 - Javascript

you can actually pass in a function as the first argument of a $watch():

 app.controller('TestCtrl', function ($scope) {
 this.name = 'Max';
 
// hmmm, a function
 $scope.$watch(function () {}, function (value){ console.log(value) });
 });

Which means we can return our this.name reference:

app.controller('TestCtrl', function ($scope) {
    this.name = 'Max';
  
    // boom
    $scope.$watch(angular.bind(this, function () {
    return this.name; // `this` IS the `this` above!!
    }), function (value) {
      console.log(value);
    });
});

Read an interesting post about controllerAs topic https://toddmotto.com/digging-into-angulars-controller-as-syntax/

Solution 7 - Javascript

You can use $onChanges angular component lifecycle.

see documentation here: https://docs.angularjs.org/guide/component under Component-based application section

Solution 8 - Javascript

Writing a $watch in ES6 syntax wasn't as easy as I expected. Here's what you can do:

// Assuming
// controllerAs: "ctrl"
// or
// ng-controller="MyCtrl as ctrl"
export class MyCtrl {
  constructor ($scope) {
    'ngInject';
    this.foo = 10;
    // Option 1
    $scope.$watch('ctrl.foo', this.watchChanges());
    // Option 2
    $scope.$watch(() => this.foo, this.watchChanges());
  }

  watchChanges() {
    return (newValue, oldValue) => {
      console.log('new', newValue);
    }
  }
}

Solution 9 - Javascript

NOTE: This doesn't work when View and Controller are coupled in a route or through a directive definition object. What's shown below only works when there's a "SomeController as SomeCtrl" in the HTML. Just like Mark V. points out in the comment below, and just as he says it's better to do like Bogdan does it.

I use: var vm = this; in the beginning of the controller to get the word "this" out of my way. Then vm.name = 'Max'; and in the watch I return vm.name. I use the "vm" just like @Bogdan uses "self". This var, be it "vm" or "self" is needed since the word "this" takes on a different context inside the function. (so returning this.name wouldn't work) And yes, you need to inject $scope in your beautiful "controller as" solution in order to reach $watch. See John Papa's Style Guide: https://github.com/johnpapa/angularjs-styleguide#controllers

function SomeController($scope, $log) {
    var vm = this;
    vm.name = 'Max';

    $scope.$watch('vm.name', function(current, original) {
        $log.info('vm.name was %s', original);
        $log.info('vm.name is now %s', current);
    });
}

Solution 10 - Javascript

Here is how you do this without $scope (and $watch!) Top 5 Mistakes - Abusing watch

If you are using "controller as" syntax, it's better and cleaner to avoid using $scope.

Here is my code in JSFiddle. (I am using a service to hold the name, otherwise the ES5 Object.defineProperty's set and get methods cause infinite calls.

var app = angular.module('my-module', []);

app.factory('testService', function() {
    var name = 'Max';

    var getName = function() {
        return name;
    }

    var setName = function(val) {
        name = val;
    }

    return {getName:getName, setName:setName};
});

app.controller('TestCtrl', function (testService) {
    var vm = this;

    vm.changeName = function () {
        vm.name = new Date();
    }

    Object.defineProperty(this, "name", {
        enumerable: true,
        configurable: false,
        get: function() {
            return testService.getName();
        },
        set: function (val) {
            testService.setName(val);
            console.log(vm.name);
        }
    }); 
});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMironView Question on Stackoverflow
Solution 1 - JavascriptRoy MilohView Answer on Stackoverflow
Solution 2 - JavascriptNico NapoliView Answer on Stackoverflow
Solution 3 - JavascriptArtyom PranovichView Answer on Stackoverflow
Solution 4 - Javascriptuser4389View Answer on Stackoverflow
Solution 5 - JavascriptNiels SteenbeekView Answer on Stackoverflow
Solution 6 - JavascriptAlexandrView Answer on Stackoverflow
Solution 7 - JavascriptPavel DurovView Answer on Stackoverflow
Solution 8 - JavascriptMaciej GurbanView Answer on Stackoverflow
Solution 9 - JavascriptwojjasView Answer on Stackoverflow
Solution 10 - JavascriptBinu JasimView Answer on Stackoverflow