I'm having a problem with a new state I added to our web site.
Short description
I have a state (/page)'q.explorer' which contains a button; when clicked it should go to a child state (named 'q.explorer.detail') but it does not. However: in the logging I see that it does try to go to that (/state) and the new url is formatted as defined in the child state.
But still the template and controller that are actually used is the 'parent' which contains the button ...
This may be a little confusing to explain; so I have also added some code in the hope that this will clarify my problem.
The setup looks like this:
$stateProvider
.state('q', {
url: '/:locale/app',
data : { ui: "V2" },
views: {
'application' : {template: '<div ui-view=""><page-home-v2></page-home-v2></div>' }
}, controller: function($scope, $stateParams, siteNavigation) {
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer', {
url: '/explorer?:year&:month&:guide',
template: '<page-explorer-v2></page-explorer-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer");
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer.detail', {
url: '/detail',
template: '<page-explorer-detail-v2></page-explorer-detail-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer.detail");
siteNavigation.applyUrlParameters($stateParams);
}
})
angular
.module('q.components')
.service('siteNavigation', function($state, $location) {
var service = this;
service.applyUrlParameters = function($stateParams) {
if (!$stateParams) $stateParams = $state.params;
console.log('Apply state parameters for state: ' + $state.current.name);
console.log('URL >> ' + $location.url());
};
};
Somewhere deep in the template of "q.explorer" there is a button to open the detail view ("q.explorer.detail"). It uses this code:
function goToDetail() {
var ui = $state.current.data.ui;
if (ui === "V1") { /*...*/ }
else if (ui === "V2") {
var params = {
year: Store.getYear(),
month: Store.getMonth(),
locale: Store.getLocale()
}
var guide = Store.getActiveSidebarItem();
if (guide) params.guide = guide.slug;
console.log("go to explorer: " + params.guide);
$state.go('q.explorer.detail', params);
}
else console.log("Unable to go to state because the ui version is not known.");
}
And this is what I see in the console after clicking the link:
go to explorer: ebit
controller: q.explorer
Apply state parameters for state: q.explorer.detail
URL >> /nl/app/explorer/detail?year=2015&month=11&guide=ebit
As you can see, it uses the controller of the 'parent' iso the child page I want to open. Even though $state.current.name is correct ... Or maybe I should say it does not change from state ...
Any help is welcome.
(PS: We are using Angular 1.4.9)
Looks like you are using nested states such that q.explorer.detail is a child of q.explorer. To render the child state's template, you also need a specific ui-view where it can be placed into. And this will be searched in the template of the parent state. Getting the console.log() output just means the controllers are instantiated, but that even happens if the template isn't rendered at all.
So check if you have an ui-view in the template of the q.explorer state. For more details, please see: https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
You could also fix this by not making q.explorer.detail a child of q.explorer. A child state is created as soon as you need the dot notation.
Yoy have to add somewhere in 'q.explorer' state's template entry point for nested view 'q.explorer.detail', otherwise child controller will not be called.
For example:
template: '<page-explorer-v2></page-explorer-v2><ui-view></ui-view>',
instead of
template: '<page-explorer-v2></page-explorer-v2>'
See jsfiddle: http://jsfiddle.net/jcpmsuxj/42/
Upd. As #ajaegle mentioned you should to visit official docs page:
https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
Related
I have setup a parent view with multiple child views, and it seems to be working with no problem in the application. The problem is if you refresh the page or try to go directly to the child view with the appropriate url.
Here are my states that I am loading. This the parent state:
{name: "myTeam", url: "/myTeam", template: "<my-team></my-team>"}
Here's one of the children:
{name: "myTeam.info", url: "/info", template: "<my-team.info></my-team.info>"}
Here's another:
{name: "myTeam.checkin", url: "/checkin/{book}/{club}/{team}/{bus}", template: "<my-team.checkin></my-team.checkin>"}
So, if I visit the Info view, it loads the template correctly and sets the url to "/myTeam/info". If try to refresh the page or just start out at the url "/myTeam/info", it won't load the state/template.
I added code to the angular-ui-router.js (version 0.1.2) to try to debug. I added the console.log() line at line 912.
UrlMatcher.prototype.exec = function (path, searchParams) {
var m = this.regexp.exec(path);
console.log(" --> " + this.regexp + " -- " + path + " = " + m);
if (!m) return null;
searchParams = searchParams || {};
Here's the output:
--> /^\/myTeam\/?$/ -- /myTeam/info = null
--> /^\/info(.*)?\/?$/ -- /myTeam/info = null
--> /^\/checkin\/([^\/]*)?\/([^\/]*)?\/([^\/]*)?\/([^\/]*)?(.*)?\/?$/ -- /myTeam/info = null
So, it's not matching on the parent or the child.
I was following the example on the Nested States and Nested Views. Their urls looked the same to me.
I don't see how it makes a difference, but I am using the UI extras to load these as future states and using the ocLazyLoad.
Any thoughts on how I can get the URL to work? Or, how it is supposed to work?
I finally determined that the problem was with the UI Extras and future state loading.
I ended out taking the easy road for time's sake. I changed my project to not use any child states, and that takes care of my problem.
From the best I can tell, it is not loading all of the children states. The future state loader is only matching the parent and loading the parent.
I think my problem is similar to this problem that I found here:
UI Router Extras Issue #63
I attempted to write code in my future state loader that would load the parent state and all of it's children. That forced me to have to modify the UI Router Extras to accept an array of promises, and I had to modify too many things to make it work.
Hopefully, at some point I can try to build a more complete fix outside my project and contribute it, but for now, my solution is to avoid child states.
weird, my project have children states, and some children have more children states too, and it's working fine without problems, i will show you how i setup my url:
dashboardState.js
the state with url: '' is the home page, and it is loaded when you navigate to url /home, but their children home loads inside dashboard.
(function () {
'use strict';
angular
.module('myapp')
.config(dashboardStates);
function dashboardStates($stateProvider) {
var dashboard = {
name: 'dashboard',
abstract: true,
url: '/home', // view inicial abaixo
templateUrl: 'app/components/Dashboard/view/dashboard.html',
controller: 'DashboardController',
controllerAs: 'dashboard'
};
var dashboardHome = {
name: 'dashboard.home',
url: '',
parent: 'dashboard',
templateUrl: 'app/components/Dashboard/view/dashboard.home.html',
controller: 'DashboardController',
controllerAs: 'home',
ncyBreadcrumb: {
label: 'Home'
}
};
// states
$stateProvider
.state(dashboard)
.state(dashboardHome);
}
}());
and the state below loads inside the dashboard view:
profileState.js
(function () {
'use strict';
angular
.module('myapp')
.config(profileStates);
function profileStates($stateProvider) {
var profile = {
name: 'dashboard.profile',
url: '/profile',
templateUrl: 'app/components/Profile/view/profile.html',
controller: 'ProfileController',
controllerAs: 'profile'
};
var profileEdit = {
name: 'dashboard.profile.edit',
url: '/edit',
templateUrl: 'app/components/Profile/view/profile.edit.html',
controller: 'ProfileController',
controllerAs: 'profile'
};
$stateProvider
.state(profile)
.state(profileEdit);
}
}());
more info: https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
I'm currently working on an Angular web application which will display various types of financial events for the user. These events are tied together under one ID, organized into different groups. Each of these groups needs to be displayed in their own separate HTML <table>.
That financial data is retrieved using an Angular Service, and resolved within the Angular UI Router configuration. All of the data is also stored within that same Service.
Below is the current Router configuration:
import EventsController from './events.controller';
import EventsService from './events.service';
import EventsTableController from './events-tables/events-table.controller';
import eventsView from './events.html';
import eventsDetailsTableView from './events-tables/details-table.html';
export default function routes($stateProvider) {
$stateProvider
.state('events', {
url: '/events/{payDate:string}',
template: eventsView,
controller: EventsController,
controllerAs: 'events',
resolve: {
eventsData: ['$http', '$stateParams', ($http, $stateParams) => {
return new EventsService($http, $stateParams);
}]
},
views: {
'details#': {
template: eventsDetailsTableView,
controller: EventsTableController,
controllerAs: 'details',
resolve: {
eventData: 'eventsData.details'
}
}
}
}
);
}
routes.$inject = ['$stateProvider'];
In the future, the EventsController will be used to filter which <table>s will be displayed based on the user's preference.
The EventsTableController is a generic Controller which stores both the type of data being displayed (in this example, "details"), and the data itself, stored as a two-dimensional array.
export default class EventsTableController {
constructor(eventData) {
this.name = eventData.name;
this.data = eventData.data;
console.log(this)
}
}
Also for reference, here's an example of the data being returned from the Service:
{
'details': [[
"Event-ID",
"Company-CUSIP",
"Company-Name"]
]
}
Each <table> will correspond to a different field within that object.
I'm having trouble, however, passing the data from the 'events' state's resolve into the nested details# view. The above configuration returns an error, stating that Angular UI Router is unable to find the specific dependency: 'eventsData.details'.
My question is this: how do I pass individual object fields into the nested views, such that they can all be independently displayed? If there is any more source information that I can provide, please let me know and I'll amend this post for clarity.
By default resolves from parent are available in child states, and named views.
If you have multiple named views inside a state, and they need to use one of the state's resolves, you can inject them normally in the controller, for example:
EventsTableController.$inject = ['eventsData
function EventsTableController(eventsData) {
console.log(eventsData);
}
Child states also inherit resolved dependencies from parent state(s). For example (resA):
$stateProvider.state('parent', {
resolve:{
resA: function(){
return {'value': 'A'};
}
},
controller: function($scope, resA){
$scope.resA = resA.value;
}
})
.state('parent.child', {
resolve:{
resB: function(resA){
return {'value': resA.value + 'B'};
}
},
controller: function($scope, resA, resB){
$scope.resA2 = resA.value;
$scope.resB = resB.value;
}
I have an example here. How i can access 'itemId' parameter in the parent state controller? Parent view must not be reloaded.
angular.module('app',['ui.router'])
.config(function($stateProvider){
$stateProvider.state('step', {
url: '/orders/:step',
templateUrl: 'parent.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
})
.state('step.item', {
url: '/:itemId',
templateUrl: 'child.html',
controller: function ($scope, $stateParams) {
$scope.itemId = $stateParams.itemId;
$scope.step = $stateParams.step;
}
});
}).controller('SimpleController', function($scope, $state){
$scope.items = [1,2,3,4,5,6];
$scope.steps = ["first", "second"];
})
I see only two ways:
add an service and inject it to both controllers
access a parent scope from a child controller and pass a parameter
But both cause me to add watchers at a parent scope.
Maybe i can accomplish it easier?
First, you need to recognize that a parent state's controller runs only once when you enter the subtree of its child states, so switching between its children would not re-run the controller.
This is to say that if you want the itemId parameter to always be up to date, you'd need a $watch (which you tried to avoid).
For the first time, you could collect the itemId like so in the parent's state:
$scope.itemId = $state.params.itemId;
If you need it to be kept up-to-date, then you'd need to watch for changes, for example:
$scope.$watch(function(){
return $state.params;
}, function(p){
$scope.itemId = p.itemId;
});
plunker
Similarly, if you only need to place it in the view, then set $state on the scope:
$scope.$state = $state;
and in the view:
<div>parent /orders/{{step}}/{{$state.params.itemId}}</div>
EDIT:
I guess another way would be to call a scope-exposed function on the parent to update the value. I'm not a fan of such an approach, since it relies on scope inheritance (which is not always the same as state inheritance) and scope inheritance in a large application is difficult to track. But it removes the need for a $watch:
In the parent controller:
$scope.updateItemId = function(itemId){
$scope.itemId = itemId;
};
and in the child controller:
if ($scope.updateItemId) $scope.updateItemId($stateParams.itemId)
I can see, that you've already found your answer, but I would like to show you different approach. And I would even name it as "the UI-Router built in approach".
It has been shown in the UI-Router example application, where if we go to child state with some ID = 42 like here we can also change the other, than the main view (the hint view in this case).
There is a working example with your scenario, showing that all in action.
What we do use, is the Multiple Named Views
The parent state, now defines root view, which is injected into unnamed ui-view="" inside of the root/index.html.
A new parent.html
<div ui-view="title"></div>
<div ui-view=""></div>
As we can see, it also contains another view target (than unnamed) - e.g. ui-view="title" which we fill with another template immediately... in parent state:
$stateProvider.state('step', {
url: '/orders/:step',
views : {
'': {
templateUrl: 'parent.html',
},
'title#step': {
templateUrl:'parent_title_view.html',
}
}
})
And child? It can continue to handle main area... but also can change the title area. Both views are independent, and belong to child.
.state('step.item', {
url: '/:itemId',
views : {
'': {
templateUrl: 'child.html',
},
'title': {
templateUrl:'child_title_view.html',
}
}
So, there are no watchers, nothing... which is not shipped with the UI-Router by default. We just adjust many places (views) with the child implementation. We can still provide their content with some defaults in parent..
Check that example here. Hope it will help to see it a bit differently...
I'm not sure if this will really solve your problem but you can do
$stateProvider.state('step', {
url: '/orders/:step/:itemId',
templateUrl: 'parent.html',
http://plnkr.co/edit/sYCino1V0NOw3qlEALNj?p=preview
This means the parent state will collect the itemID on the way through.
include $state in resolve function for parent state
.state('parent', {
url: '/parent',
controller: 'parentController',
resolve: {
params: ['$state', function ($state) {
return $state.params;
}]
}
})
then in parent controller inject the resloved vars
app.controller('parentController', ['$scope', 'params',
function ($scope, params) {
$scope.params = params;
}]);
in this way params available in both child and parent.
I am building an angular-app with ui-router where I have a parent view and a child view.
routerApp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/views/home/home.html',
controller: 'HomeController'
})
.state('new-assignment', {
url: '/new-assignment',
templateUrl: 'new-assignment.html',
controller: 'AssignmentController'
})
.state('new-assignment.new-client', {
url: '/new-client',
templateUrl: 'new-client.html',
controller: 'ClientController'
})
;
});
The child view is used (among other things) to create a new client.
So, by clicking on 'create new client' in the main view. The state is change to 'new-assignment.new-client', and the side view is shown.
When the client is created I want to transition back to the parent view 'new-assignment' and pass along information with what client that have just been created.
The parent view could still have data in its own form (for creating assignment) and should naturally not be touched.
I can detect a change by '$stateChangeSuccess' event:
routerApp.controller('AssignmentController', function($scope, Assignment, Client, $log) {
$scope.clients = Client.clients;
$scope.$on('$stateChangeSuccess', function (e, toState, toParams, fromState, fromParams){
$log.log(fromState.name + ' -> ' + toState.name);
$log.log('toParams ');
$log.log(toParams);
$log.log('fromParams ');
$log.log(fromParams);
});
});
I have tried to pass info through
$state.go('^', data);
but without success...
But I don't understand how to pass data to the parent view. Any ideas?
Child view scope inherits from the scope of the parent view. So just define $scope.clients[] array in the AssignmentController. Then add the new client to that array in the ClientController.
Take a look at this Plunker to see how the nested view scopes get access to their parent view scope.
Rudely I will answer my own question.
I decided to go with event messaging:
app.controller('ClientController', function ($scope, $state, ClientService) {
$scope.save = function (newClientData) {
var ret = ClientService.create(newClientData);
if (ret) {
// Pass newly created object to parent view.
$scope.$emit('clientCreated', newClientData);
// Then go to that state (closing child view)
$log.log($state);
if ($state.current.name === 'new-client')
return $state.go('home');
$state.go('^');
}
};
});
app.controller('AssignmentController', function ($scope, Assignment, ClientService) {
$scope.clients = ClientService.clients;
$scope.$on('clientCreated', function (e, data) {
$scope.clientSelected(data);
});
$scope.clientSelected = function (client) {
$log.log(client);
$scope.data.client = client;
$scope.data.clientName = client.name;
$scope.data.clientId = client.id;
}
});
This code is linked to the code in my question!
yes, various ideas.
You can send the data information using query parameters. Converting your data to JSON and sending like this "$state.go('^', data)" and defining your state url with "new-assignmentww/?data".
If data is assigned, you can get information and convert to object again.
Other, you can access the parent state using "$state.$parent" and assign the value in the controller, then you can call the "$state.go('^')".
You can also create a callback function in controller of the parent state and call in the controller of the children state.
I am using this FAQ entry to open a modal dialog in a child state of a certain state: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state
My code is below. When I open a modal dialog, I need to get access to the properties of the parent state's scope. Is this possible?
plnkr: http://plnkr.co/edit/knY87n
.state('edit', {
url: '/{id:[0-9a-f]+}',
views: {
'#': {
templateUrl: 'views/edit.html',
controller: 'editContr'
}
}
})
.state('edit.item', {
url: "/item/new",
onEnter: function($stateParams, $state, $modal) {
$modal.open({
controller: 'itemEditContr',
templateUrl: 'views/edit-item.html',
}).result.then(function (item) {
//
// here I need to insert item into the items
// seen by my parent state. how?
//
$state.go('^');
}, function () {
$state.go('^');
});
}
});
function editContr($scope) {
$scope.items = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
}
function itemEditContr($scope, $modalInstance) {
$scope.submit = function () {
$modalInstance.close($scope.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.item = {name: 'test'};
}
Are you planning to update params set by the parent? You should be able to get the parent state using
$state.$current.parent
For e.g. the name of the parent state will be $state.$current.parent.name
EDIT: Updating since the OP wanted to access the Scope and not the parent state.
You can emit an event from the child and then capture it in the parent.
Untested code:
In Parent:
$scope.$on('ADD_ITEM', function(evt, msg) {
$scope.items.add(msg);
}
In the child state:
$scope.$emit('ADD_ITEM', item);
long story short, yes it is. Reading the angularjs developer guide for scopes, is actually one of their more helpful and very well documented pieces: http://docs.angularjs.org/guide/scope
Aside from that, scope in angular is no different than scope with any javascript object.
You've got one or two things that you're not doing correctly. For one, passing item in your onEnter function won't help unless you're grabbing something from the url as an identifier, or resloving some data that you can inject into the states controller. You're trying to do the latter, but you aren't resolving anything, so you are getting undefined on item.
One trick you can use is to set a truthy value in your your parent, and access it.
//in parent ctrl
$scope.newItem = function(itemname){
return {name:itemname}
}
$scope.save = function(item){
$scope.items.push(item);
}
Then when you open your modal call $scope.getItem() within the controller instead of injecting item into the controller directly
function itemEditContr($scope, $modalInstance) {
$scope.submit = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.item = $scope.newItem('test') //this will look for newItem funciton in the local scope, fail to find it, and walk up the heirarchy until it has found newItem() in the parent
//now a save function, also defined on the parent scope
$scope.save($scope.item);
}
Accessing the parent scope is nothing special, just make sure to get a value, and not overwrite it. so you can access $scope.items from the child controller by assigning it to a variable, or you can push it new values, but never set it, or you will create a new local items object on the child scope instead.
I struggled with this too and found an alternative solution. Instead of using the onEnter function you can use a controller with a very simple view that calls a function to open the modal. I used a
config(['$stateProvider', function($stateProvider) {
// Now set up the states
$stateProvider
.state('exmple.modal', {
url: "/example/modal/",
controller: "ExampleController",
template: '<div ng-init="openModal()"></div>'
});
}]);
In the example controller you an put the openModal() function to open the modal where you have a scope.