UI Router transition superseded when traversing mu

2019-07-27 12:54发布

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

2条回答
对你真心纯属浪费
2楼-- · 2019-07-27 13:03

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).

查看更多
Melony?
3楼-- · 2019-07-27 13:23

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`
  }]
});}]);
查看更多
登录 后发表回答