Angularjs: 'controller as syntax' and $watch
Asked Answered
G

10

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>  
Grog answered 6/6, 2014 at 9:31 Comment(1)
what about this.$watch()? It's valid: this.$watch('name', ...)Festoon
V
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

Varipapa answered 6/6, 2014 at 9:48 Comment(9)
Can we use it without $scope to avoid mix of this and $scope?Grog
No as I know, but it's perfectly fine. $scope for you is a kind of service which supplies these kind of methods.Varipapa
Can you clarify whether name in return this.name; refers to the name of the controller or the property "name" here?Demonstrative
@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.Varipapa
I think I just understood how this works. When the bound function is called, it simply evaluates to the watched value, right?Demonstrative
I wish it was simplerCussed
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?Incult
this for me defeats the point of Controller as vm if for watches I still have to use scope.Shortcut
What about performance? Isn't better to watch the proper object? jsperf.com/angular-watch-function-result-vs-stringLeighleigha
F
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)
   });
});
Fallacy answered 18/8, 2014 at 7:13 Comment(5)
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.Janeenjanek
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.Janeenjanek
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 rescueEthnology
you can also just do () => this.nameHonesty
Can you make this work with $scope.$watchCollection and still get the oldVal, newVal params?Hangman
S
23

You can use:

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

This is working JSFiddle with your example.

Sudiesudnor answered 6/6, 2014 at 9:43 Comment(2)
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.Bracketing
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.Meilen
S
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>
Sergent answered 25/11, 2014 at 13:49 Comment(1)
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?Hyperesthesia
B
12

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

$scope.$watch("$ctrl.name", (value) => {
    console.log(value)
});
Belle answered 20/4, 2016 at 12:43 Comment(4)
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.Hasa
@Hasa See docs.angularjs.org/guide/component Comparison table Directive/Component definition and check 'controllerAs' record.Belle
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).Hasa
@Hasa 1) The $ctrl correlates the Controller (Controller as) 2) The question is about components, since it mentions: "<div ng-controller="TestCtrl as test">". 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.Belle
F
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/

Funch answered 1/7, 2016 at 21:8 Comment(0)
C
1

You can use $onChanges angular component lifecycle.

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

Cowgill answered 29/11, 2017 at 11:43 Comment(0)
P
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);
    }
  }
}
Psycho answered 5/2, 2016 at 10:41 Comment(0)
K
-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);
    });
}
Kehr answered 15/1, 2015 at 14:31 Comment(1)
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.Lesalesak
G
-2

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);
        }
    }); 
});
Gladdie answered 25/10, 2015 at 6:26 Comment(4)
The fiddle is not working and this will not observe an object property.Heman
@RooticalV. The fiddle is working. (Make sure that when you are running AngualrJS, you specify the load type as nowrap-head/nowrap-bodyGladdie
sorry but i still not managed to run it, such pity since your solution is very instererstingNanon
@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.Gladdie

© 2022 - 2024 — McMap. All rights reserved.