HP.
HP.

Reputation: 19896

$scope variable not update within $rootScope.$on

This show number 1 and a Change button. When I click that button, it supposed to trigger $state.go and within the controller, it should detect the event in $rootScope.$on and update the number to 2. However, for some reason I cannot get the number (data.id) to update even using $digest or $apply.

http://plnkr.co/edit/3qHnIm?p=preview

HTML

<!DOCTYPE html>
<html ng-app="myapp">

  <head>
        <script data-require="angular.js@*" data-semver="1.4.0-beta.5" src="https://code.angularjs.org/1.4.0-beta.5/angular.js"></script>
    <script data-require="ui-router@*" data-semver="0.2.13" src="//rawgit.com/angular-ui/ui-router/0.2.13/release/angular-ui-router.js"></script>

    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <title></title>
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width" />
  </head>

  <body class="container">
    <div ui-view=""></div>
    <div ng-controller="state2Ctrl">
      <a href="#" ng-click="change()">Change</a>
    </div>
    <script src="app.js"></script>
    <script src="controllers.js"></script>
  </body>

</html>

state1.html

<p>{{ data.id }}</p>

JS

var app = angular.module('myapp', ['ui.router']);

app.config(function($stateProvider, $urlRouterProvider) {

  $urlRouterProvider.otherwise('/state1/0');

  $stateProvider
    .state('state1', {
      url: '/state1/:id',
      controller: 'state1Ctrl',
      templateUrl: 'state1.html'
    });
});

app.controller('state2Ctrl', function ($scope, $rootScope, $state, $stateParams) {
  $scope.i = 1;

  $scope.change = function(){
    console.log('change state1');
    $state.go('state1', {id: $scope.i++});  
  };
});

app.controller('state1Ctrl', function ($scope, $rootScope, $state, $stateParams) {
  $scope.data = {id: 1};

  $rootScope.$on('$stateChangeSuccess', function(evt, toState, toParams, fromState, fromParams) {
    console.log($scope.data);

    $scope.$apply(function() {
      $scope.data = {id: 2};  
      console.log($scope.data);
    });
  });

});

Can someone find my mistake on why data.id did not get update in the view?

UPDATE 1

From Radim response below, I changed the code a bit to better reflect my REAL code: http://plnkr.co/edit/yyg0Hrl5IgzP4e44aRet?p=preview

Basic I didn't have the situation with parent state in my case. What I want to do is from the SAME state, trigger that state with new params and change some $scope variable objects. That's all.

JS

var i = 1;

app.controller('state1Ctrl', function($scope, $rootScope, $state, $stateParams) {
  $scope.change = function() {
    console.log('change state1: ' + i);
    $state.go('state1', {
      id: i++
    });
  };

  $scope.data = {
    id: 1
  };

  $rootScope.$on('$stateChangeSuccess', function(evt, toState, toParams, fromState, fromParams) {
    console.log('$stateChangeSuccess: ');
    console.log($scope.data);

    $scope.data = $scope.data || {};
    $scope.data.id = toParams.id;
    console.log('After assign:');
    console.log($scope.data);

  });

});

I found out after clicking Change 4 times, the number of $stateChangeSuccess event piling up as the screenshot below. I am not sure why either if you can help to explain and avoid this.

enter image description here

Upvotes: 3

Views: 4770

Answers (1)

Radim K&#246;hler
Radim K&#246;hler

Reputation: 123891

The reason is, that you are recreating data every time

// this will create new reference to data
// in current scope, while not effecting any other (parent)
$scope.data = {id: 2}; 

If you want to update existing reference we have to do it

// here we work with existing reference
// if it was created, it will be used, the id property will be updated
$scope.data = $scope.data  || {}; 
$scope.data.id = 2;

There is an updated version.

The state has now parent, which $scope we will be inheriting all the time, so we can use it as a shared holder for $scope.data

  $stateProvider
    .state('state1Parent', {
      template: '<div ui-view=""></div>',
      controller: 'state1ParentCtrl',
    });
  $stateProvider
    .state('state1', {
      parent: 'state1Parent',
      url: '/state1/:id',
      controller: 'state1Ctrl',
      templateUrl: 'state1.html'
    });

The controller updated def:

app.controller('state1Ctrl', function ($scope, $rootScope, $state, $stateParams) {
  // no $scope.data definition, used the inherited
  //$scope.data = {id: 3}; 

  $rootScope.$on('$stateChangeSuccess', function(evt, toState, toParams, fromState, fromParams) {
    console.log($scope.data);

    // because this state change listener still have access to $scope
    // we can update the value of data.id
    // because it is in fact parent reference
    // se even next state will have access to it


    //$scope.$apply(function() {
      $scope.data = $scope.data || {};  
      $scope.data.id = toParams.id;  
      console.log($scope.data);
    //});
  });

});

And a parent controller, with a reference data definition

app.controller('state1ParentCtrl', function ($scope, $rootScope, $state, $stateParams) {
  $scope.data = { id : 1 };
})

Check it here.

I am not saying that this is way we should go. But trying to show what is the issue, and how it could be solved

EXTEND: could we do this without a parent state?

From extended question:

Basic I didn't have the situation with parent state in my case. What I want to do is from the SAME state, trigger that state with new params and change some $scope variable objects. That's all.

So, what are the issues here.

1) The controller lifecycle is the shortest, in angular world. It will live only during the state. Once we leave it, navigate to other - it will disappear (if no memory leak hooks are in place)

2) The controller's scope will be destroyed, once the controller is destroyed...

3) We have to think about states as about isolated islands. If we do have access to state $scope - even in the internal declaration $rootScope.$on('$stateChangeSuccess' - we do have access the $scope of current state... of current controller. In fact, we create a candidate for memory leak

app.controller('state1Ctrl', ...
  ...
  $rootScope.$on('$stateChangeSuccess', ...
     // HERE - $scope belongs to Controller, and its life cycle is very short
     $scope.data =

4) if we want to change some value, inisde of "SAME state" - after transition we have to move it OUT. It cannot be in current state, because current state/view/controller (and it's scope) will not be the same.

5) The way how to keep data is - Service (singleton) or a Model inside of a parent (which will be inherited through the view/scope inheritance)

6) The $rootScope.$on Should be declared in some .run() method - outside of the controller. But in this case, it does not seem to be adding any value. Because if we will place the shared data into some service... we can do it anyhwere, e.g. in other controller as well

Upvotes: 2

Related Questions