zipper
zipper

Reputation: 397

ui-router creating nested states dynamically

I am creating a vertical navigation panel for my web page (a very basic task). Considering different user roles should have different nav-items against the authorization module on the server, it is desired to create the navigation contents dynamically rather than statically, by getting the data from the server.

I'm trying to use the UI Router to create nested states dynamically (which is really a natural idea called "divide-and-conquer") but got a problem (I described it in another thread but there are only code snippets and cannot demo). I constructed a simple demo here for the problem, in a more general way.

<!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.provider('runtimeStates', ['$stateProvider', function ($stateProvider) {
        this.$get = function () {
          return {
            newState: function (name, param) {
              $stateProvider.state(name, param);
              return name;
            }
          };
        };
      }]);
    
      app.config(['$urlRouterProvider', '$stateProvider', function (up, sp) {
        sp.state('state1', state1);
        up.otherwise('/state1');
      }]);
      
      let state1 = {
        url: '/state1',
        controller: ['runtimeStates', '$state', function ($rs, $st) {
          this.stateName = $st.current.name;
          this.createSubState = function(){
            $st.go($rs.newState($st.current.name + '.state2', state2), {
              message: 'message from ' + $st.current.name + ' to state2'
            });
          }
        }],
        controllerAs: '$ctrl',
        template: `<div ng-click="$ctrl.createSubState()" style="border-style: solid; cursor: pointer;">
          <p>{{$ctrl.stateName}} begin</p>
          <ui-view></ui-view>
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };
      
      let state2 = {
        params: {message : ''},
        controller: ['runtimeStates', '$transition$', '$state', function ($rs, $tr, $st) {
          this.parentMessage = $tr.params().message;
          this.stateName = $st.current.name;
          this.createSubState = function(){
            $st.go($rs.newState($st.current.name + '.state3', state3),{
              message: 'message from ' + $st.current.name + ' to state3'
            });
          };
        }],
        controllerAs: '$ctrl',
        template: `<div ng-click="$ctrl.createSubState()" style="border-style: solid; cursor: pointer;">
          <p>{{$ctrl.stateName}} begin</p>
          {{$ctrl.parentMessage}}
          <ui-view></ui-view>
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };
      
      let state3 = {
        params: {message : ''},
        controller: ['runtimeStates', '$transition$', '$state', function ($rs, $tr, $st) {
          this.parentMessage = $tr.params().message;
          this.stateName = $st.current.name;
        }],
        controllerAs: '$ctrl',
        template: `<div style="border-style: solid;">
          <p>{{$ctrl.stateName}} begin</p>
          {{$ctrl.parentMessage}}
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };
    </script>
  </head>

  <body>
    <ui-view></ui-view>
  </body>

</html>

When the view of state1 populated, I can click on it and generates the view of state2 with the contents expected; but when continuing to click on the view of state2, the generated contents are totally messed. Expected:

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

Generated:

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

I cannot explain why and don't know how to fix.

EDIT

Following the idea of @scipper (the first answer) I updated the demo to bellow:

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

  <head>
    <meta charset="utf-8" />
    <title>Demo28</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('demo28', ['ui.router']);
      
      app.provider('runtimeStates', ['$stateProvider', function ($stateProvider) {
        this.$get = function () {
          return {
            newState: function (name, param) {
              $stateProvider.state(name, param);
              return name;
            }
          };
        };
      }]);
    
      app.config(['$urlRouterProvider', '$stateProvider', function (up, sp) {
        sp.state('state1', state1);
        up.otherwise('/state1');
      }]);
      
      let state1 = {
        url: '/state1',
        controller: ['runtimeStates', '$state', function ($rs, $st) {
          this.stateName = $st.current.name;
          this.createSubState = function(){
            $st.go($rs.newState($st.current.name + '.state2', state2), {
              message: 'message from ' + $st.current.name + ' to state2'
            });
          }
        }],
        controllerAs: '$ctrl',
        template: `<div style="border-style: solid;">
          <p ng-click="$ctrl.createSubState()" style="cursor: pointer; color: blue;">{{$ctrl.stateName}} begin</p>
          <ui-view></ui-view>
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };
      
      let state2 = {
        params: {message : ''},
        controller: ['runtimeStates', '$transition$', '$state', function ($rs, $tr, $st) {
          this.parentMessage = $tr.params().message;
          this.stateName = $st.current.name;
          this.createSubState = function(){
            $st.go($rs.newState($st.current.name + '.state3', state3),{
              message: 'message from ' + $st.current.name + ' to state3'
            });
          };
        }],
        controllerAs: '$ctrl',
        template: `<div style="border-style: solid;">
          <p ng-click="$ctrl.createSubState()" style="cursor: pointer; color: blue;">{{$ctrl.stateName}} begin</p>
          {{$ctrl.parentMessage}}
          <ui-view></ui-view>
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };
      
      let state3 = {
        params: {message : ''},
        controller: ['runtimeStates', '$transition$', '$state', function ($rs, $tr, $st) {
          this.parentMessage = $tr.params().message;
          this.stateName = $st.current.name;
        }],
        controllerAs: '$ctrl',
        template: `<div style="border-style: solid;">
          <p>{{$ctrl.stateName}} begin</p>
          {{$ctrl.parentMessage}}
          <p>{{$ctrl.stateName}} end</p>
        </div>`
      };

    </script>
  </head>

  <body>
    <ui-view></ui-view>
  </body>

</html>

and the contents becomes:

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

It shows that the view of stat2 is effected by state3, which should be a problem of using UI Router. -- The problem is still unsolved.

Upvotes: 1

Views: 324

Answers (1)

scipper
scipper

Reputation: 3153

It messes up, because of the ng-click's. The first click works, the second click triggers the inner ng-click, then the outer one. That's why .state2 als always appended.

EDIT

Try adding $event.stopPropagation() to the ng-click's:

<div ng-click="$event.stopPropagation(); $ctrl.createSubState()">

EDIT 2

Second suggestion: You only have unnamed views. With my fix I found out, that the two inner views seem to be the same. My output after the second click is:

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

EDIT 3 - SOLUTION

The reason I mentioned that state2 controller gets invoked after the state change to state3 is the parameter message. Every change to a state parameter, causes the state to resolve by default. If you do not want that, specify the parameter as dynamic like:

params: {
  message: {
    value: '',
    dynamic: true
  }
}

Upvotes: 1

Related Questions