diff --git a/.travis.yml b/.travis.yml
index 8bba685..96d9d98 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: node_js
node_js:
- "8.9.0"
before_install:
- - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
+ - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.5.1
- export PATH="$HOME/.yarn/bin:$PATH"
script:
- yarn run travis
diff --git a/MIGRATIONS.md b/MIGRATIONS.md
new file mode 100644
index 0000000..eced28c
--- /dev/null
+++ b/MIGRATIONS.md
@@ -0,0 +1,52 @@
+## Migrating from 1.x.x -> 2.x.x
+
+> 1.) `withBreadcrumbs` is now the default export
+
+_1.x.x_
+```js
+import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc';
+```
+
+_2.x.x_
+```js
+import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
+```
+
+> 2.) The breadcrumbs array returned by the HOC is now _just_ the components. It _used_ to be an array of objects, but I decided this approach was easier to understand and made the implementation code a bit cleaner.
+
+_1.x.x_
+```js
+{breadcrumbs.map(({ breadcrumb, path, match }) => (
+
+
+ {breadcrumb}
+
+
+))}
+```
+
+_2.x.x_
+```js
+{breadcrumbs.map(breadcrumb => (
+
+
+ {breadcrumb}
+
+
+))}
+```
+
+> 3.) The package will now attempt to return sensible defaults for breadcrumbs unless otherwise provided making the the package now "opt-out" instead of "opt-in" for all paths. See the readme for how to disable default breadcrumb behavior.
+
+_1.x.x_
+```js
+withBreadcrumbs([
+ { path: '/', breadcrumb: 'Home' },
+ { path: '/users', breadcrumb: 'Users' },
+])(Component);
+```
+
+_2.x.x_ (the above breadcrumbs will be automagically generated so there's no need to include them in config)
+```js
+withBreadcrumbs()(Component);
+```
diff --git a/README.md b/README.md
index b4b5d71..d1e3b79 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
- Just a tiny, flexible, higher order component for rendering breadcrumbs with react-router 4.x
+ A tiny (~2kb minified), flexible, higher order component for rendering breadcrumbs with react-router 4.x
- site.com/user/id → user / John Doe
+ site.com/user/id → Home / User / John Doe
@@ -34,35 +34,36 @@ or
## Usage
```js
-withBreadcrumbs(routeConfigObject)(MyComponent);
+withBreadcrumbs()(MyComponent);
```
-## Example
+## Simple example
```js
import React from 'react';
import { NavLink } from 'react-router-dom';
-import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc';
+import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
+// breadcrumbs can be any type of component or string
const UserBreadcrumb = ({ match }) =>
{match.params.userId} ; // use match param userId to fetch/display user name
+// define some custom breadcrumbs for certain routes (optional)
const routes = [
- { path: '/', breadcrumb: 'Home' },
- { path: '/users', breadcrumb: 'Users' },
{ path: '/users/:userId', breadcrumb: UserBreadcrumb },
- { path: '/something-else', breadcrumb: ':)' },
+ { path: '/example', breadcrumb: 'Custom Example' },
];
-// map & render your breadcrumb components however you want
+// map & render your breadcrumb components however you want.
+// each `breadcrumb` has the props `key`, `location`, and `match` included!
const Breadcrumbs = ({ breadcrumbs }) => (
- {breadcrumbs.map(({ breadcrumb, path, match }) => (
-
-
+ {breadcrumbs.map((breadcrumb, index) => (
+
+
{breadcrumb}
- /
+ {(index < breadcrumbs.length - 1) && / }
))}
@@ -77,72 +78,79 @@ Pathname | Result
--- | ---
/users | Home / Users
/users/id | Home / Users / John
-/something-else | Home / :)
+/example | Home / Custom Example
+
+## Disabling default breadcrumbs for paths
+
+This package will attempt to create breadcrumbs for you based on the route section via [humanize-string](https://github.com/sindresorhus/humanize-string). For example `/users` will auotmatically create the breadcrumb `"Users"`. There are two ways to disable default breadcrumbs for a path:
+
+1.) Pass `breadcrumb: null` in the routes config
+`{ path: '/a/b', breadcrumb: null }`
+
+2.) Pass an `excludePaths` array in the `options`
+`withBreadcrumbs(routes, { excludePaths: ['/', '/no-breadcrumb/for-this-route'] })`
+
+in your routes array.
## Already using a [route config](https://reacttraining.com/react-router/web/example/route-config) array with react-router?
-Just add a `breadcrumbs` prop to your routes that require breadcrumbs!
+Just add a `breadcrumbs` prop to your routes that require custom breadcrumbs.
-> Note: currently nested `routes` arrays are not supported, but will be soon (see: https://github.com/icd2k3/react-router-breadcrumbs-hoc/issues/24)
+> Note: currently, nested `route`s arrays are _not_ supported, but will be soon (see: https://github.com/icd2k3/react-router-breadcrumbs-hoc/issues/24)
## API
```js
Route = {
path: String
- breadcrumb: String|Function
+ breadcrumb: String|Function? // note: if not provided, a default breadcrumb will be returned
matchOptions?: Object
}
-Breadcrumb = {
- path: String
- match: String
- breadcrumb: Component
+Options = {
+ excludePaths: Array
}
-// react-router's location object: https://reacttraining.com/react-router/web/api/location
-Location = {
- key: String
- pathname: String
- search: String
- hash: String
- state: Object
-}
-
-withBreadcrumbs(routes: Array): HigherOrderComponent
+// if routes are not passed, default breadcrumbs will be returned
+withBreadcrumbs(routes?: Array, options? Object): HigherOrderComponent
// you shouldn't ever really have to use `getBreadcrumbs`, but it's
// exported for convenience if you don't want to use the HOC
-getBreadcrumbs({ routes: Array, location: Location }): Array
+getBreadcrumbs({
+ routes: Array,
+ location: Object, // react-router's location object: https://reacttraining.com/react-router/web/api/location
+ options: Object,
+}): Array
```
-## Order Matters!
+## Order matters!
-Consider the following route config:
+Consider the following route configs:
```js
[
- { path: '/users', breadcrumb: 'Users' },
- { path: '/users/:id', breadcrumb: 'Users - id' },
- { path: '/users/create', breadcrumb: 'Users - create' },
+ { path: '/users/:id', breadcrumb: 'id-breadcrumb' },
+ { path: '/users/create', breadcrumb: 'create-breadcrumb' },
]
-```
-This package acts like a switch statement and matches the first breadcrumb it can find. So, unfortunately, visiting `/users/create` will result in the `Users > Users - id` breadcrumbs instead of the desired `Users > Users - create` breadcrumbs.
+// example.com/users/create = 'id-breadcrumb' (because path: '/users/:id' will match first)
+// example.com/users/123 = 'id-breadcumb'
+```
-To get the right breadcrumbs, simply change the order:
+To fix the issue above, just adjust the order of your routes:
```js
[
- { path: '/users', breadcrumb: 'Users' },
- { path: '/users/create', breadcrumb: 'Users - create' },
- { path: '/users/:id', breadcrumb: 'Users - id' },
+ { path: '/users/create', breadcrumb: 'create-breadcrumb' },
+ { path: '/users/:id', breadcrumb: 'id-breadcrumb' },
]
+
+// example.com/users/create = 'create-breadcrumb' (because path: '/users/create' will match first)
+// example.com/users/123 = 'id-breadcrumb'
```
-Now, `/users/create` will match the create breadcrumb first, and all others will fall through to `/:id`.
+## Using the location object
-## Using the Location Object
React Router's [location](https://reacttraining.com/react-router/web/api/location) object lets you pass `state` property. Using the `state` allows one to update the Breadcrumb to display dynamic info at runtime. Consider this example:
```jsx
diff --git a/jest.config.js b/jest.config.js
index 192c92a..bd31af3 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -16,5 +16,4 @@ module.exports = {
},
},
setupFiles: ['./jest.setup.js'],
- snapshotSerializers: ['enzyme-to-json/serializer'],
};
diff --git a/package.json b/package.json
index 2bd37f4..3f48cf3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-router-breadcrumbs-hoc",
- "version": "1.2.0",
+ "version": "2.0.0",
"description": "Just a tiny, flexible, higher order component for rendering breadcrumbs with react-router 4.x",
"repository": "icd2k3/react-router-breadcrumbs-hoc",
"keywords": [
@@ -19,6 +19,9 @@
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2"
},
+ "dependencies": {
+ "humanize-string": "^1.0.1"
+ },
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.8.0",
@@ -30,11 +33,9 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"coveralls": "^3.0.0",
- "cross-env": "^5.1.3",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.0",
- "enzyme-to-json": "^3.3.0",
- "eslint": "^4.18.1",
+ "eslint": "^4.18.2",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
@@ -45,16 +46,16 @@
"react-dom": "^16.2.0",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
- "rollup": "^0.56.3",
+ "rollup": "^0.56.5",
"rollup-plugin-babel": "^3.0.3",
- "rollup-plugin-commonjs": "^8.2.6",
- "rollup-plugin-node-resolve": "^3.0.2"
+ "rollup-plugin-commonjs": "^9.0.0",
+ "rollup-plugin-node-resolve": "^3.2.0"
},
"scripts": {
"prepublishOnly": "npm run build",
"build": "rollup -c",
- "test": "cross-env NODE_ENV=test jest",
- "travis": "jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
+ "test": "jest",
+ "travis": "yarn run lint && jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"lint": "eslint ./src/**"
}
}
diff --git a/rollup.config.js b/rollup.config.js
index 34d3e4e..c044014 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -13,6 +13,7 @@ const globals = {
const config = {
input: 'src/index.js',
+ exports: 'named',
plugins: [
babel({
exclude: 'node_modules/**',
diff --git a/src/__snapshots__/index.test.js.snap b/src/__snapshots__/index.test.js.snap
deleted file mode 100644
index 130c4ea..0000000
--- a/src/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,606 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`react-router-breadcrumbs-hoc Custom match options Should render empty container 1`] = `
-
-
-
-
-
-
- 1
-
-
- 1
-
-
-
-
-
-
-`;
-
-exports[`react-router-breadcrumbs-hoc No matching routes Should render empty container 1`] = `
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`react-router-breadcrumbs-hoc Valid routes Should render breadcrumb components as expected 1`] = `
-
-
-
- ,
- "match": Object {
- "isExact": true,
- "params": Object {},
- "path": "/1/2",
- "url": "/1/2",
- },
- "path": "/1/2",
- },
- Object {
- "breadcrumb": ,
- "match": Object {
- "isExact": true,
- "params": Object {
- "number": "3",
- },
- "path": "/1/2/:number",
- "url": "/1/2/3",
- },
- "path": "/1/2/:number",
- },
- Object {
- "breadcrumb": ,
- "match": Object {
- "isExact": true,
- "params": Object {
- "number": "3",
- },
- "path": "/1/2/:number/4",
- "url": "/1/2/3/4",
- },
- "path": "/1/2/:number/4",
- },
- ]
- }
- history={
- Object {
- "action": "POP",
- "block": [Function],
- "createHref": [Function],
- "go": [Function],
- "goBack": [Function],
- "goForward": [Function],
- "listen": [Function],
- "location": Object {
- "hash": "",
- "pathname": "/1/2/3/4",
- "search": "",
- },
- "push": [Function],
- "replace": [Function],
- }
- }
- location={
- Object {
- "hash": "",
- "pathname": "/1/2/3/4",
- "search": "",
- }
- }
- match={
- Object {
- "isExact": false,
- "params": Object {},
- "path": "/",
- "url": "/",
- }
- }
- staticContext={Object {}}
- >
-
-
- Home
-
-
- 1
-
-
-
-
- TWO
-
-
-
-
-
-
- 3
-
-
-
-
-
-
-
-
-
- Link
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`react-router-breadcrumbs-hoc When extending react-router config Should render expected breadcrumbs and omit routes that do not require them 1`] = `
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/index.js b/src/index.js
index 4ab3eb5..0b1406c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,61 +1,117 @@
import { createElement } from 'react';
import { matchPath, withRouter } from 'react-router';
+import humanizeString from 'humanize-string';
const DEFAULT_MATCH_OPTIONS = { exact: true };
+const NO_BREADCRUMB = 'NO_BREADCRUMB';
-// if user is passing a function (component) as a breadcrumb, make sure we
-// pass the match object into it. Else just return the string.
-const renderer = ({ breadcrumb, match, location }) => {
+// renders and returns the breadcrumb complete with `match`, `location`, and `key` props
+const render = ({ breadcrumb, match, location }) => {
+ const componentProps = { match, location, key: match.path };
if (typeof breadcrumb === 'function') {
- return createElement(breadcrumb, { match, location });
+ return createElement(breadcrumb, componentProps);
}
- return breadcrumb;
+ return createElement('span', componentProps, breadcrumb);
};
-export const getBreadcrumbs = ({ routes, location }) => {
+// small helper method to get a default `humanize-string` breadcrumb if the
+// user hasn't provided one
+const getDefaultBreadcrumb = ({ pathSection, currentSection, location }) => {
+ const match = matchPath(pathSection, { ...DEFAULT_MATCH_OPTIONS, path: pathSection });
+
+ return render({
+ breadcrumb: humanizeString(currentSection),
+ match,
+ location,
+ });
+};
+
+// loops through the route array (if provided) and returns either
+// a user-provided breadcrumb OR a sensible default via `humanize-string`
+const getBreadcrumb = ({
+ currentSection,
+ excludePaths,
+ location,
+ pathSection,
+ routes,
+}) => {
+ let breadcrumb;
+
+ // check the optional `exludePaths` option in `options` to see if the
+ // current path should not include a breadcrumb
+ if (excludePaths && excludePaths.includes(pathSection)) {
+ return NO_BREADCRUMB;
+ }
+
+ // loop through the route array and see if the user has provided a custom breadcrumb
+ routes.some(({ breadcrumb: userProvidedBreadcrumb, matchOptions, path }) => {
+ if (!path) {
+ throw new Error('withBreadcrumbs: `path` must be provided in every route object');
+ }
+
+ const match = matchPath(pathSection, { ...(matchOptions || DEFAULT_MATCH_OPTIONS), path });
+
+ // if user passed breadcrumb: null OR custom match options to suppress a breadcrumb
+ // we need to know NOT to add it to the matches array
+ // see: `if (breadcrumb !== NO_BREADCRUMB)` below
+ if ((match && userProvidedBreadcrumb === null) || (!match && matchOptions)) {
+ breadcrumb = NO_BREADCRUMB;
+ return true;
+ }
+
+ if (match) {
+ breadcrumb = render({
+ // although we have a match, the user may be passing their react-router config object
+ // which we support. The route config object may not have a `breadcrumb` param specified.
+ // If this is the case, we should provide a default via `humanizeString`
+ breadcrumb: userProvidedBreadcrumb || humanizeString(currentSection),
+ match,
+ location,
+ });
+ return true;
+ }
+ return false;
+ });
+
+ // if there are no breadcrumbs provided in the routes array we return a default breadcrumb instead
+ return breadcrumb
+ || getDefaultBreadcrumb({
+ pathSection,
+ // include a "Home" breadcrumb by default (can be overrode or disabled in config)
+ currentSection: pathSection === '/' ? 'Home' : currentSection,
+ location,
+ });
+};
+
+export const getBreadcrumbs = ({ routes, location, options = {} }) => {
const matches = [];
const { pathname } = location;
pathname
- // remove trailing slash "/" from pathname (avoids multiple of the same match)
+ .split('?')[0]
+ // remove trailing slash "/" from pathname
.replace(/\/$/, '')
// split pathname into sections
.split('/')
// reduce over the sections and find matches from `routes` prop
- .reduce((previous, current) => {
- // combine the last route section with the current
+ .reduce((previousSection, currentSection) => {
+ // combine the last route section with the currentSection
// ex `pathname = /1/2/3 results in match checks for
// `/1`, `/1/2`, `/1/2/3`
- const pathSection = !current ? '/' : `${previous}/${current}`;
-
- let breadcrumbMatch;
-
- routes.some(({ breadcrumb, matchOptions, path }) => {
- if (!path) {
- throw new Error('withBreadcrumbs: `path` must be provided in every route object');
- }
- if (!breadcrumb) {
- return false;
- }
- const match = matchPath(pathSection, { ...(matchOptions || DEFAULT_MATCH_OPTIONS), path });
-
- // if a route match is found ^ break out of the loop with a rendered breadcumb
- // and match object to add to the `matches` array
- if (match) {
- breadcrumbMatch = {
- breadcrumb: renderer({ breadcrumb, match, location }),
- path,
- match,
- };
- return true;
- }
-
- return false;
+ const pathSection = !currentSection ? '/' : `${previousSection}/${currentSection}`;
+
+ const breadcrumb = getBreadcrumb({
+ currentSection,
+ excludePaths: options.excludePaths,
+ location,
+ pathSection,
+ routes,
});
- /* istanbul ignore else */
- if (breadcrumbMatch) {
- matches.push(breadcrumbMatch);
+ // add the breadcrumb to the matches array
+ // unless the user has explicitly passed { path: x, breadcrumb: null } to disable
+ if (breadcrumb !== NO_BREADCRUMB) {
+ matches.push(breadcrumb);
}
return pathSection === '/' ? '' : pathSection;
@@ -64,11 +120,14 @@ export const getBreadcrumbs = ({ routes, location }) => {
return matches;
};
-export const withBreadcrumbs = routes => Component => withRouter(props =>
+const withBreadcrumbs = (routes = [], options) => Component => withRouter(props =>
createElement(Component, {
...props,
breadcrumbs: getBreadcrumbs({
routes,
location: props.location,
+ options,
}),
}));
+
+export default withBreadcrumbs;
diff --git a/src/index.test.js b/src/index.test.js
index ab7ff55..e5106c1 100644
--- a/src/index.test.js
+++ b/src/index.test.js
@@ -3,20 +3,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { mount } from 'enzyme';
-import { StaticRouter as Router } from 'react-router';
+import { MemoryRouter as Router } from 'react-router';
import { NavLink } from 'react-router-dom';
-import { getBreadcrumbs, withBreadcrumbs } from './index';
+import withBreadcrumbs, { getBreadcrumbs } from './index';
const components = {
Breadcrumbs: ({ breadcrumbs }) => (
-
- {breadcrumbs.map(({ breadcrumb, path }, index) => (
- {breadcrumb}
+
+ {breadcrumbs.map((breadcrumb, index) => (
+
+ {breadcrumb}
+ {(index < breadcrumbs.length - 1) && / }
+
))}
-
+
),
BreadcrumbMatchTest: ({ match }) => {match.params.number} ,
BreadcrumbNavLinkTest: ({ match }) => Link ,
+ BreadcrumbLocationTest: ({ location: { state: { isLocationTest } } }) => (
+
+ {isLocationTest ? 'pass' : 'fail'}
+
+ ),
+};
+
+const render = ({
+ options,
+ pathname,
+ routes,
+ state,
+}) => {
+ const Breadcrumbs = withBreadcrumbs(routes, options)(components.Breadcrumbs);
+ const wrapper =
+ mount( );
+
+ return {
+ Breadcrumbs: wrapper.find('.breadcrumbs-container'),
+ wrapper,
+ };
};
const matchShape = {
@@ -27,10 +51,7 @@ const matchShape = {
};
components.Breadcrumbs.propTypes = {
- breadcrumbs: PropTypes.arrayOf(PropTypes.shape({
- match: PropTypes.shape(matchShape).isRequired,
- path: PropTypes.string.isRequired,
- })).isRequired,
+ breadcrumbs: PropTypes.arrayOf(PropTypes.node).isRequired,
};
components.BreadcrumbMatchTest.propTypes = {
@@ -41,91 +62,158 @@ components.BreadcrumbNavLinkTest.propTypes = {
match: PropTypes.shape(matchShape).isRequired,
};
+components.BreadcrumbLocationTest.propTypes = {
+ location: PropTypes.shape({
+ state: PropTypes.shape({
+ isLocationTest: PropTypes.bool.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
describe('react-router-breadcrumbs-hoc', () => {
describe('Valid routes', () => {
- const routes = [
- // test home route
- { path: '/', breadcrumb: 'Home' },
- // test breadcrumb passed as string
- { path: '/1', breadcrumb: '1' },
- // test simple breadcrumb component
- { path: '/1/2', breadcrumb: () => TWO },
- // test advanced breadcrumb component (user can use `match` however they wish)
- { path: '/1/2/:number', breadcrumb: components.BreadcrumbMatchTest },
- // test NavLink wrapped breadcrumb
- { path: '/1/2/:number/4', breadcrumb: components.BreadcrumbNavLinkTest },
- // test a no-match route
- { path: '/no-match', breadcrumb: 'no match' },
- ];
- const routerProps = {
- context: {},
- location: { pathname: '/1/2/3/4' },
- };
-
it('Should render breadcrumb components as expected', () => {
- const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs);
- const wrapper = mount( );
-
- expect(wrapper.find(ComposedComponent)).toMatchSnapshot();
+ const routes = [
+ // test home route
+ { path: '/', breadcrumb: 'Home' },
+ // test breadcrumb passed as string
+ { path: '/1', breadcrumb: 'One' },
+ // test simple breadcrumb component
+ { path: '/1/2', breadcrumb: () => TWO },
+ // test advanced breadcrumb component (user can use `match` however they wish)
+ { path: '/1/2/:number', breadcrumb: components.BreadcrumbMatchTest },
+ // test NavLink wrapped breadcrumb
+ { path: '/1/2/:number/4', breadcrumb: components.BreadcrumbNavLinkTest },
+ // test a no-match route
+ { path: '/no-match', breadcrumb: 'no match' },
+ ];
+ const { Breadcrumbs, wrapper } = render({ pathname: '/1/2/3/4', routes });
+ expect(Breadcrumbs.text()).toBe('Home / One / TWO / 3 / Link');
expect(wrapper.find(NavLink).props().to).toBe('/1/2/3/4');
});
});
- describe('No matching routes', () => {
- const routes = [
- { path: '/1', breadcrumb: '1' },
- ];
- const routerProps = {
- context: {},
- location: { pathname: 'nope' },
- };
-
- it('Should render empty container', () => {
- const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs);
- const wrapper = mount( );
+ describe('Route order', () => {
+ it('Should match the first breadcrumb in route array user/create', () => {
+ const routes = [
+ {
+ path: '/user/create',
+ breadcrumb: 'Add User',
+ },
+ {
+ path: '/user/:id',
+ breadcrumb: '1',
+ },
+ ];
+ const { Breadcrumbs } = render({ pathname: '/user/create', routes });
+ expect(Breadcrumbs.text()).toBe('Home / User / Add User');
+ });
- expect(wrapper.find(ComposedComponent)).toMatchSnapshot();
+ it('Should match the first breadcrumb in route array user/:id', () => {
+ const routes = [
+ {
+ path: '/user/:id',
+ breadcrumb: 'Oops',
+ },
+ {
+ path: '/user/create',
+ breadcrumb: 'Add User',
+ },
+ ];
+ const { Breadcrumbs } = render({ pathname: '/user/create', routes });
+ expect(Breadcrumbs.text()).toBe('Home / User / Oops');
});
});
describe('Custom match options', () => {
- const routes = [
- {
- path: '/1',
- breadcrumb: '1',
- // not recommended, but supported
- matchOptions: { exact: false, strict: true },
- },
- ];
- const routerProps = {
- context: {},
- location: { pathname: '/1/2' },
- };
-
- it('Should render empty container', () => {
- const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs);
- const wrapper = mount( );
-
- expect(wrapper.find(ComposedComponent)).toMatchSnapshot();
+ it('Should allow `strict` rule', () => {
+ const routes = [
+ {
+ path: '/one/',
+ breadcrumb: '1',
+ // not recommended, but supported
+ matchOptions: { exact: false, strict: true },
+ },
+ ];
+ const { Breadcrumbs } = render({ pathname: '/one', routes });
+ expect(Breadcrumbs.text()).toBe('');
});
});
describe('When extending react-router config', () => {
- const routes = [
- { path: '/1', breadcrumb: '1' },
- // no breadcrumb required for this route
- { path: '/2' },
- ];
- const routerProps = {
- context: {},
- location: { pathname: '/2' },
- };
-
- it('Should render expected breadcrumbs and omit routes that do not require them', () => {
- const ComposedComponent = withBreadcrumbs(routes)(components.Breadcrumbs);
- const wrapper = mount( );
-
- expect(wrapper.find(ComposedComponent)).toMatchSnapshot();
+ it('Should render expected breadcrumbs with sensible defaults', () => {
+ const routes = [
+ { path: '/one', breadcrumb: 'OneCustom' },
+ { path: '/one/two' },
+ ];
+ const { Breadcrumbs } = render({ pathname: '/one/two', routes });
+ expect(Breadcrumbs.text()).toBe('Home / OneCustom / Two');
+ });
+ });
+
+ describe('Defaults', () => {
+ describe('No routes array', () => {
+ it('Should automatically render breadcrumbs with default strings', () => {
+ const { Breadcrumbs } = render({ pathname: '/one/two' });
+
+ expect(Breadcrumbs.text()).toBe('Home / One / Two');
+ });
+ });
+
+ describe('Override defaults', () => {
+ it('Should render user-provided breadcrumbs where possible and use defaults otherwise', () => {
+ const routes = [{ path: '/one', breadcrumb: 'Override' }];
+ const { Breadcrumbs } = render({ pathname: '/one/two', routes });
+
+ expect(Breadcrumbs.text()).toBe('Home / Override / Two');
+ });
+ });
+
+ describe('No breadcrumb', () => {
+ it('Should be possible to NOT render a breadcrumb', () => {
+ const routes = [{ path: '/one', breadcrumb: null }];
+ const { Breadcrumbs } = render({ pathname: '/one/two', routes });
+
+ expect(Breadcrumbs.text()).toBe('Home / Two');
+ });
+
+ it('Should be possible to NOT render a "Home" breadcrumb', () => {
+ const routes = [{ path: '/', breadcrumb: null }];
+ const { Breadcrumbs } = render({ pathname: '/one/two', routes });
+
+ expect(Breadcrumbs.text()).toBe('One / Two');
+ });
+ });
+ });
+
+ describe('When using the location object', () => {
+ it('Should be provided in the rendered breadcrumb component', () => {
+ const routes = [{ path: '/one', breadcrumb: components.BreadcrumbLocationTest }];
+ const { Breadcrumbs } = render({ pathname: '/one', state: { isLocationTest: true }, routes });
+ expect(Breadcrumbs.text()).toBe('Home / pass');
+ });
+ });
+
+ describe('When pathname includes query params', () => {
+ it('Should not render query breadcrumb', () => {
+ const { Breadcrumbs } = render({ pathname: '/one?mock=query' });
+ expect(Breadcrumbs.text()).toBe('Home / One');
+ });
+ });
+
+ describe('When pathname includes a trailing slash', () => {
+ it('Should ignore the trailing slash', () => {
+ const { Breadcrumbs } = render({ pathname: '/one/' });
+ expect(Breadcrumbs.text()).toBe('Home / One');
+ });
+ });
+
+ describe('Options', () => {
+ describe('excludePaths', () => {
+ it('Should not return breadcrumbs for specified paths', () => {
+ const { Breadcrumbs } = render({ pathname: '/one/two', options: { excludePaths: ['/', '/one'] } });
+ expect(Breadcrumbs.text()).toBe('Two');
+ });
});
});
diff --git a/yarn.lock b/yarn.lock
index ec4af42..5952640 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -99,13 +99,13 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.0:
+acorn@^5.0.0, acorn@^5.3.0, acorn@^5.4.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.0.tgz#1abb587fbf051f94e3de20e6b26ef910b1828298"
-ajv-keywords@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
ajv@^4.9.1:
version "4.11.8"
@@ -114,7 +114,7 @@ ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.1.0, ajv@^5.3.0:
+ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@@ -123,14 +123,6 @@ ajv@^5.1.0, ajv@^5.3.0:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
-ajv@^6.0.1:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.0.tgz#afac295bbaa0152449e522742e4547c1ae9328d2"
- dependencies:
- fast-deep-equal "^1.0.0"
- fast-json-stable-stringify "^2.0.0"
- json-schema-traverse "^0.3.0"
-
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -1108,10 +1100,14 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
-builtin-modules@^1.0.0, builtin-modules@^1.1.0, builtin-modules@^1.1.1:
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
+
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1313,13 +1309,6 @@ coveralls@^3.0.0:
minimist "^1.2.0"
request "^2.79.0"
-cross-env@^5.1.3:
- version "5.1.3"
- resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7"
- dependencies:
- cross-spawn "^5.1.0"
- is-windows "^1.0.0"
-
cross-spawn@^5.0.1, cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@@ -1550,12 +1539,6 @@ enzyme-adapter-utils@^1.3.0:
object.assign "^4.0.4"
prop-types "^15.6.0"
-enzyme-to-json@^3.3.0:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz#64239dcd417e2fb552f4baa6632de4744b9b5b93"
- dependencies:
- lodash "^4.17.4"
-
enzyme@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
@@ -1693,9 +1676,9 @@ eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
-eslint@^4.18.1:
- version "4.18.1"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.1.tgz#b9138440cb1e98b2f44a0d578c6ecf8eae6150b0"
+eslint@^4.18.2:
+ version "4.18.2"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
@@ -1732,7 +1715,7 @@ eslint@^4.18.1:
semver "^5.3.0"
strip-ansi "^4.0.0"
strip-json-comments "~2.0.1"
- table "^4.0.1"
+ table "4.0.2"
text-table "~0.2.0"
espree@^3.5.2:
@@ -1774,7 +1757,7 @@ estree-walker@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
-estree-walker@^0.5.0:
+estree-walker@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854"
@@ -2243,6 +2226,12 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+humanize-string@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/humanize-string/-/humanize-string-1.0.1.tgz#fce2d6c545efc25dea1f23235182c98da0180b42"
+ dependencies:
+ decamelize "^1.0.0"
+
iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@@ -2470,10 +2459,6 @@ is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-is-windows@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
-
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -3862,7 +3847,7 @@ resolve@1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@^1.1.6, resolve@^1.4.0, resolve@^1.5.0:
+resolve@^1.1.6, resolve@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
dependencies:
@@ -3897,21 +3882,20 @@ rollup-plugin-babel@^3.0.3:
dependencies:
rollup-pluginutils "^1.5.0"
-rollup-plugin-commonjs@^8.2.6:
- version "8.3.0"
- resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.3.0.tgz#91b4ba18f340951e39ed7b1901f377a80ab3f9c3"
+rollup-plugin-commonjs@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.0.0.tgz#9118e9041b67cdd2a6d8c59dda6349556dce3d0c"
dependencies:
- acorn "^5.2.1"
- estree-walker "^0.5.0"
+ estree-walker "^0.5.1"
magic-string "^0.22.4"
- resolve "^1.4.0"
+ resolve "^1.5.0"
rollup-pluginutils "^2.0.1"
-rollup-plugin-node-resolve@^3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.0.3.tgz#8f57b253edd00e5b0ad0aed7b7e9cf5982e98fa4"
+rollup-plugin-node-resolve@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.2.0.tgz#31534952f3ab21f9473c1d092be7ed43937ea4d4"
dependencies:
- builtin-modules "^1.1.0"
+ builtin-modules "^2.0.0"
is-module "^1.0.0"
resolve "^1.1.6"
@@ -3929,9 +3913,9 @@ rollup-pluginutils@^2.0.1:
estree-walker "^0.3.0"
micromatch "^2.3.11"
-rollup@^0.56.3:
- version "0.56.3"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.3.tgz#7900695531afa1badd3235f285cc4aa0d49ce254"
+rollup@^0.56.5:
+ version "0.56.5"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.56.5.tgz#40fe3cf0cd1659d469baad11f4d5b6336c14ce84"
rst-selector-parser@^2.2.3:
version "2.2.3"
@@ -4190,12 +4174,12 @@ symbol-tree@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
-table@^4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
+table@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
dependencies:
- ajv "^6.0.1"
- ajv-keywords "^3.0.0"
+ ajv "^5.2.3"
+ ajv-keywords "^2.1.0"
chalk "^2.1.0"
lodash "^4.17.4"
slice-ansi "1.0.0"
@@ -4263,8 +4247,8 @@ to-fast-properties@^2.0.0:
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
- version "2.3.4"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
dependencies:
punycode "^1.4.1"