157

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>  
Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Miron
  • 2,137
  • 3
  • 15
  • 14

10 Answers10

161

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

Roy Miloh
  • 3,381
  • 1
  • 18
  • 17
  • 10
    Can we use it without $scope to avoid mix of this and $scope? – Miron Jun 06 '14 at 10:04
  • 4
    No as I know, but it's perfectly fine. `$scope` for you is a kind of service which supplies these kind of methods. – Roy Miloh Jun 06 '14 at 10:09
  • Can you clarify whether `name` in `return this.name;` refers to the name of the controller or the property "`name`" here? – Jannik Jochem Nov 16 '14 at 20:19
  • 3
    @Jannik, `angular.bind` returns a function with a bounded context (arg #1). In our case, we bind `this`, which is the instance of the controller, to the function (arg #2), so `this.name` means the property `name` of the instance of the controller. – Roy Miloh Nov 17 '14 at 21:11
  • I think I just understood how this works. When the bound function is called, it simply evaluates to the watched value, right? – Jannik Jochem Nov 17 '14 at 21:20
  • I wish it was simpler – piotr_cz Nov 22 '14 at 22:55
  • Is the `name` argument really necessary? I suspect it's unused because `this.name` will return the literal property named `name` of `this`. To use the `name` argument one would have to do `this[name]`, no? – Nico Aug 06 '15 at 03:51
  • this for me defeats the point of Controller as vm if for watches I still have to use scope. – Toolkit Mar 05 '16 at 13:25
  • What about performance? Isn't better to watch the proper object? https://jsperf.com/angular-watch-function-result-vs-string – sebasuy Mar 16 '17 at 18:11
139

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)
   });
});
Jossef Harush Kadouri
  • 32,361
  • 10
  • 130
  • 129
Nico Napoli
  • 1,867
  • 2
  • 13
  • 12
  • 3
    I agree that this is the best answer, though I would add that the confusion on this is probably on passing a function as the first argument in `$scope.$watch` and using that function to return a value from the closure. I have yet to run across another example of this, but it works and is the best. The reason I didn't choose the answer below (i.e., `$scope.$watch('test.name', function (value) {});`) is because it required that I hard-code what I named my controller in my template or in ui.router's $stateProvider and any change there would inadvertently break the watcher. – Morris Singer Apr 23 '15 at 15:48
  • Also, the only substantive difference between this answer and the presently accepted answer (which uses `angular.bind`) is whether you want to bind to `this` or simply add another reference to `this` within the closure. These are functionally equivalent, and, in my experience, this kind of choice is often a subjective call and the matter of very strong opinion. – Morris Singer Apr 23 '15 at 15:50
  • 1
    one nice thing about ES6 will be the elimination of having to do the 2 aforementioned workarounds to get the right *js* scope. `$scope.$watch( ()=> { return this.name' }, function(){} )` Fat arrow to the rescue – jusopi May 22 '15 at 17:26
  • 1
    you can also just do `() => this.name` – coblr Apr 01 '16 at 18:28
  • Can you make this work with `$scope.$watchCollection` and still get the `oldVal, newVal` params? – Kraken Aug 01 '16 at 21:20
23

You can use:

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

This is working JSFiddle with your example.

Artyom Pranovich
  • 6,814
  • 8
  • 41
  • 60
  • 25
    The problem with this approach is that the JS is now relying on the HTML, forcing the controller to be bound as the same name (in this case "test") everywhere in order for the $watch to work. Would be very easy to introduce subtle bugs. – jsdw Jul 11 '14 at 11:03
  • This turns out to work wonderfully if you're writing Angular 1 like Angular 2 where everything is a directive. Object.observe would be amazing right now though. – Langdon Jun 10 '15 at 01:00
13

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>
user4389
  • 131
  • 1
  • 2
  • Just want to know one thing, ie, `$scope` is a service, so If we add `$scope.self = this`, then in another controller if we do the same, What will happens there? – Vivek Kumar Nov 07 '16 at 17:40
12

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

$scope.$watch("$ctrl.name", (value) => {
    console.log(value)
});
Niels Steenbeek
  • 4,692
  • 2
  • 41
  • 50
  • Does not work for me when using $watchGroup, is this a known limit? can you share a link to this feature as I can't find anything about it. – user1852503 Oct 24 '16 at 13:47
  • @user1852503 See https://docs.angularjs.org/guide/component Comparison table Directive/Component definition and check 'controllerAs' record. – Niels Steenbeek Oct 24 '16 at 14:19
  • I understand now. Your answer is a bit misleading. the identifier $ctrl does not correlate with the controller as a feature (like $index does for example in a ng-repeat), it just happens to be the default name for the controller inside a component (and the question is not even about a component). – user1852503 Oct 24 '16 at 14:53
  • @user1852503 1) The $ctrl correlates the Controller (Controller as) 2) The question is about components, since it mentions: "
    ". 3) All anwers on this page are somehow the same as my answer. 4) Regarding the documentation $watchGroup should just work fine when using $ctrl.name since it's based on $watch.
    – Niels Steenbeek Oct 24 '16 at 15:18
2

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/

Alexandr
  • 5,460
  • 4
  • 40
  • 70
1

You can use $onChanges angular component lifecycle.

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

Pavel Durov
  • 1,287
  • 2
  • 13
  • 28
0

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);
    }
  }
}
Maciej Gurban
  • 5,615
  • 4
  • 40
  • 55
-1

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);
    });
}
wojjas
  • 1,046
  • 1
  • 9
  • 21
  • 11
    This works as long as you have "SomeController as vm" in your HTML. It's misleading, though: the "vm.name" in the watch expression has nothing to do with "var vm = this;". The only safe way to use $watch with "controller as" is to pass a function as the first argument, as Bogdan illustrates above. – Mark Visser Jan 15 '15 at 21:09
-1

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);
        }
    }); 
});
Ferie
  • 1,358
  • 21
  • 36
Binu Jasim
  • 297
  • 3
  • 9
  • The fiddle is not working and this will not observe an object property. – Rootical V. Jan 28 '16 at 09:59
  • @RooticalV. The fiddle is working. (Make sure that when you are running AngualrJS, you specify the load type as nowrap-head/nowrap-body – Binu Jasim Jan 29 '16 at 12:21
  • sorry but i still not managed to run it, such pity since your solution is very insterersting – happyZZR1400 Feb 09 '17 at 12:44
  • @happy Make sure you choose the library as Angular 1.4. (I am not sure whether 2.0 will work) and Load type as No wrap, and press Run. It should work. – Binu Jasim Feb 10 '17 at 13:09