I am new to AngularJS and I am using Angular UI-Router for my SPA.
What I am trying to do is to update the Parent View values from the Child View. I have gone through the UI-Router documentation for Nested Views and Multiple Views but couldn't find a way to update the values.
My use case is, Parent view will be the Header and every time a Child View changes via the State Transition I want to update the header value which is part of the Parent View.
Code :
HTML File -
<div ui-view></div>
JS File where Angular UI-Routing configuration happens -
angular.module('myApp', ['ui.router']).config(['$stateProvider', '$routeProvider',
function($stateProvider, $routeProvider) {
$stateProvider
.state('main', {
resolve: {
resA: function() {
return {
'value': 'Hello !!'
};
}
},
controller: function($scope, resA) {
$scope.resA = resA.value;
},
abstract: true,
url: '/main',
template: '<h1>{{resA}}</h1>' +
'<div ui-view></div>'
})
.state('main.one', {
url: '/one',
views: {
'#main': {
template: "Im View One"
}
},
resolve: {
resB: function(resA) {
return {
'value': resA.value + ' from One'
};
}
},
controller: function($scope, resA, resB) {
$scope.resA = resB.value;
}
}).state('main.two', {
url: '/two',
views: {
'#main': {
template: '<div ui-view="sub1"></div>' +
'<div ui-view="sub2"></div>'
},
'sub1#main.two': {
template: "Am awesome"
},
'sub2#main.two': {
template: "Am awesome two/too"
}
},
resolve: {
resC: function(resA) {
return {
'value': resA.value + ' from Two'
};
}
},
controller: function($scope, resA, resC) {
$scope.resA = resC.value;
}
});
}]).run(['$rootScope', '$state', '$stateParams', function($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$state.transitionTo('main.two');
}]);
Here is the JSFiddle Link of the same code snippet.
There are muliple ways you can update the parent scope.
Using controllerAs
https://jsfiddle.net/9n7wrevt/17/
controller: function($scope, resA) {
this.resA = resA.value;
},
controllerAs: 'main'
referring parent as below
controller: function($scope, resB) {
$scope.main.resA = resB.value;
}
Using $parent
https://jsfiddle.net/9n7wrevt/18/
controller: function($scope, resB) {
$scope.$parent.resA = resB.value;
}
Better way(highly recommended) is to use $scope $emit, $on to communicate between controllers.
https://jsfiddle.net/9n7wrevt/19/
I am using UI router and my 2 states use the same view. My approach is as follows.
<button ui-sref="test.state1">
.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('test.state1', {
url: '/mystate/start',
template: require('./myPage.html'),
controller: function ($scope, myService) {
console.log('2 ctrl called ');
myService.myMethod().then(function () {
console.log('3 calling my method ');
});
},
resolve: {
testOne: [function () {
//service call
}],
testTwo: [function () {
//service call
}]
}
}).state('test.state2', {
url: '/mystate/end',
template: require('./myPage.html'),
controller: 'MyController as myctr'
});
}])
I can see my console logs in order of 1,2 and 3, first resolving the data and then hitting inline controller.
I am trying to change this to use abstract parameter.
$stateProvider
.state('test', {
abstract: true,
url: '/mystate',
template: require('./myPage.html')
})
.state('test.state1', {
url: '/start',
controller: function ($scope, myService) {
console.log('2 ctrl called ');
myService.myMethod().then(function () {
console.log('3 calling my method ');
});
},
resolve: {
testOne: [function () {
//service call
}],
testTwo: [function () {
//service call
}]
}
})
.state('test.state2', {
url: '/end',
controller: 'MyController as myctr'
})
Now I can only see the resolve and the inline ctrl doesn't get invoked. Is it something that I am missing in the setup ? documentation here is inconclusive.
When an item is clicked, its path is sent as parameter to state function and the state function does $state.go to load a state with the passed path as parameter.
This doesn't seem to work. What am I missing here?
Template
<div ng-click="state(item.class, item.path, item.mime_type)">
Controller
controller("groupsListCtrl", ["$scope", "handler", "$state",
function($scope, handler, $state) {
handler.get("/home").then(function(response) {
$scope.data = response;
$scope.items = $scope.data.inventory;
$scope.state = function(stateType, objectPath, mimeType) {
$state.go("workarea.user", {
path: objectPath
});
}
})
}
])
Router
.state("workarea.user", {
url: "^/workarea/:path",
requireLogin: true,
views: {
"options": {
templateUrl: "/views/options.html",
controller: "optionsCtrl"
},
"workspace": {
templateUrl: "/views/workspace.html",
controller: "workspaceCtrl"
},
"comments": {
templateUrl: "/views/comments.html",
controller: "commentsCtrl"
}
}
});
handler.get("/home").then(function(response) { looks like a promise to me.
Try moving $scope.state definition outside this promise.
controller("groupsListCtrl", ["$scope", "handler", "$state",
function($scope, handler, $state) {
handler.get("/home").then(function(response) {
$scope.data = response;
$scope.items = $scope.data.inventory;
});
$scope.state = function(stateType, objectPath, mimeType) {
$state.go("workarea.user", {
path: objectPath
});
}
}
])
I am new to AngularJS and I used ncy-breadcrumb for my AngularJS project. There is an abstract true parent state and two child states of it. I used these child states for tabs. But I couldn't find a way to show these states in the breadcrumb dynamically. The only thing I can do is hardcode one child state name as parent in other state. But I need a solution to display these child states in collectionsWorkPage state dynamically.
.state('collectionsLibrary', {
url: '/headquarters/collections-library/',
templateUrl: 'app/views/collectionsLibrary/base.html',
controller: 'CollectionsLibraryBaseController',
ncyBreadcrumb: {
label: 'Collections Library',
parent: 'headquarters'
},
abstract: true,
resolve: {
controller: function ($q) {
var deferred = $q.defer();
require(['controllers/collectionsLibrary/CollectionsLibraryBaseController'], function () {
deferred.resolve();
});
return deferred.promise;
}
}
})
.state('collectionsLibrary.available', {
url: 'available/',
templateUrl: 'app/views/collectionsLibrary/available.html',
controller: 'CollectionsLibraryAvailableController',
ncyBreadcrumb: {
label: 'Collections Library-Available',
parent: 'headquarters'
},
resolve: {
controller: function ($q) {
var deferred = $q.defer();
require(['controllers/collectionsLibrary/CollectionsLibraryAvailableController'], function () {
deferred.resolve();
});
return deferred.promise;
}
}
})
.state('collectionsLibrary.my', {
url: 'my/',
templateUrl: 'app/views/collectionsLibrary/my.html',
controller: 'CollectionsLibraryMyController',
ncyBreadcrumb: {
label: 'Collections Library-My',
parent: 'headquarters'
},
resolve: {
controller: function ($q) {
var deferred = $q.defer();
require(['controllers/collectionsLibrary/CollectionsLibraryMyController'], function () {
deferred.resolve();
});
return deferred.promise;
}
}
})
.state('collectionsWorkPage', {
url: '/headquarters/collections-library/:id/edit/',
templateUrl: 'app/views/collectionsWorkPage.html',
controller: 'CollectionsWorkPageController',
ncyBreadcrumb: {
label: 'Edit Collection',
parent: 'collectionsLibrary.available'
},
params: {
data: {}
},
resolve: {
controller: function ($q, $stateParams) {
var deferred = $q.defer($stateParams);
require(['controllers/CollectionsWorkPageController'], function () {
deferred.resolve();
});
return deferred.promise;
}
}
})
The parent property can be either a string or a function returning the parent name. The function provides the scope of the current state controller (the same used for labels interpolation).
So you can do something like this:
ncyBreadcrumb: {
label: 'Edit Collection',
parent: function($scope) {
if($scope.tab === 'MY') // Constant defined in CollectionsLibraryMyController
return 'collectionsLibrary.my';
else if($scope.tab === 'AVAILABLE') // Constant defined in CollectionsLibraryAvailableController
return 'collectionsLibrary.available';
}
}
See API reference for more details
app.config(['$breadcrumbProvider', configNcyBreadcrumb])
function configNcyBreadcrumb($breadcrumbProvider) {
$breadcrumbProvider.setOptions({
includeAbstract : true
});
}
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('^');
});
};
};
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
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.
});
};
};
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