1

Well, I have this project, and ui-router is giving me hard times. I made a quick Plunker demo: http://plnkr.co/edit/imEErAtOdEfaMMjMXQfD?p=preview

So basically I have a main view, index.html, into which other top-level views get injected, like this operations.index.html. The pain in my brain starts when there are multiple named views in a top-level view, operations.detail.html and operations.list.html are injected into operations.index.html, which is in turn injected into index.html.

Basically, what I'm trying to achieve is the following behaviour:

  1. When a user clicks Operations item in the navbar, a page with empty (new) operation is shown. The URL is /operations.
  2. When they select an item in a list, the fields are updated with some data (the data is requested via a service, but for simplicity let's assume it's right there in the controller). The URL is /operations/:id.
  3. If they decide that they want to create a new item, while editing a current one, they click New operation button on top of the list, the URL changes from /operations/:id to /operations.
  4. No matter new or old item, the item Operations in the navbar stays active.
  5. If the user is editing an item, it should be highlighted as active in the list, if they create a new item — New operation button should be highlighted, accordingly.

Now, check out the weird behaviour: go to Operations and then click Operations navbar item again. Everything disappears. The same happens if I do Operations -> select Operation 1 -> select New operation.

Besides, please, check out the part where I try to get the id parameter:

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
  if (toParams) {
    if (toParams.id) {
      for (var i = 0; i < vm.operations.length; i++) {
        if (vm.operations[i].id == toParams.id) {
          vm.operation = vm.operations[i];
          break;
        }
      }
    }
  }
});

I am no expert, but it seems too long and complex to be true, especially for such a simple task as getting a request parameter. If I try to check on state change $stateParams the object is empty, hence this workaround. If I try to tamper with states in app.js, things change slightly, but there are always bugs like Operations navbar item losing its active state or other weird stuff.

I know that asking such general questions is uncommon in SO, but I really can't grasp the concept of the ui-router, and I can feel that I'm doing things wrong here, and I would really appreciate any help in pointing me in the right direction of how to properly use ui-router for my purposes. Cheers.

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
Anton Egorov
  • 1,328
  • 1
  • 16
  • 33

1 Answers1

1

There is the updated plunker

I just used technique from this Q & A: Redirect a state to default substate with UI-Router in AngularJS

I added the redirectTo setting (could be on any state)

.state('operations', {
      url: '/operations',
      templateUrl: 'operations.index.html',
      controller: 'operationsController as op',
      // here is redirect
      redirectTo: 'operations.new',
 })

and added this redirector:

app.run(['$rootScope', '$state', function($rootScope, $state) {

    $rootScope.$on('$stateChangeStart', function(evt, to, params) {
      if (to.redirectTo) {
        evt.preventDefault();
        $state.go(to.redirectTo, params)
      }
    });
}]);

and also, I removed the redirection currently sitting in the operationsController.js:

angular.module('uiRouterApp')
  .controller('operationsController', function($state, $stateParams, $rootScope) {
    var vm = this;

    //if ($state.current.name === 'operations') $state.go('operations.new');

And that all above is just to keep the new state - without url. Because the solution would become much more easier, if we would just introduce url: '/new':

.state('operations', {
  url: '/operations',
  ..
})
.state('operations.new', {
  //url: '',
  url: '/new',

Check the plunker here

So, this way we gave life to our routing. Now is time to make the detail working. To make it happen we would need more - there is another updated plunker

Firstly, we will get brand new controller to both child state views:

.state('operations.new', {
  url: '',
  views: {
    'detail': {
      templateUrl: 'operations.detail.html',
      controller: 'detailCtrl as dc',         // here new controller
   ...
})
.state('operations.detail', {
  url: '/:id',
  views: {
    'detail': {
      templateUrl: 'operations.detail.html',
      controller: 'detailCtrl as dc',         // here new controller
    ...

It could be same controller for both, because we will keep decision new or existing on the content of the $stateParams.id. This would be its implementation:

.controller('detailCtrl', function($scope, $state, $stateParams) {
    var op = $scope.op;

    op.operation = {id:op.operations.length + 1};

    if ($stateParams.id) {
      for (var i = 0; i < op.operations.length; i++) {
        if (op.operations[i].id == $stateParams.id) {
          op.operation = op.operations[i];
          break;
        }
      }
    }
})

We keep the original approach, and set the op.operation just if $stateParams.id is selected. If not, we create new item, with id properly incremented.

Now we just adjust parent controller, to not save existing, just new:

  .controller('operationsController', function($state, $stateParams, $rootScope) {
    var vm = this;

    //if ($state.current.name === 'operations') $state.go('operations.new');

    vm.operation = {};

    /*$rootScope.$on('$stateChangeStart', 
                    function(event, toState, toParams, fromState, fromParams) {
      if (toParams) {
        if (toParams.id) {
          for (var i = 0; i < vm.operations.length; i++) {
            if (vm.operations[i].id == toParams.id) {
              vm.operation = vm.operations[i];
              break;
            }
          }
        }
      }
    });*/

    vm.save = function() {

      if(vm.operations.indexOf(vm.operation) >= 0){
        return;
      }
      if (vm.operation.name 
      && vm.operation.description 
      && vm.operation.quantity) {
        vm.operations.push(vm.operation);
        vm.operation = {id: vm.operations.length + 1};
      }
    };

Check the complete version here

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • You are a life-saver. I really exhausted my intellectual mana to figure out the desired configuration of states settings like `abstract` and so on. What saved me is 1) adding a `redirectTo` setting and a `redirector`; 2) adding `controller` settings to states, since now `$stateParams.id` is updated on every route change (i.e. selecting an operation). Nothing stops us now from conquering the world _hehehe_ :D Cheers, mate! – Anton Egorov Aug 21 '15 at 07:27
  • If that is working, I am really very glad. Enjoy mighty UI-Router, sir ;) – Radim Köhler Aug 21 '15 at 07:29