11

I am trying to create a "Todo App" with angularjs ui-router. It has 2 columns:

  • Column 1: list of Todos
  • Column 2: Todo details or Todo edit form

In the Edit and Create controller after saving the Todo I would like to reload the list to show the appropriate changes. The problem: after calling $state.go('^') when the Todo is created or updated, the URL in the browser changes back to /api/todo, but the ListCtrl is not executed, i.e. $scope.search is not called, hence the Todo list (with the changed items) is not retrieved, nor are the details of the first Todo displayed in Column 2 (instead, it goes blank).

I have even tried $state.go('^', $stateParams, { reload: true, inherit: false, notify: false });, no luck.

How can I do a state transition so the controller eventually gets executed?

Source:

var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
    .config(function ($stateProvider, $urlRouterProvider) {
        $urlRouterProvider.otherwise('/api/todo');

        $stateProvider
            .state('todo', {
                url: '/api/todo',
                controller: 'ListCtrl',
                templateUrl: '/_todo_list.html'
            })
            .state('todo.details', {
                url: '/{id:[0-9]*}',
                views: {
                    'detailsColumn': {
                        controller: 'DetailsCtrl',
                        templateUrl: '/_todo_details.html'
                    }
                }
            })
            .state('todo.edit', {
                url: '/edit/:id',
                views: {
                    'detailsColumn': {
                        controller: 'EditCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
            .state('todo.new', {
                url: '/new',
                views: {
                    'detailsColumn': {
                        controller: 'CreateCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
        ;

    })
;

TodoApp.factory('Todos', function ($resource) {
    return $resource('/api/todo/:id', { id: '@id' }, { update: { method: 'PUT' } });
});

var ListCtrl = function ($scope, $state, Todos) {
    $scope.todos = [];

    $scope.search = function () {
        Todos.query(function (data) {
            $scope.todos = $scope.todos.concat(data);
            $state.go('todo.details', { id: $scope.todos[0].Id });
        });
    };

    $scope.search();
};

var DetailsCtrl = function ($scope, $stateParams, Todos) {
    $scope.todo = Todos.get({ id: $stateParams.id });
};

var EditCtrl = function ($scope, $stateParams, $state, Todos) {
    $scope.action = 'Edit';

    var id = $stateParams.id;
    $scope.todo = Todos.get({ id: id });

    $scope.save = function () {
        Todos.update({ id: id }, $scope.todo, function () {
            $state.go('^', $stateParams, { reload: true, inherit: false, notify: false });
        });
    };
};

var CreateCtrl = function ($scope, $stateParams, $state, Todos) {
    $scope.action = 'Create';

    $scope.save = function () {
        Todos.save($scope.todo, function () {
            $state.go('^');
        });
    };
};
5
  • 2
    I would say, it is some misunderstanidng about states. check github.com/angular-ui/ui-router/wiki/…. When the application is in a particular state—when a state is active—all of its ancestor states are implicitly active as well., if you go up, you go to existing state. no reload, no refresh... it is the one you've been and you are... Commented Jan 23, 2014 at 13:28
  • Ok, I understand what you mean. Then how can I achieve the required functionality? Commented Jan 23, 2014 at 13:32
  • 2
    I will tell you my story ;) How I do that: I do call axplicit $scope.refresh() created by me and placed on the parent, when (or before) I go back from child to parent, E.g. in case of success (After Edit was correct, and have to reload the detail again)... in case of cancel.. I call $scope.reset() again declared in parent... but this is just my way ;) Commented Jan 23, 2014 at 13:35
  • @Radim Köhler: Sounds good. Can you please modify my code according to these? Just to see how it is put together. (E.g. how to call the $scope.refresh() on the parent, etc. Commented Jan 23, 2014 at 13:37
  • 1
    I am sure, that you've already done it. I am sorry, for later response, but as promised here it is. I'd say that at least the below approach could give you some hints. Please, take it as an inspiration (code reflects the idea but was written here in SO editor, summarizing my real HOW TO). Good luck with angular JS ;) Commented Jan 25, 2014 at 6:46

3 Answers 3

5

I would give an example (a draft) of HOW TO nest edit into detail. Well, firstly let's amend the templates.

The Detail template, contains full definition of the detail. Plus it now contains the attribute ui-view="editView". This will assure, that the edit, will "replace" the detail from the visibility perspective - while the edit scope will inherit all the detail settings. That's the power of ui-router

<section ui-view="editView">
  <!-- ... here the full description of the detail ... -->
</section>

So, secondly let's move the edit state, into the detail

// keep detail definition as it is
.state('todo.details', {
    url: '/{id:[0-9]*}',
    views: {
        'detailsColumn': {
            controller: 'DetailsCtrl',
            templateUrl: '/_todo_details.html'
        }
    }
})
// brand new definition of the Edit
.state('todo.details.edit', { // i.e.: url for detail like /todo/details/1/edit
    url: '/edit',
    views: {
        'editView': {    // inject into the parent/detail view
            controller: 'EditCtrl',
            templateUrl: '/_todo_edit.html'
        }
    }
})

Having this adjusted state and template mapping, we do have a lot. Now we can profit from the ui-router in a full power.

We'll define some methods on a DetailCtrl (remember, to be available on the inherit Edit state)

var DetailsCtrl = function ($scope, $stateParams, Todos) {

    $scope.id =  $stateParams.id // keep it here

    // model will keep the item (todos) and a copy for rollback
    $scope.model = {
        todos : {},
        original : {},
    }

    // declare the Load() method

    $scope.load = function() {
        Todos
          .get({ id: $stateParams.id })
          .then(function(response){

              // item loaded, and its backup copy created
              $scope.model.todos = response.data;
              $scope.model.original = angular.copy($scope.model.todos);

          });
    };

    // also explicitly load, but just once,
    // not auto-triggered when returning back from Edit-child
    $scope.load()
};

OK, it should be clear now, that we do have a model with the item model.todos and its backup model.original.

The Edit controller could have two actions: Save() and Cancel()

var EditCtrl = function ($scope, $stateParams, $state, Todos) {
    $scope.action = 'Edit';

    // ATTENTION, no declaration of these, 
    // we inherited them from parent view !
    //$scope.id ..     // we DO have them
    //$scope.model ...

    // the save, then force reload, and return to detail
    $scope.save = function () {
        Todos
           .update({ id: id })
           .then(function(response){

              // Success
              $scope.load(); 
              $state.go('^');
           },
           function(reason){

             // Error
             // TODO 
           });
    };

    // a nice and quick how to rollback
    $scope.cancel = function () {
         $scope.model.todos = Angular.copy($scope.model.original);
         $state.go('^');
    };
};

That should give some idea, how to navigate between parent/child states and forcing reload.

NOTE in fact, instead of Angular.copy() I am using lo-dash _.cloneDeep() but both should work

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for this. But what about the refreshing of the list, if I modify or add a new Todo?
Won't be the mechanism similar? the list load() or refresh() function published on the upper scope/view (as detail does for edit)... and it could be called as well (inside of the save()) .. The logic is then the same everywhere... that's the trick ;) I'd say
What I am trying to say: Instead of managing reloading via "state changes" we should adjust the thinking about them. The biggest advantage I see there is: 1) The 'todo' state, should show me the list. 2) If I select the item, I won't loose the list, just a child view detail will be injected 3) if I click edit, some part of detail view (if not the whole) will be extended with edit part. Now, which model part is updated based on edit changes... is in our hands. While all other pieces, related to these state do not have to be changed and that's the way how I do use ui-router ;)
Thank you for the explanation. All I needed was that $scope is inherited and I can call $scope.load(); before $state.go('^');. I have added it and it works. I'll post my solution.
Awesome! Brilliant. A note: do not forget that inheritance goes via the "View" path. great ;) Enjoy Angular
5

Huge thanks for Radim Köhler for pointing out that $scope is inherited. With 2 small changes I managed to solve this. See below code, I commented where I added the extra lines. Now it works like a charm.

var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
    .config(function ($stateProvider, $urlRouterProvider) {
        $urlRouterProvider.otherwise('/api/todo');

        $stateProvider
            .state('todo', {
                url: '/api/todo',
                controller: 'ListCtrl',
                templateUrl: '/_todo_list.html'
            })
            .state('todo.details', {
                url: '/{id:[0-9]*}',
                views: {
                    'detailsColumn': {
                        controller: 'DetailsCtrl',
                        templateUrl: '/_todo_details.html'
                    }
                }
            })
            .state('todo.edit', {
                url: '/edit/:id',
                views: {
                    'detailsColumn': {
                        controller: 'EditCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
            .state('todo.new', {
                url: '/new',
                views: {
                    'detailsColumn': {
                        controller: 'CreateCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
        ;

    })
;

TodoApp.factory('Todos', function ($resource) {
    return $resource('/api/todo/:id', { id: '@id' }, { update: { method: 'PUT' } });
});

var ListCtrl = function ($scope, $state, Todos) {
    $scope.todos = [];

    $scope.search = function () {
        Todos.query(function (data) {
            $scope.todos = $scope.todos(data); // No concat, just overwrite
            if (0 < $scope.todos.length) { // Added this as well to avoid overindexing if no Todo is present
                $state.go('todo.details', { id: $scope.todos[0].Id });
            }
        });
    };

    $scope.search();
};

var DetailsCtrl = function ($scope, $stateParams, Todos) {
    $scope.todo = Todos.get({ id: $stateParams.id });
};

var EditCtrl = function ($scope, $stateParams, $state, Todos) {
    $scope.action = 'Edit';

    var id = $stateParams.id;
    $scope.todo = Todos.get({ id: id });

    $scope.save = function () {
        Todos.update({ id: id }, $scope.todo, function () {
            $scope.search(); // Added this line
            //$state.go('^'); // As $scope.search() changes the state, this is not even needed.
        });
    };
};

var CreateCtrl = function ($scope, $stateParams, $state, Todos) {
    $scope.action = 'Create';

    $scope.save = function () {
        Todos.save($scope.todo, function () {
            $scope.search(); // Added this line
            //$state.go('^'); // As $scope.search() changes the state, this is not even needed.
        });
    };
};

Comments

3

I might have faced a similar problem the approach i took was to use $location.path(data.path).search(data.search); to redirect the page then in the controller I caught the $locationChangeSuccess event. I other words I use the $location.path(...).search(...) as apposed to $state.go(...) then caught the $locationChangeSuccess event which will be fired when the location changes occurs before the route is matched and the controller invoked.

var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
    .config(function ($stateProvider, $urlRouterProvider) {
        $urlRouterProvider.otherwise('/api/todo');

        $stateProvider
            .state('todo', {
                url: '/api/todo',
                controller: 'ListCtrl',
                templateUrl: '/_todo_list.html'
            })
            .state('todo.details', {
                url: '/{id:[0-9]*}',
                views: {
                    'detailsColumn': {
                        controller: 'DetailsCtrl',
                        templateUrl: '/_todo_details.html'
                    }
                }
            })
            .state('todo.edit', {
                url: '/edit/:id',
                views: {
                    'detailsColumn': {
                        controller: 'EditCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
            .state('todo.new', {
                url: '/new',
                views: {
                    'detailsColumn': {
                        controller: 'CreateCtrl',
                        templateUrl: '/_todo_edit.html'
                    }
                }
            })
        ;

    })
;

TodoApp.factory('Todos', function ($resource) {
    return $resource('/api/todo/:id', { id: '@id' }, { update: { method: 'PUT' } });
});

var ListCtrl = function ($scope, $state, Todos, todo.details) {
    /*here is where i would make the change*/        
    $scope.$on('$locationChangeSuccess', function () {
          $scope.search();
          $route.reload();
    });

        $scope.todos = [];

        $scope.search = function () {
            Todos.query(function (data) {
                $scope.todos = $scope.todos.concat(data);
            });
        };

    $scope.search();
};

var DetailsCtrl = function ($scope, $stateParams, Todos) {
    $scope.todo = Todos.get({ id: $stateParams.id });
};

var EditCtrl = function ($scope, $stateParams, $state, Todos, $location) {
    $scope.action = 'Edit';

    var id = $stateParams.id;
    $scope.todo = Todos.get({ id: id });

    $scope.save = function () {
        Todos.update({ id: id }, $scope.todo, function () {
           //here is where I would make a change
                $location.path('todo.details').search($stateParams);
        });
    };
};

var CreateCtrl = function ($scope, $stateParams, $state, Todos, $location) {
    $scope.action = 'Create';

    $scope.save = function () {
        Todos.save($scope.todo, function () {
           //here is where I would make a change
                $location.path('todo.details');
        });
    };
};

the $locationChangeSuccess event occurs before the route is matched and the controller invoked

2 Comments

Can you please elaborate it? I.e. modify my code with this solution. Also at first sight it looks like a 'hack'. What other effects does this have (subscribing to the $locationChangeSuccess)?
It is a bit of a hack and I would be interested in a better answer myself but I know that it worked for me and my appologies I will change the answer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.