Search code examples
angularjsangular-ui-routerangular-ui-router-extras

What is the minimum needed to preserve angularjs ui-router states with sticky states from ui-router-extras?


I have a page which includes HTML form elements distributed on several tabs. I want the user to be able to switch tabs without loosing the data that he entered on the form elements (and I also want to save needless data reloads). And I still want to be able to pass someone a link to a specific tab.

It sounds like ui-extras sticky states should do exactly what I need. Except that I have been remarkably unsuccessful to get it to work.

I studied the example source code and when I fire up the example I e.g. set a breakpoint with firebug in the constructor of the inventory controller found in Line 57 in controllers.js, I see that the constructor is fired only once. My controller constructors however are fired over and over again and my application behaves pretty much like if sticky states are not existent although if I enable sticky state debugging it tells me that it is doing something (deactivating and reactivating states).

I found someone who stated that sticky states only work with named views in a comment in this answer, so I tried to give my view a name but that did not make any difference.

I tried inserting an explicit "root state" before my tabs.

I tried to insert the controller either by ng-controller or by controller definition in the states.

The example on github is a nice show off but is way more than minimal and makes it difficult to see what is actually needed and what not.

What would be a minimal example what is needed to get started with sticky states? (Bonus: What is wrong with my code?).

For reference here is a plunkr with my failed attempt (see history to see a selection of previous attempt).

Here is my current failing source code:

var log = '';

function mkController(msg) {
  return function($scope) {
    // This is the constructor of a controller
    // I'd expect this constructor to the first time a state is loaded.
    // When switching to a sister state and back it should not be called again.

    if (!$scope.random) {
      // I expect the $scope object to be retained when changing states for and
      // back. So even if my assumption that the controller will be persistent
      // would be wrong this is to check whether the $scope survives.
      // If the scope survives the random number will be initialized only once
      // and then it won't change anymore:

      $scope.random = Math.round(Math.random()*10000);
    }
    // This log will tell us how often the controller constructor has been called
    // (Should be only once, I think)
    log += 'creating: ' + msg + '\n';
    this.message = log;
  }
}
angular.module('plunker', ['ui.router', 'ct.ui.router.extras.sticky', 'ct.ui.router.extras.dsr'])
.controller('ControllerA', mkController('ControllerA'))
.controller('ControllerB', mkController('ControllerB'))
.run(function($templateCache) {
  $templateCache.put('root.html', '<div ui-view="myview"></div>');
  $templateCache.put('templateA.html', '<div ng-controller="ControllerA as controller"><pre>Random: {{random}}, Message (templateA): {{controller.message}}</pre></div>');
  $templateCache.put('templateB.html', '<div ng-controller="ControllerB as controller"><pre>Random: {{random}}, Message (templateB): {{controller.message}}</pre></div>');
})
.config(function($stateProvider) {
  $stateProvider
  .state('root', {
    url: '/',
    templateUrl: 'root.html'
  }).state('root.stateA', {
    url: '/stateA',
    views: {
      myview: {
        templateUrl: 'templateA.html',
      }
    },
    sticky: true,
    deepStateRedirect: true
  }).state('root.stateB', {
    url: '/stateB',
    views: {
      myview: {
        templateUrl: 'templateB.html',
      }
    },
    sticky: true,
    deepStateRedirect: true
  });
})
.config(function($stickyStateProvider) {
  $stickyStateProvider.enableDebug(true);
});

Solution

  • I figured it out:

    • First of all I did not realize how the named views were actually meant to be used: With a separate view per tab.
    • Second I did not realize that I need to hide&show the views myself.

    Both is mentioned in the demo page, but I have to admit that I did not really read the text of the demo page closely as I thought this were just information on this particular demo.

    Anyhow: Here is an example which I believe is the minimum needed to get sticky states going:

    HTML

    <!DOCTYPE html>
    <html ng-app="plunker">
    
      <head>
        <meta charset="utf-8" />
        <title>AngularJS Plunker</title>
        <script>document.write('<base href="' + document.location + '" />');</script>
        <link href="style.css" rel="stylesheet" />
        <script data-semver="1.3.12" src="https://code.angularjs.org/1.3.12/angular.js" data-require="[email protected]"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.13/angular-ui-router.js"></script>
        <script src="ui-router-extras.js"></script>
        <script src="app.js"></script>
      </head>
    
      <body>
        <ul class="nav nav-tabs nav-tabs-head" role="tablist">
            <li ui-sref-active="active"><a ui-sref="root.stateA" role="tab">StateA</a></li>
            <li ui-sref-active="active"><a ui-sref="root.stateB" role="tab">StateB</a></li>
        </ul>
        <div ui-view="" />
      </body>
    
    </html>
    

    JavaScript

    var log = '';
    
    function mkController(msg) {
      return function($scope) {
        // This is the constructor of a controller
        // I'd expect this constructor to the first time a state is loaded.
        // When switching to a sister state and back it should not be called again.
    
        if (!$scope.random) {
          // I expect the $scope object to be retained when changing states for and
          // back. So even if my assumption that the controller will be persistent
          // would be wrong this is to check whether the $scope survives.
          // If the scope survives the random number will be initialized only once
          // and then it won't change anymore:
    
          $scope.random = Math.round(Math.random()*10000);
        }
        // This log will tell us how often the controller constructor has been called
        // (Should be only once, I think)
        log += 'creating: ' + msg + '\n';
        this.message = 'Current msg: ' + msg + '\n\n' + log;
      }
    }
    angular.module('plunker', ['ui.router', 'ct.ui.router.extras.sticky'])
    .controller('ControllerA', mkController('ControllerA'))
    .controller('ControllerB', mkController('ControllerB'))
    .config(function($stateProvider) {
      $stateProvider
      .state('root', {
        url: '/',
        template: '<div ui-view="a" ng-show="$state.includes(\'root.stateA\')"></div><div ui-view="b"  ng-show="$state.includes(\'root.stateB\')"></div>'
      }).state('root.stateA', {
        url: '/stateA',
        views: {
          'a@root': {
            template: '<div ng-controller="ControllerA as controller"><h1>A</h1><pre>Random: {{random}}, Message (templateA): {{controller.message}}</pre></div>',
          }
        },
        sticky: true
      }).state('root.stateB', {
        url: '/stateB',
        views: {
          'b@root': {
            template: '<div ng-controller="ControllerB as controller"><h1>B</h1><pre>Random: {{random}}, Message (templateB): {{controller.message}}</pre></div>',
          }
        },
        sticky: true
      });
    })
    .config(function($stickyStateProvider) {
      $stickyStateProvider.enableDebug(true);
    })
    .run(function ($rootScope, $state) {
      $rootScope.$state = $state;
    });
    

    Demo on plunkr

    Link