35

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!

egervari
  • 22,372
  • 32
  • 121
  • 175
  • Having the exact same problem. Trying to figure out a good way to do this. It's annoying that the 'data' object for the state is not more like the 'resolve' object. Would be fun to have access to dependency injected stuff in the state data. – Nailer Mar 07 '14 at 15:05

6 Answers6

41

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:

enter image description here

As well as the html <title> tag:

enter image description here

PS to Angular UI Team: Please add something like this out of the box!

egervari
  • 22,372
  • 32
  • 121
  • 175
  • Abstract states seem to break this, or I am doing something wrong :) – swestner Apr 04 '14 at 20:13
  • 1
    So in the end this did end up working. In case any one else is having issues, I needed to add a call to generate in the linking function of the directive, and make sure that the directive existed somewhere within the scope of the state. After that smooth sailing...even handles abstract states :) Any ways +1 for the solution, especially adding the interpolation :) – swestner Apr 07 '14 at 14:29
  • Note that this solution requires a hard-to-find dependency regarding the translation service. The original post could be improved to make that explicit. – CSSian Apr 17 '14 at 19:32
  • hint: use $locationChangeSuccess handler ($stateChangeSuccess for ui-router) to call the generate function – user2847643 Apr 28 '14 at 14:14
  • @swestner can you please share your code somewhere, i am having same issue but something is really missing... – sufyan.shoaib Nov 02 '14 at 07:20
  • 1
    @sufyan.shoaib I might have made some additional changes, it was a while ago and i forget. But there is a gist of what it looked like [here](https://gist.github.com/swestner/82a29c42d6ef6efa6c0a). – swestner Nov 06 '14 at 04:31
  • I tried this solution, but I can't make it work. I have no console errors. I splitted up the directive, service and controller to 3 different files, made my breadcrumb.html template, and added to my state a template field containing the title. In the debugger, how can look for the entry point ? Do I need to only add a breadcrumb object to my state in order to ask it to use the directive ? – Alex Oct 20 '15 at 15:29
  • @Alex make sure you're calling the BreadcrumbService.generate() method in your controller. That will kick everything off. – TALLBOY Mar 18 '16 at 19:25
  • @Alex you could also inject the service into the controller property of the directive and do some generation when the directive is instantiated. `controller: ['$scope', 'BreadcrumbService', function ($scope, BreadcrumbService) { BreadcrumbService.generate(); $scope.breadcrumbList = BreadcrumbService.getList(); }]` – TALLBOY Mar 18 '16 at 20:17
  • @TALLBOY Aww sorry but I used Ncy breadcrumb angular plugin in order to make my breadcrumbs. So I will not be able to tell you the result. For info, NcyBreadcrumb works well, allow easy translation and easy integration. – Alex Mar 21 '16 at 12:38
  • 1
    Please could we have a demo of this beauty or a link to an example? – Chips147 Feb 10 '17 at 12:15
25

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.

$interpolate-able breadcrumb names

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.

Demo

Here is a working demo on Plunker: http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview

Code

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

Michael Bromley
  • 4,792
  • 4
  • 35
  • 57
  • @michael m getting this error "Error: [$compile:tpload] Failed to load template: uiBreadcrumbs.tpl.html at breadcrumb.js i ve change template url to -->bower_components/angular-utils-ui-breadcrumbs/uiBreadcrumbs.tpl.html.. but still its showing the same error... please do suggest me some idea... – Bijay Rai Nov 13 '14 at 04:50
  • 1
    @BijayRai Well, sounds like something is off with the template path.. if you can't fix it, open an issue and include your code: https://github.com/michaelbromley/angularUtils/issues – Michael Bromley Nov 13 '14 at 08:42
  • @michael so sorry there was simple spelling mistake... :) – Bijay Rai Nov 17 '14 at 06:41
  • 1
    @michael it would be great help if there is some guide or tips for making dropdown within breadcrumb.. :) – Bijay Rai Nov 17 '14 at 06:43
  • @BijayRai Yeah, but that is out of the scope of the question. It does not sound like a trivial change. Why don't you fork my repo and have a go at it yourself - it's not one of my requirements in my project so I'm afraid I won't be working to implement something like this myself. – Michael Bromley Nov 18 '14 at 09:49
  • @MichaelBromley - thanks for this. I installed your directive using bower and I am having an issue getting it injected into my app. `Uncaught Error: [$injector:modulerr]` and the important part: `0%3DangularUtils.directives.uiBreadcrumbs%26p1`.. is this the proper injection? `'ui.router','angularUtils.directives.uiBreadcrumbs',` ? – Stephen Nielsen Jul 16 '15 at 17:40
  • @StephenNielsen Hi, I think it's better if we take this to GitHub - the comments system here is not a good place for support-type discussions. https://github.com/michaelbromley/angularUtils/issues – Michael Bromley Jul 17 '15 at 07:20
  • has anybody added dropdown feature? in a way that it adds dropdown items based on a query, not state values. – nurp Jan 15 '16 at 17:11
4

I made a Angular module which generate a breadcrumb based on ui-router's states. All the features you speak about are included (I recently add the possibility to ignore a state in the breadcrumb while reading this post :-) ) :

Here is the github repo

It allows dynamic labels interpolated against the controller scope (the "deepest" in case of nested/multiple views).

The chain of states is customizable by state options (See API reference)

The module comes with pre-defined templates and allows user-defined templates.

ncuillery
  • 14,308
  • 2
  • 22
  • 31
0

I do not believe there is built in functionality, but all the tools are there for you, take a look at the LocationProvider. You could simply have navigation elements use this and whatever else you want to know just inject it.

Documentation

Gent
  • 2,675
  • 1
  • 24
  • 34
  • The documentation for this is basically non-existent. I am also using Angular UI Router, not ngRoute provided by Angular JS. – egervari Feb 23 '14 at 19:01
  • added the link to the documentation for you. Also looking through Angular UI Router, it appears that location is used in conjunction with Router, in their examples. – Gent Feb 23 '14 at 19:08
  • Thank you both for the feedback. I agree it is a weak answer given the thoroughness of the OPs answer. However, I was simply trying to steer the OP in a different direction. His solution looks solid and I have even bookmarked it for the next time I try to do something similar. However, I am not convinced that this sort of suggestion should be relegated to a comment. I would appreciate some clarification as to why it should have been a comment. – Gent Sep 17 '14 at 20:06
0

After digging deep into the internals of ui-router I understood how I could create a breadcrumb using resolved resources.

Here is a plunker to my directive.

NOTE: I couldn't get this code to work properly within the plunker, but the directive works in my project. routes.js is provided merely for example of how to you can set titles for your breadcrumbs.

Nailer
  • 2,446
  • 1
  • 24
  • 34
0

Thanks for the solution provided by @egervari. For those who need add some $stateParams properties into custom data of breadcrumbs. I've extended the syntax {:id} for the value of key 'title'.

.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 {:courseId}'
    }
})

Here is an Plunker example. FYI.

Indiana
  • 313
  • 4
  • 8