4

I have a problem with a variable that is not updated to a $scope function when a state change event occur even though I see that the variable is updated in the event listener.

Here is the code:

angular.module('testapp')
.controller('AnotherCtrl',
['$scope', '$rootScope', '$state',
    function ($scope, $rootScope, $state) {
        'use strict';
        console.log("Init prevState.");
        var prevState = 'null_state';
        $scope.state = prevState;


        $rootScope.$on('$stateChangeError',
            function (event, toState, toParams, fromState, fromParams, error) {
                console.log("Error");
                if (toState.name === 'anotherState') {

                    console.log("Old State:" + prevState);
                    prevState = fromState.name;
                    console.log("New State:" + prevState);


                }
            })

        $rootScope.$on('$stateChangeSuccess',
            function (event, toState, toParams, fromState, fromParams) {
                console.log("Success");
                if (toState.name === 'anotherState') {

                    console.log("Old State:" + prevState);
                    prevState = fromState.name;
                    console.log("New State:" + prevState);

                }
            })


        $scope.goBack = function () {
            //$scope.state = 'anotherState';
            console.log("goBack:" + prevState);
            $state.transitionTo(prevState, {arg: 'Robert'});
        };
    }]);

Here is the HTML template:

<div>
<h1>Another view</h1>
<br>
<br>
State:  <span ng-model="state">{{state}}</span>
<br>
<br>
<button ng-click="goBack()">Back</button>
</div>

Here is the console output:

console output

So when I press a button on the button which invokes the function goBack(), the prevState variable is still 'null_state'.

Can anyone explain what the problem is?

UPDATE after review of the answers I have reviewed and tested all the suggested solutions. The core problem, AFAICS, had nothing to do with the string type being immutable. As I see It the best answer comes from @Khanh TO. The event listeners were not initialized when the controller was created in the first place. This was fixed with a bootstrapping (angular.module.run). Also the controller was re-initialized every time the state/view was loaded causing the variable prevState to be re-initialized, thus the listener functions and the goBack() function used different outer prevState variables. Storing the prevState on the rootScope solved this problem.

Robert Höglund
  • 954
  • 9
  • 12

4 Answers4

1

The root cause is whenever you change state, angular re-initializes the controller associated with the current state, causing your previously stored state be cleared and re-initialized.

Therefore, the solution is to store the previous state in a shared service. In this case, we could just use $rootScope.

$rootScope.$on('$stateChangeError',
            function (event, toState, toParams, fromState, fromParams, error) {
                console.log("Error");
                if (toState.name === 'anotherState') {
                    $rootScope.prevState = fromState.name;
                }
 });

 $rootScope.$on('$stateChangeSuccess',
            function (event, toState, toParams, fromState, fromParams) {
                console.log("Success");
                if (toState.name === 'anotherState') {
                    $rootScope.prevState = fromState.name;
                }
  });

  $scope.goBack = function () {
    $state.transitionTo($rootScope.prevState, {arg: 'Robert'});
    //or $state.transitionTo($scope.prevState, {arg: 'Robert'}); //due to scope inheritance
  };

But your code has a serious problem of registering more and more event handlers to $rootScope whenever the AnotherCtrl is re-initialized. The second problem with your code is that the $stateChangeSuccess event doesn't trigger when loading a page for the first time https://github.com/angular-ui/ui-router/issues/299

You should only register event handlers once in .run block:

angular.module('testapp')
       .run(["$rootScope",function ($rootScope){
                $rootScope.$on('$stateChangeError',
                   function (event, toState, toParams, fromState, fromParams, error) {
                      console.log("Error");
                      if (toState.name === 'anotherState') {
                        $rootScope.prevState = fromState.name;
                      }
                   });

               $rootScope.$on('$stateChangeSuccess',
                  function (event, toState, toParams, fromState, fromParams) {
                    console.log("Success");
                    if (toState.name === 'anotherState') {
                        $rootScope.prevState = fromState.name;
                    }
                });  
            }]);

And only this code should remain in your controller:

$scope.goBack = function () {
    $state.transitionTo($rootScope.prevState, {arg: 'Robert'});
    //or $state.transitionTo($scope.prevState, {arg: 'Robert'}); //due to scope inheritance
 };
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • I thought so too, but if the controller is re-initialized, how come the "Init prevState." console print doesn't show on his console screenshot? It's the first thing the controller does. – haimlit Jun 28 '14 at 11:11
  • @haimlit: there is `"Init prevState."` in the console. Please take a closer look. – Khanh TO Jun 28 '14 at 12:00
  • But if what you're saying is correct, there should've been two - one when the controller was initialized, and another when it was re-initialized due to state change. – haimlit Jun 28 '14 at 12:29
  • @haimlit: looking at the code, we can tell that he is coming from `"modeltest.transfer"` and there should be only **one** `"Init prevState."`. I'm using angular 1.2.17 and the flow is this: `$stateChangeSuccess` is fired first before re-initializing the controller (which logs `"Init prevState."` in the console). I also feel that there is something wrong with the console as `"Init prevState."` should be in between `"New State"` and `"go back"`. Not sure that in other versions, controller initialization is run before `$stateChangeSuccess` – Khanh TO Jun 28 '14 at 13:33
  • @haimlit: I'm not sure how he is able to reproduce this case. It may have something to do with the problem I pointed above. The code does not work for the first time and event handlers are registered more and more whenever we come to this state. – Khanh TO Jun 28 '14 at 13:37
0

The problem you are facing is due to how JavaScript closures work. When you access prevState in the scope of the functions goBack() and the function associated to stateChangeSuccess, you are keeping a handle on the parent's prevState. But strings in JavaScript are immutable. So when in goBack you assign a value to prevState, a new string is created and prevState inside of goBack() points to that. But the prevState associated with stateChangeSuccess is still pointing to "null_state".

What you could do is wrap prevState inside an object as such:

var prevData = {prevState: 'null_state'};

and use prevData.prevState to modify prevState. The reason this will work is because all the closures hold the reference to the same object (since now you are using an object rather than a string) and so the change to the string will be propagated to the different functions.

For more information read the following carefully:

How do JavaScript closures work?

Pass a string by reference in Javascript

Community
  • 1
  • 1
Sid
  • 7,511
  • 2
  • 28
  • 41
0

Each time that you change the route, the current controller is destroyed (the $destroy event is broadcasted) and a new controller is created.

You can see when a controller is destroyed putting inside the controller:

$scope.$on('$destroy',function () {
   console.log("The controller is destroyed.");
});

The way for maintain the data in the application is through Services (like $state) or through $rootScope.

This means that when you create the controller you can get the data from the Service. Create a getter function for this.

On the other hand, if you want bind the data with the controller, you must update $scope.state, not prevState. The template only updates values from $scope.

Result:

angular.module('testapp')
.controller('AnotherCtrl',
['$scope', '$rootScope', '$state',
    function ($scope, $rootScope, $state) {
        'use strict';
        console.log("Init prevState.");
        $scope.state = $state.getState(); //create the getter


        $rootScope.$on('$stateChangeError',
            function (event, toState, toParams, fromState, fromParams, error) {
                console.log("Error");
                if (toState.name === 'anotherState') {

                    console.log("Old State:" + $scope.state);
                    $scope.state = fromState.name;
                    console.log("New State:" + $scope.state);


                }
            })

        $rootScope.$on('$stateChangeSuccess',
            function (event, toState, toParams, fromState, fromParams) {
                console.log("Success");
                if (toState.name === 'anotherState') {

                    console.log("Old State:" + $scope.state);
                    $scope.state = fromState.name;
                    console.log("New State:" + $scope.state);

                }
            })


        $scope.goBack = function () {
            //$scope.state = 'anotherState';
            console.log("goBack:" + $scope.state);
            $state.transitionTo($scope.state, {arg: 'Robert'});
        };
    }]);
cespon
  • 5,630
  • 7
  • 33
  • 47
0

May be you want to change:

var prevState = 'null_state';

to

$scope.prevState = 'null_state';

and fix other stuff accordingly.

Raouf Athar
  • 1,803
  • 2
  • 16
  • 30