One key component to web applications is breadcrumbs/navigation. With Angular UI Router, it would make sense to put the breadcrumb metadata with the individual states, rather than in your controllers. Manually creating the breadcrumbs object for each controller where it's needed is a straight-forward task, but it's also a very messy one.
I have seen some solutions for automated Breadcrumbs with Angular, but to be honest, they are rather primitive. Some states, like dialog boxes or side panels should not update the breadcrumbs, but with current addons to angular, there is no way to express that.
Another problem is that titles of breadcrumbs are not static. For example, if you go to a User Detail page, the breadcrumb title should probably be the user's Full Name, and not a generic "User Detail".
The last problem that needs to be solved is using all of the correct state parameter values for parent links. For example, if you're looking at a User detail page from a Company, obviously you'll want to know that the parent state requires a :companyId
.
Are there any addons to angular that provide this level of breadcrumbs support? If not, what is the best way to go about it? I don't want to clutter up my controllers - I will have a lot of them - and I want to make it as automated and painless as possible.
Thanks!
I did solve this myself awhile back, because nothing was available. I decided to not use the data
object, because we don't actually want our breadcrumb titles to be inherited by children. Sometimes there are modal dialogs and right panels that slide in that are technically "children views", but they shouldn't affect the breadcrumb. By using a breadcrumb
object instead, we can avoid the automatic inheritance.
For the actual title
property, I am using $interpolate
. We can combine our breadcrumb data with the resolve scope without having to do resolves in a different place. In all of the cases I had, I just wanted to use the resolve scope anyway, so this works very well.
My solution also handles i18n too.
$stateProvider .state('courses', { url: '/courses', template: Templates.viewsContainer(), controller: function(Translation) { Translation.load('courses'); }, breadcrumb: { title: 'COURSES.TITLE' } }) .state('courses.list', { url: "/list", templateUrl: 'app/courses/courses.list.html', resolve: { coursesData: function(Model) { return Model.getAll('/courses'); } }, controller: 'CoursesController' }) // this child is just a slide-out view to add/edit the selected course. // It should not add to the breadcrumb - it's technically the same screen. .state('courses.list.edit', { url: "/:courseId/edit", templateUrl: 'app/courses/courses.list.edit.html', resolve: { course: function(Model, $stateParams) { return Model.getOne("/courses", $stateParams.courseId); } }, controller: 'CourseFormController' }) // this is a brand new screen, so it should change the breadcrumb .state('courses.detail', { url: '/:courseId', templateUrl: 'app/courses/courses.detail.html', controller: 'CourseDetailController', resolve: { course: function(Model, $stateParams) { return Model.getOne('/courses', $stateParams.courseId); } }, breadcrumb: { title: '{{course.name}}' } }) // lots more screens.
I didn't want to tie the breadcrumbs to a directive, because I thought there might be multiple ways of showing the breadcrumb visually in my application. So, I put it into a service:
.factory("Breadcrumbs", function($state, $translate, $interpolate) { var list = [], title; function getProperty(object, path) { function index(obj, i) { return obj[i]; } return path.split('.').reduce(index, object); } function addBreadcrumb(title, state) { list.push({ title: title, state: state }); } function generateBreadcrumbs(state) { if(angular.isDefined(state.parent)) { generateBreadcrumbs(state.parent); } if(angular.isDefined(state.breadcrumb)) { if(angular.isDefined(state.breadcrumb.title)) { addBreadcrumb($interpolate(state.breadcrumb.title)(state.locals.globals), state.name); } } } function appendTitle(translation, index) { var title = translation; if(index < list.length - 1) { title += ' > '; } return title; } function generateTitle() { title = ''; angular.forEach(list, function(breadcrumb, index) { $translate(breadcrumb.title).then( function(translation) { title += appendTitle(translation, index); }, function(translation) { title += appendTitle(translation, index); } ); }); } return { generate: function() { list = []; generateBreadcrumbs($state.$current); generateTitle(); }, title: function() { return title; }, list: function() { return list; } }; })
The actual breadcrumb directive then becomes very simple:
.directive("breadcrumbs", function() { return { restrict: 'E', replace: true, priority: 100, templateUrl: 'common/directives/breadcrumbs/breadcrumbs.html' }; });
And the template:
<h2 translate-cloak> <ul class="breadcrumbs"> <li ng-repeat="breadcrumb in Breadcrumbs.list()"> <a ng-if="breadcrumb.state && !$last" ui-sref="{{breadcrumb.state}}">{{breadcrumb.title | translate}}</a> <span class="active" ng-show="$last">{{breadcrumb.title | translate}}</span> <span ng-hide="$last" class="divider"></span> </li> </ul> </h2>
From the screenshot here, you can see it works perfectly in both the navigation:
As well as the html <title>
tag:
PS to Angular UI Team: Please add something like this out of the box!
I'd like to share my solution to this. It has the advantage of not requiring anything to be injected into your controllers, and supports named breadcrumb labels, as well as using resolve:
functions to name your breadcrumbs.
Example state config:
$stateProvider .state('home', { url: '/', ... data: { displayName: 'Home' } }) .state('home.usersList', { url: 'users/', ... data: { displayName: 'Users' } }) .state('home.userList.detail', { url: ':id', ... data: { displayName: '{{ user.name | uppercase }}' } resolve: { user : function($stateParams, userService) { return userService.getUser($stateParams.id); } } })
Then you need to specify the location of the breadcrumb label (displayname) in an attribute on the directive:
<ui-breadcrumbs displayname-property="data.displayName"></ui-breadcrumbs>
In this way, the directive will know to look at the value of $state.$current.data.displayName
to find the text to use.
Notice that in the last state (home.userList.detail
), the displayName uses the usual Angular interpolation syntax {{ value }}
. This allows you to reference any values defined in the resolve
object in the state config. Typically this would be used to get data from the server, as in the example above of the user name. Note that, since this is just a regular Angular string, you can include any type of valid Angular expression in the displayName field - as in the above example where we are applying a filter to it.
Here is a working demo on Plunker: http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview
I thought it was a bit much to put all the code here, so here it is on GitHub: https://github.com/michaelbromley/angularUtils/tree/master/src/directives/uiBreadcrumbs
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With