201 lines
8.5 KiB
Markdown
201 lines
8.5 KiB
Markdown
# uiBreadcrumbs Directive
|
|
|
|
This is a directive that auto-generates breadcrumbs based on angular-ui-router routes.
|
|
|
|
## Demo
|
|
|
|
You can see a working demo demonstrating most of the features here: [http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview](http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview)
|
|
|
|
## Requirements
|
|
|
|
This directive is designed to work with [angular-ui-router](https://github.com/angular-ui/ui-router), and will not work with the default Angular router.
|
|
|
|
The design of the directive also relies on the use of nested states in order to auto-generate the breadcrumbs hierarchy. See more on nested states here:
|
|
[https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-custom-data](https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-custom-data)
|
|
Note that the use of nested states does not imply nested *views*. Often, in a usual breadcrumbs use case, you won't want to have to nest a new view each time you go down the breadcrumb trail. To avoid using
|
|
nested views, you should use a named view and refer to it when configuring your states. See the [demo](http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview) and the example below for an idea of how this would work.
|
|
|
|
## Installation
|
|
|
|
You can install with Bower:
|
|
|
|
`bower install angular-utils-ui-breadcrumbs`
|
|
|
|
Alternatively just download the files `uiBreadcrumbs.js` and `uiBreadcrumbs.tpl.html`. Using bower has the advantage of making version management easier.
|
|
|
|
## Usage
|
|
|
|
Assuming you already have your app configured to use ui-router, you then need to put this directive somewhere and use it like so:
|
|
|
|
```HTML
|
|
<ui-breadcrumbs displayname-property="data.displayName"
|
|
[template-url=""]
|
|
[abstract-proxy-property=""]>
|
|
</ui-breadcrumbs>
|
|
```
|
|
|
|
* **`displayname-property`** (required) This attribute must point to some property on your state config object that contains the string you wish to display as the
|
|
route's breadcrumb. If none is specified, or if the specified property is not found, the directive will default to displaying the route's name.
|
|
* **`template-url`** (optional) Use this attribute to specify the URL of the `uiBreadcrumbs.tpl.html` file. Alternatively this may be configured in the JavaScript file
|
|
itself, in which case this attribute would not be needed.
|
|
* **`abstract-proxy-property`** (optional) Used when working with abstract states. See the section on working with abstract states below for a full explanation.
|
|
|
|
## Example setup
|
|
|
|
He is an example that illustrates the main features of the directive:
|
|
|
|
```JavaScript
|
|
angular.module('yourModule').config(function($stateProvider) {
|
|
$stateProvider
|
|
.state('home', {
|
|
url: '/',
|
|
views: {
|
|
'content@': {
|
|
templateUrl: ...
|
|
}
|
|
},
|
|
data: {
|
|
displayName: 'Home'
|
|
}
|
|
})
|
|
.state('home.usersList', {
|
|
url: 'users/',
|
|
views: {
|
|
'content@': {
|
|
templateUrl: ...
|
|
}
|
|
},
|
|
data: {
|
|
displayName: 'Users'
|
|
}
|
|
})
|
|
.state('home.userList.detail', {
|
|
url: ':id',
|
|
views: {
|
|
'content@': {
|
|
templateUrl: ...
|
|
}
|
|
},
|
|
data: {
|
|
displayName: '{{ user.firstName }} {{ user.lastName | uppercase }}'
|
|
}
|
|
resolve: {
|
|
user : function($stateParams, userService) {
|
|
return userService.getUser($stateParams.id);
|
|
}
|
|
}
|
|
})
|
|
.state('home.userList.detail.image', {
|
|
views: {
|
|
'content@': {
|
|
templateUrl: ...
|
|
}
|
|
},
|
|
data: {
|
|
displayName: false
|
|
}
|
|
})
|
|
```
|
|
|
|
```html
|
|
// in the app template somewhere
|
|
<ui-breadcrumbs displayname-property="data.displayName"></ui-breadcrumbs>
|
|
<div ui-view="content"></div>
|
|
```
|
|
|
|
The first two states are straightforward. The property specified in the `displayname-property` attribute can be seen
|
|
to exist in the config object, the value of which is a string with the name we want to display in the breadcrumb.
|
|
|
|
The third state illustrates how we can use a resolved value as our breadcrumb display name. This is done by using the
|
|
regular Angular interpolation syntax `{{ value }}`. In this case, `user` corresponds to the `resolve` function that is using our
|
|
imaginary `userService` to asynchronously grab an object containing the details of the current user. You can also see that, just like in any Angular interpolation
|
|
string, you can reference properties of objects, use filters and so on.
|
|
|
|
The fourth state illustrates that if we don't want a state to show up in the breadcrumbs, we should set the
|
|
display name to `false`.
|
|
|
|
## Working With Abstract States
|
|
|
|
AngularUI Router provides the option of setting up [abstract states](https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#abstract-states), which
|
|
by definition cannot be transitioned to. Therefore, we cannot show them in the breadcrumbs, as clicking on an abstract state would cause the router to try
|
|
to transition to that state, which results in an exception.
|
|
|
|
Therefore, the default behaviour is to ignore abstract states and skip that level of the breadcrumb hierarchy.
|
|
|
|
However, in many cases this behaviour would not be desirable. Consider the following setup (taken from the ui-router docs):
|
|
|
|
```JavaScript
|
|
$stateProvider
|
|
.state('contacts', {
|
|
abstract: true,
|
|
url: '/contacts',
|
|
|
|
// Note: abstract still needs a ui-view for its children to populate.
|
|
// You can simply add it inline here.
|
|
template: '<ui-view/>'
|
|
})
|
|
.state('contacts.list', {
|
|
// url will become '/contacts/list'
|
|
url: '/list'
|
|
//...more
|
|
})
|
|
.state('contacts.detail', {
|
|
// url will become '/contacts/detail'
|
|
url: '/detail',
|
|
//...more
|
|
})
|
|
```
|
|
|
|
In this case, if we were in the `contacts.detail` state, the breadcrumbs would only display that state and ignore the parent state as it
|
|
is abstract. What we really want to do is substitute the `contacts.list` state for the parent state. This is because, logically, the
|
|
list of contacts is one level up from the detail page, even though they are strictly at the same level in the $state definition.
|
|
|
|
In order to achieve this substitution, we can use the `abstract-proxy-property` attribute on our directive. This tells the directive
|
|
to look for the specified property on the state config object, where it should find the name of the state to use instead of the abstract state.
|
|
|
|
To implement this, we would modify the above example to look like this:
|
|
|
|
```JavaScript
|
|
$stateProvider
|
|
.state('contacts', {
|
|
abstract: true,
|
|
url: '/contacts',
|
|
template: '<ui-view/>'
|
|
data: {
|
|
breadcrumbProxy: 'contacts.list'
|
|
}
|
|
})
|
|
.state('contacts.list', {
|
|
url: '/list'
|
|
//...more
|
|
})
|
|
.state('contacts.detail', {
|
|
url: '/detail',
|
|
//...more
|
|
})
|
|
```
|
|
|
|
The directive element would then look like this:
|
|
|
|
```HTML
|
|
<ui-breadcrumbs displayname-property="data.displayName" abstract-proxy-property="data.breadcrumbProxy"></ui-breadcrumbs>
|
|
```
|
|
|
|
Now, when we are in the `contacts.detail` state, the breadcrumbs will show the `contacts.list` as the immediate parent,
|
|
rather than the abstract `contacts` state.
|
|
|
|
## Styling
|
|
The template structure is based on the [Bootstrap 3 breadcrumbs component](http://getbootstrap.com/components/#breadcrumbs), so it
|
|
includes an `active` class to signify the current (left-most) item in the breadcrumbs list. You can, of course, modify the template as needed
|
|
or simply define your own CSS to fit it with your app's style.
|
|
|
|
## Note on the `data` object
|
|
A potential "gotcha" is the fact that the `data` object gets inherited by child states ([see docs here](https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-custom-data)).
|
|
If you want to avoid this behaviour, don't use the `data` object to define your breadcrumb's `displayname-property`. Use another property on the `.state()` config object instead.
|
|
|
|
## Credits
|
|
I used some ideas and approaches from the following sources:
|
|
|
|
- [http://stackoverflow.com/a/22263990/772859](http://stackoverflow.com/a/22263990/772859)
|
|
- [http://milestone.topics.it/2014/03/angularjs-ui-router-and-breadcrumbs.html](http://milestone.topics.it/2014/03/angularjs-ui-router-and-breadcrumbs.html)
|