My application has two controllers. I have a pageCtrlwhich I use to handle my navigation and sidebar. I also have a calendarCtrl for handling the data on my page. This controller is configured as shown below:
$stateProvider
.state('calendar', {
url: '/calendar/:category/:competition/:team',
controller: 'calendarCtrl',
templateUrl: 'app/modules/calendar/calendarView.html',
})
To make my navigation work I also need access to the :category/:competition/:team-params in my pageCtrl. Can I configure this using the same way? Something like:
$stateProvider
.state("page", {
abstract: true,
controller: 'pageCtrl',
// params: :category/:competition/:team
})
Edit: Using $stateParams in the calendarCtrl works fine. I just can't figure out how I can make sure my pageCtrl also can get read the url.
Since you're using ui.router, inject $stateParams in your controller(s) and then you can access those values like so:
controller.js
function($stateParams){
$stateParams.category
$stateParams.competition
$stateParams.team
My suggestion would be - use more views - the UI-Router built feature.
Multiple Named Views
There is a working plunker
Let's have the 'Parent' state which has this template:
This blue is the parent template. Orange are child views
<!-- HERE is one named view target -->
<div ui-view="title">This is a title filled by child having access to param</div>
...
<!-- HERE is other view target un-named -->
<div ui-view></div>
And its state is very simple. The interesting is the child state, which is taking care about both views:
.state('parent', {
abstract: true,
url: "/parent",
templateUrl: 'tpl.parent.html',
})
.state('parent.child', {
url: "/child/:id",
views : {
'': {
templateUrl: 'tpl.child.html',
},
'title': {
templateUrl: 'tpl.title.html',
controller: 'TitleCtrl',
},
}
})
So, we do have a target for "some other view" title or side bar. Check it here
And we can even place some default implementation there inside of our "non-abstract" parent state.
There is extended plunker with non abstract parent state definition:
.state('parent', {
url: "/parent",
views : {
'': {
templateUrl: 'tpl.parent.html',
},
'title#parent': {
template: 'the parent own TITLE',
},
}
})
Check it here
There is a way, how to grant access to latest/up-to-date $stateParams - including current state and its child(ren) as well. (working example here)
It is surprisingly easy:
.run(['$rootScope', '$state', '$stateParams',
function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
}])
And that's it. (check similar answer and some discussion here)
With this approach, we will even in parent $scopes have updated reference to the latest $stateParams. While in our own, we will still receive just our own part
.controller('ParentCtrl', ['$scope', '$stateParams', function ($scope, $stateParams) {
$scope.currentStateParams = $stateParams;
}])
The above is valid for states like these:
.state('parent', {
url: "/parent?area",
templateUrl: 'tpl.html',
controller: 'ParentCtrl',
})
.state('parent.child', {
url: "/child/:id",
templateUrl: 'tpl.html',
controller: 'ChildCtrl',
})
Working example to play here.
But I still would say, that this is a bit ... against the UI-Router. I would prefer this answer. Because in that case, each view (while injected into some parent area) is really aware about $stateParams, which belongs to that state. What we are doing here is introduction of some observer pattern (we should watch changes if we want to react in parent) and that would later bring more issues then profit
Related
I'd like to set the title based on the state in my angular app. It works nicely, but if there's no title defined on the child state, I'd like to fall back to the parent's title property.
This is what I have so far:
In my app run block:
...
$rootScope.$on('$stateChangeSuccess', function (event, current, previous) {
if (current.hasOwnProperty('title')) {
$rootScope.title = current.title;
}
});
...
In my module's route
.state('main.parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'vm',
templateUrl: 'app/parent.html',
title: 'Parent',
})
.state('main.parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'vm',
templateUrl: 'app/child.html'
})
During the code inspection, I found that $stateChangeSuccess is triggered twice as expected, first for the parent and then for the child. I tried to access current.parent when the child was called, but it has no such property. Is there any way to access it?
UPDATE
I saw these questions:
get parent state from ui-router $stateChangeStart,
get parent state from ui-router $stateChangeStart
But none of them had a clear answer to this problem (except the state splitting hack in the latter one but I'd rather avoid that).
The both ways are hacks a bit.
Don't forget to import $state (and $stateParams for 2nd variant) providers in your controller.
You can use:
$state.$current.parent.self.title
or
var tree = current.name.split('.');
tree.splice(tree.length-1, 1);
var parent = tree.join('.');
$state.get(parent, $stateParams);
It seems that you just need the title property to fall back to the parent's title property if there's no title defined on the child state, I will suggest you to use this repository: angular-ui-router-title.
So finally I came up with the following solution:
I store title inside the state's data object, because from $state you can't access the parent's custom properties, only the ones stored in data (which makes sense - not to pollute the state definition with custom properties).
.state('main.parent', {
url: '/parent',
controller: 'ParentController',
controllerAs: 'vm',
templateUrl: 'app/parent.html',
data: {
title: 'Parent'
}
})
.state('main.parent.child', {
url: '/child',
controller: 'ChildController',
controllerAs: 'vm',
templateUrl: 'app/child.html'
})
And I changed the runblock as follows:
...
$rootScope.$on('$stateChangeSuccess', function (event, toState, fromState) {
var current = $state.$current;
if (current.data.hasOwnProperty('title')) {
$rootScope.title = current.data.title;
} else if(current.parent && current.parent.data.hasOwnProperty('title')) {
$rootScope.title = current.parent.data.title;
} else {
$rootScope.title = null;
}
});
...
This is not the perfect solution yet, as in the case of multiple nested states it'd be nice to fall back to the title of the youngest ancestor's title but it does it for now.
Simply below code is working for me
$state.$current.parent
Looking at the UI-Router documentation in the "to" section, you could do something like below:
$state.go('^');
to go back to the parent state.
I am creating a web app to help students in science, history and math. When you first land on the site I have a home/landing page. When you click get started I route to /exam/instructions. Each of my steps instructions, math and science our templates that I load into the ui-view="exam-detail". Currently the whole ui-view loads when I navigate to and from instructions through sciences. Ideally I simply want an area for pagination and an area for the subject matter and only want the ui-view="exam-detail" to update with the correct template.
I have not used UI-Router at all and any assistance would be greatly appreciated.
index.html
<div ui-view></div>
state-exam>exam.html
<div class="state-exam">
<nav ui-view="exam-pagination"></nav>
<section ui-view="exam-detail"></section>
</div>
route.js
(function() {
'use strict';
angular
.module('studentPortal')
.config(routeConfig);
function routeConfig($stateProvider, $urlRouterProvider) {
$stateProvider
.state('home', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainController',
controllerAs: 'main'
})
.state('exam', {
url: '/exam/:step',
abstract: true,
templateUrl: 'app/state-exam/exam.html',
controller: 'ExamController',
controllerAs: 'examController',
})
.state('exam.instructions', {
url: '/instructions',
views: {
'exam-pagination':{
templateUrl: 'app/state-exam/exam-pagination.html'
},
'exam-detail' : {
templateUrl: 'app/state-exam/exam-instructions.html'
}
}
})
.state('exam.math', {
url: '/math',
views: {
'exam-pagination':{
templateUrl: 'app/state-exam/exam-pagination.html'
},
'exam-detail' : {
templateUrl: 'app/state-exam/exam-math.html'
}
}
});
$urlRouterProvider.otherwise('/');
}
})();
There is a working plunker
There is a similar Q & A in fact, with working plunker:
Angular UI Router - Nested States with multiple layouts
Solution here, is to move the static view from child to parent. It won't be reloaded for each child (view is reloaded only if parent state is changed). We will use absolute naming (see included links for more details)
So this is the code adjustment
.state('exam', {
url: '/exam/:step',
abstract: true,
// the root view and the static pagination view
// will be defined here, so we need views : {}
views: {
'':{
templateUrl: 'app/state-exam/exam.html',
controller: 'ExamController',
controllerAs: 'examController',
},
// absolute naming targets the view defined above
'exam-pagination#exam':{
templateUrl: 'app/state-exam/exam-pagination.html'
},
}
})
.state('exam.instructions', {
url: '/instructions',
views: {
// 'exam-pagination':{}, // defined in parent
'exam-detail' : {
templateUrl: 'app/state-exam/exam-instructions.html'
}
}
})
.state('exam.math', {
url: '/math',
views: {
// 'exam-pagination':{}, // defined in parent
'exam-detail' : {
templateUrl: 'app/state-exam/exam-math.html'
}
}
});
Also check this to get more details about absolute view naming
Angular UI router nested views
Angular-UI Router: Nested Views Not Working
The working example is here
I have a simple angularjs controller which uses jquery that logs something to the console when mouse goes over an anchor element:
app.controller('MenuController', function() {
$("a").on('mouseover', function (e) {
console.log("mouser over a link");
});
});
I am using ui-router for organizing my app states:
app.config(["$urlRouterProvider", "$stateProvider", function($urlRouterProvider, $stateProvider) {
// For any unmatched url, redirect
$urlRouterProvider
.otherwise("/");
$stateProvider
.state('menu', {
controller: "MenuController",
controllerAs: "menuCtrl",
templateUrl: "partials/menu.html"
})
.state('menu.menu', {
url: '/',
templateUrl: "partials/menu.menu.html"
})
.state('menu.difficulty', {
url: '/difficulty',
templateUrl: "partials/menu.difficulty.html",
controller: "DifficultyController",
controllerAs: "difCtrl"
})
.state('menu.settings', {
url: "/settings",
templateUrl: "partials/menu.settings.html"
})
}]);
My basic html for the menu is in the menu.html file:
<!-- view - menu -->
<div ui-view>
<!-- nested views -->
</div>
Inside here a bunch of nested views get inserted through states. These views have a lot of anchor elements yet nothing happens when mouse goes over them. Why is that so? Shouldn't parent state controller expand on to child states? Thanks for the help!
The data which is bound to $scope in parent controller is accessible in the child states because the parent controller always runs if we access the child state.
In Menu Controller,If write write
$scope.name = 'XYZ'
This $scope.name is accessible in every child controller using $scope.name.
EDIT:
In your MenuController bind this anchor on document like this and It will work
app.controller('MenuController', function() {
$(document).on('mouseover','a', function (e) {
console.log("mouser over a link");
});
});
I'm getting this error:
Error: Error: [$injector:unpr] http://errors.angularjs.org/1.3.7/$injector/unpr?p0=HttpResponseProvider%20%3C-%20HttpResponse%20%3C-%20DealerLeads
Injector Unknown provider
Here's my router (ui.router):
$stateProvider
.state('main', {
url: "/main",
templateUrl: "views/main.html",
data: { pageTitle: 'Main Page' }
})
.state('leads', {
url: "/leads",
templateUrl: "views/leads.html",
data: { pageTitle: 'Dealer Leads' },
controller: 'DealerLeads',
resolve: DealerLeads.resolve
})
Here's my Controller:
function DealerLeads($scope, HttpResponse) {
alert(JSON.stringify(HttpResponse));
}
Here's my resolve:
DealerLeads.resolve = {
HttpResponse: function ($http) {
...
}
}
The data is getting to the controller, I see it in the alert. However, after the controller is done, during the rendering of the view (I think), the issue seems to be happening.
The final rendered view has two controllers: One main controller in the body tag, and the second controller 'DealerLeads' inside of that. I've tried removing the main controller, and the issue is still present.
What am I doing wrong? Is there any more code that is necessary to understand/resolve the issue?
When you use route resolve argument as dependency injection in the controller bound to the route, you cannot use that controller with ng-controller directive because the service provider with the name HttpResponse does not exist. It is a dynamic dependency that is injected by the router when it instantiates the controller to be bound in its respective partial view.
Just remove the ng-controller="DealerLeads" from the view and make sure that view is part of the html rendered by the state leads # templateUrl: "views/leads.html",. Router will bind it to the the template for you resolving the dynamic dependency HttpResponse. If you want to use controllerAs you can specify that in the router itself as:-
controller: 'DealerLeads',
controllerAs: 'leads' //Not sure if this is supported by ui router yet
or
controller: 'DealerLeads as leads',
Also when you do:
.state('leads', {
url: "/leads",
templateUrl: "views/leads.html",
data: { pageTitle: 'Dealer Leads' },
controller: 'DealerLeads',
resolve: DealerLeads.resolve
})
make sure that DealerLeads is accessible at the place where the route is defined. It would be a better practice to move the route definition to its own controller file so that they are all in one place. And whenever possible especially in a partial view of a route it is better to get rid of ng-controller starting the directive and instead use route to instantiate and bind the controller for that template. It gives more re-usability in terms of the view as a whole not being tightly coupled with a controller name and instead only with its contract. So i would not worry about removing ng-controller directive where router can instantiate the controller.
I'm not completely understand you question, and also not an expert as #PSL in angular.
If you just want to pass some data into the controller, maybe below code can help you.
I copied a piece of code from the project:
.state('masthead.loadTests.test',{
url: '/loadTests/:id',
templateUrl: 'load-tests/templates/load-test-entity.tpl.html',
controller: 'loadTestEntityCtrl',
data: { pageTitle: 'loadTests',
csh: '1005'
},
resolve: {
// Get test entity data before enter to the page (need to know running state)
LoadTestEntityData: [
'$stateParams',
'$state',
'LoadTestEntity',
'LoggerService',
'$rootScope',
function ($stateParams, $state, LoadTestEntity, LoggerService, $rootScope) {
// Get general data
return LoadTestEntity.get({id: $stateParams.id},function () {
},
// Fail
function () {
// When error navigate to homepage
LoggerService.error('error during test initiation');
$state.go('masthead.loadTests.list', {TENANTID: $rootScope.session.tenantId});
}).$promise;
}
]
}
})
Here the LoadTestEntityData is the data we injected into the controller,
LoadTestEntity and LoggerService are services needed for building the data.
.factory('LoadTestEntity', ['$resource', function ($resource) {
return $resource(
'/api/xxx/:id',
{id: '#id'},
{
create: {method: 'POST'},
update: {method: 'PUT'}
}
);
}])
Hope it helps!
I currently have an AngularJS application with routing built in.
It works and everything is ok.
My app.js file looks like this:
angular.module('myapp', ['myapp.filters', 'myapp.services', 'myapp.directives']).
config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', { templateUrl: '/pages/home.html', controller: HomeController });
$routeProvider.when('/about', { templateUrl: '/pages/about.html', controller: AboutController });
$routeProvider.when('/privacy', { templateUrl: '/pages/privacy.html', controller: AboutController });
$routeProvider.when('/terms', { templateUrl: '/pages/terms.html', controller: AboutController });
$routeProvider.otherwise({ redirectTo: '/' });
}]);
My app has a CMS built in where you can copy and add new html files within the /pages directory.
I would like to still go through the routing provider though even for the new dynamically added files.
In an ideal world the routing pattern would be:
$routeProvider.when('/pagename', { templateUrl: '/pages/pagename.html', controller: CMSController });
So if my new page name was "contact.html" I would like angular to pick up "/contact" and redirect to "/pages/contact.html".
Is this even possible?! and if so how?!
Update
I now have this in my routing config:
$routeProvider.when('/page/:name', { templateUrl: '/pages/home.html', controller: CMSController })
and in my CMSController:
function CMSController($scope, $route, $routeParams) {
$route.current.templateUrl = '/pages/' + $routeParams.name + ".html";
alert($route.current.templateUrl);
}
CMSController.$inject = ['$scope', '$route', '$routeParams'];
This sets the current templateUrl to the right value.
However I would now like to change the ng-view with the new templateUrl value. How is this accomplished?
angular.module('myapp', ['myapp.filters', 'myapp.services', 'myapp.directives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/page/:name*', {
templateUrl: function(urlattr){
return '/pages/' + urlattr.name + '.html';
},
controller: 'CMSController'
});
}
]);
Adding * let you work with multiple levels of directories dynamically.
Example: /page/cars/selling/list will be catch on this provider
From the docs (1.3.0):
"If templateUrl is a function, it will be called with the following
parameters:
{Array.} - route parameters extracted from the current
$location.path() by applying the current route"
Also
when(path, route) : Method
path can contain named groups starting with a colon and ending with a star: e.g.:name*. All characters are eagerly stored in $routeParams under the given name when the route matches.
Ok solved it.
Added the solution to GitHub - http://gregorypratt.github.com/AngularDynamicRouting
In my app.js routing config:
$routeProvider.when('/pages/:name', {
templateUrl: '/pages/home.html',
controller: CMSController
});
Then in my CMS controller:
function CMSController($scope, $route, $routeParams) {
$route.current.templateUrl = '/pages/' + $routeParams.name + ".html";
$.get($route.current.templateUrl, function (data) {
$scope.$apply(function () {
$('#views').html($compile(data)($scope));
});
});
...
}
CMSController.$inject = ['$scope', '$route', '$routeParams'];
With #views being my <div id="views" ng-view></div>
So now it works with standard routing and dynamic routing.
To test it I copied about.html called it portfolio.html, changed some of it's contents and entered /#/pages/portfolio into my browser and hey presto portfolio.html was displayed....
Updated
Added $apply and $compile to the html so that dynamic content can be injected.
I think the easiest way to do such thing is to resolve the routes later, you could ask the routes via json, for example. Check out that I make a factory out of the $routeProvider during config phase, via $provide, so I can keep using the $routeProvider object in the run phase, and even in controllers.
'use strict';
angular.module('myapp', []).config(function($provide, $routeProvider) {
$provide.factory('$routeProvider', function () {
return $routeProvider;
});
}).run(function($routeProvider, $http) {
$routeProvider.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
}).otherwise({
redirectTo: '/'
});
$http.get('/dynamic-routes.json').success(function(data) {
$routeProvider.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
});
// you might need to call $route.reload() if the route changed
$route.reload();
});
});
In the $routeProvider URI patters, you can specify variable parameters, like so: $routeProvider.when('/page/:pageNumber' ... , and access it in your controller via $routeParams.
There is a good example at the end of the $route page: http://docs.angularjs.org/api/ng.$route
EDIT (for the edited question):
The routing system is unfortunately very limited - there is a lot of discussion on this topic, and some solutions have been proposed, namely via creating multiple named views, etc.. But right now, the ngView directive serves only ONE view per route, on a one-to-one basis. You can go about this in multiple ways - the simpler one would be to use the view's template as a loader, with a <ng-include src="myTemplateUrl"></ng-include> tag in it ($scope.myTemplateUrl would be created in the controller).
I use a more complex (but cleaner, for larger and more complicated problems) solution, basically skipping the $route service altogether, that is detailed here:
http://www.bennadel.com/blog/2420-Mapping-AngularJS-Routes-Onto-URL-Parameters-And-Client-Side-Events.htm
Not sure why this works but dynamic (or wildcard if you prefer) routes are possible in angular 1.2.0-rc.2...
http://code.angularjs.org/1.2.0-rc.2/angular.min.js
http://code.angularjs.org/1.2.0-rc.2/angular-route.min.js
angular.module('yadda', [
'ngRoute'
]).
config(function ($routeProvider, $locationProvider) {
$routeProvider.
when('/:a', {
template: '<div ng-include="templateUrl">Loading...</div>',
controller: 'DynamicController'
}).
controller('DynamicController', function ($scope, $routeParams) {
console.log($routeParams);
$scope.templateUrl = 'partials/' + $routeParams.a;
}).
example.com/foo -> loads "foo" partial
example.com/bar-> loads "bar" partial
No need for any adjustments in the ng-view. The '/:a' case is the only variable I have found that will acheive this.. '/:foo' does not work unless your partials are all foo1, foo2, etc... '/:a' works with any partial name.
All values fire the dynamic controller - so there is no "otherwise" but, I think it is what you're looking for in a dynamic or wildcard routing scenario..
As of AngularJS 1.1.3, you can now do exactly what you want using the new catch-all parameter.
https://github.com/angular/angular.js/commit/7eafbb98c64c0dc079d7d3ec589f1270b7f6fea5
From the commit:
This allows routeProvider to accept parameters that matches
substrings even when they contain slashes if they are prefixed
with an asterisk instead of a colon.
For example, routes like edit/color/:color/largecode/*largecode
will match with something like this
http://appdomain.com/edit/color/brown/largecode/code/with/slashs.
I have tested it out myself (using 1.1.5) and it works great. Just keep in mind that each new URL will reload your controller, so to keep any kind of state, you may need to use a custom service.
Here is another solution that works good.
(function() {
'use strict';
angular.module('cms').config(route);
route.$inject = ['$routeProvider'];
function route($routeProvider) {
$routeProvider
.when('/:section', {
templateUrl: buildPath
})
.when('/:section/:page', {
templateUrl: buildPath
})
.when('/:section/:page/:task', {
templateUrl: buildPath
});
}
function buildPath(path) {
var layout = 'layout';
angular.forEach(path, function(value) {
value = value.charAt(0).toUpperCase() + value.substring(1);
layout += value;
});
layout += '.tpl';
return 'client/app/layouts/' + layout;
}
})();