1

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.

zipper
  • 377
  • 1
  • 5
  • 18
  • I do not see my suggested fix. – scipper Nov 26 '17 at 12:43
  • If you mean the suggestion to add `$event.stopPropagation();` to the `ng-click`, then I tried it does not help so I eliminated it. Also, I moved the `ui-view` out of the `ng-click` effecting area by putting the `ng-click` to the `

    ` element; so the click on state2 should not trigger the `ng-click` for state1 now. --- that's why I got the same result of your EDIT2 in your bellow answer; which is still not the desired because state3 output overwrote state2's view.

    – zipper Nov 26 '17 at 13:28
  • I figured partly out, why state2's values are overridden. When you click on state2 to get to state3, the controller of state2 is invoked after the state change, therefore the value of this.stateName is set to the current value of $st.current.name, which is then state1.state2.state3. I try to find out, why the controller is invoked again. – scipper Nov 26 '17 at 14:42
  • It seems this s a problem caused by UI Router. I created query on github: https://github.com/angular-ui/ui-router/issues/3567 – zipper Nov 27 '17 at 07:21
  • 1
    Please read my EDIT 3. This is no bug. The router behaves like expected. – scipper Nov 27 '17 at 07:27
  • Can you help to modify my code and make it work? I tried to change the message from a plan string to a object with value and dynamic=true, and passed this object in the `$st.go()`, but it did not change the result; maybe I make some mistake. if you want to directly change on it instead of past again, I created [the plunker here](http://plnkr.co/edit/cCyjhklE0zsYUywnlT7Q?p=preview). – zipper Nov 27 '17 at 10:00
  • 1
    http://plnkr.co/edit/IBe9EmiKPN1cklrh2c3u?p=preview I had to forge your plunker, because of missing permissions. – scipper Nov 27 '17 at 10:21
  • I got it. so in the `$st.go` I should pass the `message` like a normal string not a object with `value` and `dynamic` properties. So this is finally solved. Thank you very much for all the help to solve this problem. – zipper Nov 27 '17 at 13:29
  • Nice to hear that you fixed it. – scipper Nov 27 '17 at 13:30

1 Answers1

1

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
  }
}
scipper
  • 2,944
  • 3
  • 22
  • 45
  • I tried adding `$event.stopPropagation();` to both ng-click's, but there is no change to the result. – zipper Nov 26 '17 at 09:03
  • following your idea that the `ng-click` was triggered unexpectedly, I moved it into the

    tag, so the `ui-view` is out of the effecting scope of the `ng-click`. Then the contents of state3 comes to be the expected (`state1.state2.state3 begin\nmessage from state1.state2 to state3\nstate1.state2.state3 end`); but the contents of the state2's view changed to the same of the above state3's as well! So I think this is really something related to the UI Router. I voted you up, but the problem is still not solved.

    – zipper Nov 26 '17 at 11:01