1

I'm trying to manage my admin page by bellow states with multi views: admin, admin.header, admin.leftPanel, admin.main, admin.tail. In the header, leftPanel, main and tail, I use $state.go to their sub states respectively to render their contents. I write bellow simple code to demo this problem.

Demo states model:

state1:
  state2view
    controller: $state.go(state1.state2) <---superseded
  state3view
    controller: $state.go(state1.state3)

Code (plunker):

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

  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.3/angular-ui-router.js"></script>
    <script>
      let app = angular.module('demo', ['ui.router']);
    
      app.config(['$urlRouterProvider', '$stateProvider', function ($up, $sp) {
        $sp.state('state1', state1);
        $sp.state('state1.state2', new SubState('state2view'));
        $sp.state('state1.state3', new SubState('state3view'));
        $up.otherwise('/');
      }]);
      
      let state1 = {
        url: '/',
        views: {
          "state1view1": {
            controller: ['$transition$', '$state', function ($tr, $st) {
              this.stateName = $st.current.name;
              $st.go('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
            }],
            controllerAs: '$ctrl',
            template: `<div>
              {{$ctrl.stateName}} begin<br>
              <ui-view name="state2view"></ui-view>
              {{$ctrl.stateName}} end
            </div>`
          },
          
          "state1view2": {
            controller: ['$transition$', '$state', function ($tr, $st) {
              this.stateName = $st.current.name;
              $st.go('state1.state3', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state3'});
            }],
            controllerAs: '$ctrl',
            template: `<div>
              {{$ctrl.stateName}} begin<br>
              <ui-view name="state3view"></ui-view>
              {{$ctrl.stateName}} end
            </div>`
          }
        }
      };
      
      function SubState(view1Name) {
        this.params = {message: ''};
        this.views = {};
        this.views[view1Name] = {
          controller: ['$transition$', '$state', function ($tr, $st) {
            this.parentMessage = $tr.params().message;
            this.stateName = $st.current.name;
          }],
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}} begin<br>
            {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}} end
          </div>`
        };
      }
      
      app.run(function($transitions) {
        $transitions.onStart({}, function($tr) {
            console.log("trans begin: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
        $transitions.onSuccess({}, function($tr) {
            console.log("trans done: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
      });

    </script>
    <style>
    div{border-style: solid;}
    </style>
  </head>

  <body>
    <ui-view name="state1view1"></ui-view>
    <br>
    <ui-view name="state1view2"></ui-view>
  </body>

</html>

Expected result:

state1 begin
state1.state2 begin
message from state1 to state1.state2
state1.state2 end
state1.state3 begin
message from state1 to state1.state3
state1.state3 end
state1 end

Actual result:

state1 begin
state1 end
state1 begin
state1.state3 begin
message from state1 to state1.state3
state1.state3 end
state1 end

Console output: console output

zipper
  • 377
  • 1
  • 5
  • 18
  • First off, great to a complete, verifiable, runnable code in a question. It seems like the issue is that you have overlapping asynchronous state transition events that are conflicting. `ui-router` has changed a lot since I worked with it (heavily about 2 years ago), but I'm unclear on what you want to achieve. – Aluan Haddad Dec 01 '17 at 07:13
  • @Aluan I need the transition execute controllers and render the views synchronously when traversing a state tree. I expect the behavior is (dynamically if possible) configurable specifically for each state, but cannot find a config on it. – zipper Dec 01 '17 at 08:40
  • ui-router is asynchronous https://ui-router.github.io/ng1/docs/latest/classes/transition.transition-1.html – Aluan Haddad Dec 01 '17 at 08:41
  • @Aluan What is your experience to traverse a state tree to build the whole web page which is composes of many components with hierarchies? Note some of the components depends on the data from server side. What I can think of is to generate states dynamically ([done](https://stackoverflow.com/questions/25866387/angular-ui-router-programmatically-add-states)) and use $state.go to drive to traverse through the whole tree (some parts are done recursively). But if transition only kicks off callbacks asynchronously then I get conflict. – zipper Dec 01 '17 at 08:51
  • For server side data, I would use `resolve`(s) which return promises and can have injected deps. – Aluan Haddad Dec 01 '17 at 08:54
  • $http itself returns a promise and works directly inside a controller. – zipper Dec 01 '17 at 09:00
  • You can also do it while transitioning by using a `resolve`. – Aluan Haddad Dec 01 '17 at 09:07
  • sorry I think the point of server side data is misleading. in my demo code no server side data involved, though in my target project I do need it. I think the demo code is self-explainable; just cannot figure out what's the problem with it. – zipper Dec 01 '17 at 09:14
  • I feel I finally find the design is wrong. I need a single logic outside all of the controllers of the sub states to traverse the TREE MODEL of the state tree, and enter the node states according to the output (node sequence) . The single logic may reside in the root state's control. Trying to make code to verify this... – zipper Dec 01 '17 at 16:28

2 Answers2

0

You should use stateHelper module created by@marklagendijk.

Read this article in the link below about nested states for more options if you do not wish to use the aforementioned module

Nested States

plunkr

angular.module('app').config([
'$urlRouterProvider',
 'stateHelperProvider',
 function($urlRouterProvider, stateHelperProvider) {
stateHelperProvider.state({
  name: 'state1',
  template: '<ui-view/>',
  abstract: true,
  resolve: {
    // haven't got to this point yet :-/
  },
  children: [{
      name: 'state2',
      controller:['$state','$q', function ( $st,$q) {
          this.stateName = $st.current.name;
         // $st.transitionTo('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
        }], controllerAs: '$ctrl',
      url : '/',
      template: `<br>{{$ctrl.stateName}} begin
      <br>message from {{$ctrl.stateName}} to {{$ctrl.stateName}}.state3
      <br>{{$ctrl.stateName}} end`
  },{
      name: 'state3',
      url : '/',
      controller:['$state','$q', function ( $st,$q) {
          this.stateName = $st.current.name;

         // $st.transitionTo('state1.state2', {message: 'message from ' + $st.current.name + ' to ' + $st.current.name + '.state2'});
        }], controllerAs: '$ctrl',
      template: `<br>{{$ctrl.stateName}} begin
      <br>message from {{$ctrl.stateName}} to {{$ctrl.stateName}}.state3
      <br>{{$ctrl.stateName}} end`
  }]
});}]);
Vibhu
  • 536
  • 1
  • 3
  • 11
  • Thanks for help. I did not realize there is any execution difference between different notations of sub state definition. But I feel the sample code in your plunker is different from my situation. In yours no state tree auto full-traverse involved; and the problem of interference of life cycles between sibling states never exists. – zipper Dec 01 '17 at 14:59
0

It turns out that I just hit the wall.

The idea of enclosing a page's contents into UI Router states and layout by visiting these states one by one programmablly is totally wrong. Particularly, you can not show two sibling states' views in one time.

UI Router is designed for routing by mouse clicks. Even though the pieced documents strongly hint we can relay our full page on a state tree to layout all contents, but it is not always the case. As long as the app logic transit to another state which is not the decedent of the from-state, the from-state exits before entering the to-state, and its run-time generated view(s) of the exited state is fully removed.

Bellow code is originally trying to prove my concept bellow (the improvement of the design in the original question, since I realized that there is not break-point/resume design in state) and to solve my problem, but it turns out to be a example of revealing the opposite -- the impossibility under the wrong idea.

The concept

  1. Define the states and their hierarchy and templates to framework a page layout;
  2. Make the state tree;
  3. Make the traverse path;
  4. Traverse the path by $state.go to each state to unfold and render the layout.

The code (plnkr)

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

  <head>
    <meta charset="utf-8" />
    <title>Demo</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.3/angular-ui-router.js"></script>

    <script>
      let app = angular.module('demo', ['ui.router']);

      app.config(['$urlRouterProvider', function ($up) {
        $up.otherwise('/');
      }]);

      app.provider('runtimeStates', ['$stateProvider', function ($stateProvider) {
        this.$get = function () {
          return {
            newState: function (name, param) {
              $stateProvider.state(name, param);
              return name;
            }
          };
        };
      }]);

      app.factory('sharingSpace', function () {
        return {
          stateTree: [],
          traversePath: []
        };
      });

      app.run(['sharingSpace', '$transitions', '$state' ,function(ss, $trs, $st) {
        $trs.onStart({}, function($tr) {
            console.log("trans begin: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
        $trs.onSuccess({}, function($tr) {
            nextHop(ss, $st);
            console.log("trans succeeded: " + $tr.from().name + " -> " + $tr.to().name);
          }
        );
      }]);

      app.run(['runtimeStates', 'sharingSpace', function(rt, ss) {
        makeStateTree(rt, ss);
      }]);

      function StateParam(stateName) {
        let me = this;
        me.name = stateName;
        me.params = {
          message : {
            value: '',
            dynamic: true
          }
        };
        me.views = {};
        //me.sticky = true; <---does not prevent the view port from removed when exit.
        me.onExit = ['$state', function($state){
          let goodByeMsg = 'Goodbye ' + $state.current.name;
          console.log(goodByeMsg);
          alert(goodByeMsg);
        }];
        me.addView = function(viewParam) {
          me.views[viewParam.name] = {
            controller: viewParam.controller,
            controllerAs: viewParam.controllerAs,
            template: viewParam.template,
          };
          return me;
        };
        return me;
      }

      function makeStateTree(rt, ss) {

        let state1view1param = {
          name: 'state1view1',
          controller: ['sharingSpace', '$transition$', '$state', function (ss, $tr, $st) {
            this.stateName = $st.current.name;
            this.viewName = 'state1view1';
            makeTraversePath(ss);
            //do something ...
          }],
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            let's start ...<br>
            <ui-view name="state2view"></ui-view>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let trivialCtrl = function(viewName) {
          return ['sharingSpace', '$transition$', '$state', function (ss, $tr, $st) {
            this.parentMessage = $tr.params().message;
            this.stateName = $st.current.name;
            this.viewName = viewName;
            //do something ...
            console.log('this.stateName = ' + this.stateName);
          }];
        };

        let state1view2param = {
          name: 'state1view2',
          controller: trivialCtrl('state1view2'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            <ui-view name="state3view"></ui-view>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let state2viewParam = {
          name: 'state2view',
          controller: trivialCtrl('state2view'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            parentMessage: {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }

        let state3viewParam = {
          name: 'state3view',
          controller: trivialCtrl('state3view'),
          controllerAs: '$ctrl',
          template: `<div>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} begin<br>
            parentMessage: {{$ctrl.parentMessage}}<br>
            {{$ctrl.stateName}}.{{$ctrl.viewName}} end
          </div>`
        }
        let mainStateParam = new StateParam('state1');
        mainStateParam.url = "/";
        mainStateParam.addView(state1view1param).addView(state1view2param);
        let subStateParam1 = (new StateParam('state1.state2')).addView(state2viewParam);
        let subStateParam2 = (new StateParam('state1.state3')).addView(state3viewParam);
        
        rt.newState(mainStateParam.name, mainStateParam);
        ss.stateTree.push(rt.newState(subStateParam1.name, subStateParam1));
        ss.stateTree.push(rt.newState(subStateParam2.name, subStateParam2));

      }

      function makeTraversePath(ss) {
        for(let i = 0; i<ss.stateTree.length; i++){
          ss.traversePath.push(ss.stateTree[i]); //trivial example
        };
      }

      function nextHop(ss, $st){
        if(ss.traversePath[0] != undefined) {
          let nextHop = ss.traversePath[0];
          ss.traversePath.splice(0, 1);
          console.log('nextHop = ' + nextHop);
          $st.go(nextHop, {message: 'message from ' + $st.current.name});
        }
      }

    </script>

    <style>
      div{border-style: solid;}
    </style>
  </head>

  <body>
    <ui-view name="state1view1"></ui-view>
    <br>
    <ui-view name="state1view2"></ui-view>
  </body>

</html>

The result (Firefox 57.0.1)

When entering the page: enter image description here

After click and close the alert: enter image description here

Above process revealed that the state1.state2 was executed and layed out (but not evaluated/rendered by angular yet), as we can see in the first picture. At that point the exiting was not happened yet because the onExit alert pop blocked the process. After the alert pop closed the state exited and the view was complete removed.

There is a sticky-state developed for the in-page-tab specific purpose, but as I tried it does not work here. It remembers the last visited stick-states but the views of the exited states are always removed.

I'm now trying to use the UI Router as a routing notation facility only. But I have to be very conscious NOT to run into the idea that UI Router can be used as a general tool to layout a page like a extension of angular component. But this can be difficult: I cannot think of a right pattern to use UI Router at the moment. In case of multi views, if any two sibling views both has their own sub states, I must be very careful because visiting one exits another -- they are exclusive. This makes me think it is not worth its complexity.

While removing views on exit is desired in most case during navigation, I would suggest the UI Router to change and give a chance to keep the views to provide more flexibility. It can be more complicated than the first thought, but it should be possible.

It is also desirable to cache all the "last seen" parameters for each states (not just for sticky states) so we can return to them easily. You may argue the use case, but we cannot imagine how people will use a tool and should not limit the possibilities.

It is also desirable to provide facility for full life cycle hooks per state base (now only have onEnter and onExit).

zipper
  • 377
  • 1
  • 5
  • 18