├── .eslintrc
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
├── pre-commit
└── pre-push
├── .npmignore
├── .nvmrc
├── .yarn
├── plugins
│ └── @yarnpkg
│ │ └── plugin-interactive-tools.cjs
└── releases
│ └── yarn-2.4.2.cjs
├── .yarnrc.yml
├── CONTRIBUTING.md
├── LICENSE
├── MIGRATIONS.md
├── README.md
├── babel.config.js
├── commitlint.config.js
├── jest.config.js
├── jest.setup.js
├── package.json
├── rollup.config.js
├── scripts
└── test-build.sh
├── src
├── index.test.js
└── index.tsx
├── tsconfig.json
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint"],
5 | "env": { "jasmine": true },
6 | "rules": {
7 | "no-use-before-define": "off",
8 | "@typescript-eslint/no-use-before-define": ["error"],
9 | "object-curly-newline": 0,
10 | "react/destructuring-assignment": 0,
11 | "react/jsx-one-expression-per-line": 0,
12 | "react/jsx-filename-extension": 0,
13 | "react/no-multi-comp": 0,
14 | "react/jsx-props-no-spreading": 0
15 | },
16 | "globals": {
17 | "jest": "writeable"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [15.14.0]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - name: yarn install
28 | uses: borales/actions-yarn@v2.1.0
29 | with:
30 | cmd: install
31 | - name: yarn build
32 | uses: borales/actions-yarn@v2.1.0
33 | with:
34 | cmd: build
35 | - name: yarn lint
36 | uses: borales/actions-yarn@v2.1.0
37 | with:
38 | cmd: lint
39 | - name: yarn types
40 | uses: borales/actions-yarn@v2.1.0
41 | with:
42 | cmd: types
43 | - name: yarn test
44 | uses: borales/actions-yarn@v2.1.0
45 | with:
46 | cmd: test
47 | - name: yarn test-build
48 | uses: borales/actions-yarn@v2.1.0
49 | with:
50 | cmd: test-build
51 | - name: Coveralls
52 | uses: coverallsapp/github-action@master
53 | with:
54 | github-token: ${{ secrets.GITHUB_TOKEN }}
55 | env:
56 | CI: true
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist/*
3 | /yarn-error.log
4 | /coverage
5 | .yarn/*
6 | !.yarn/patches
7 | !.yarn/releases
8 | !.yarn/plugins
9 | !.yarn/sdks
10 | !.yarn/versions
11 | .pnp.*
12 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn build && yarn lint && yarn test
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn build && yarn types && yarn lint && yarn test-build
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintrc
2 | .git/
3 | .github/
4 | .gitignore
5 | .gitignore
6 | .husky/
7 | .npmignore
8 | .nvmrc
9 | .yarn/
10 | .yarnrc.yml
11 | CONTRIBUTING.md
12 | MIGRATIONS.md
13 | babel.config.js
14 | commitlint.config.js
15 | coverage/
16 | hooks/
17 | jest.config.js
18 | jest.setup.js
19 | rollup.config.js
20 | scripts/
21 | src/
22 | tsconfig.json
23 | yarn-error.log
24 | yarn.lock
25 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v15.14.0
2 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | plugins:
4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
5 | spec: "@yarnpkg/plugin-interactive-tools"
6 |
7 | yarnPath: .yarn/releases/yarn-2.4.2.cjs
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ===============================================================================
3 |
4 | Have something to add? Feature requests, bug reports, and contributions are
5 | enormously welcome!
6 |
7 | 1. Fork this repo
8 | 2. Update the tests and implement the change
9 | 3. Submit a [pull request][github-pull-request]
10 |
11 | (hint: following the conventions in the [the code review
12 | checklist][code-review-checklist] will expedite review and merge)
13 |
14 | [github-pull-request]: help.github.com/pull-requests/
15 | [code-review-checklist]: https://github.com/rjz/code-review-checklist
16 |
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Justin Schrader
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MIGRATIONS.md:
--------------------------------------------------------------------------------
1 | ## Migrating from 2.x.x -> 3.x.x
2 |
3 | #### First things, first...
4 |
5 | `withBreadcrumbs` now returns an array of `Object`s instead of `Component`s:
6 |
7 | ```diff
8 | - breadcrumbs.map(breadcrumb)
9 | + breadcrumbs.map({ breadcrumb })
10 | ```
11 |
12 | Within this object, other props like `match`, `location`, and pass-through props are also returned:
13 |
14 | ```diff
15 | - breadcrumbs.map((breadcrumb) => {})
16 | + breadcrumbs.map(({ breadcrumb, match, location, someCustomProp }) => {})
17 | ```
18 |
19 | #### Why was this change made?
20 |
21 | Under the hood, `withBreadcrumbs` uses React's `createElement` method to render breadcrumbs. In version 2, all props (like `match`, `location`, etc) were assigned to the rendered component (for example: `createElement(breadcrumb, componentProps);`).
22 |
23 | This had the unintended side-effect of rendering any of these props as an _attribute_ on the DOM element. So, ultimately this resulted in some breadcrumbs rendering like `'` as well as some React console warnings [in certain cases](https://github.com/icd2k3/react-router-breadcrumbs-hoc/issues/59).
24 |
25 | This issue has been solved by adding the following logic:
26 | - If the breadcrumb is a simple string, don't render it with props applied
27 | - If the breadcrumb is a function/class (dynamic), _then_ pass all the props to it
28 | - Return objects instead of components so that we can still utilize all the props during the `map`
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | LEGACY
3 |
4 |
5 | This repository is for legacy support of react-router v5.
6 | Please use use-react-router-breadcrumbs and react-router v6 instead.
7 |
8 |
9 |
10 |
11 |
12 | React Router Breadcrumbs HOC
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | A small (~1.3kb compressed & gzipped), flexible, higher order component for rendering breadcrumbs with react-router 5
23 |
24 |
25 |
26 |
27 |
28 | example.com/user/123 → Home / User / John Doe
29 |
30 |
31 |
32 |
33 |
34 |
35 | - [Description](#description)
36 | - [Features](#features)
37 | - [Install](#install)
38 | - [Usage](#usage)
39 | - [Examples](#examples)
40 | - [Simple](#simple)
41 | - [Advanced](#advanced)
42 | - [Route config compatibility](#route-config-compatibility)
43 | - [Dynamic breadcrumbs](#dynamic-breadcrumbs)
44 | - [Options](#options)
45 | - [Disabling default generated breadcrumbs](#disabling-default-generated-breadcrumbs)
46 | - [Order matters!](#order-matters)
47 | - [API](#api)
48 |
49 |
50 |
51 | ## Description
52 |
53 | Render breadcrumbs for `react-router` however you want!
54 |
55 | #### Features
56 | - Easy to get started with automatically generated breadcrumbs.
57 | - Render, map, and wrap breadcrumbs any way you want.
58 | - Compatible with existing [route configs](https://reacttraining.com/react-router/web/example/route-config).
59 |
60 | ## Install
61 |
62 | `yarn add react-router-breadcrumbs-hoc`
63 |
64 | or
65 |
66 | `npm i react-router-breadcrumbs-hoc --save`
67 |
68 | ## Usage
69 |
70 | ```js
71 | withBreadcrumbs()(MyComponent);
72 | ```
73 |
74 | ## Examples
75 |
76 | ### Simple
77 | Start seeing generated breadcrumbs right away with this simple example ([codesandbox](https://codesandbox.io/s/bare-bones-example-kcdrt))
78 | ```js
79 | import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
80 |
81 | const Breadcrumbs = ({ breadcrumbs }) => (
82 | <>
83 | {breadcrumbs.map(({ breadcrumb }) => breadcrumb)}
84 | >
85 | )
86 |
87 | export default withBreadcrumbs()(Breadcrumbs);
88 | ```
89 |
90 | ### Advanced
91 | The example above will work for some routes, but you may want other routes to be dynamic (such as a user name breadcrumb). Let's modify it to handle custom-set breadcrumbs. ([codesandbox](https://codesandbox.io/s/basic-dynamic-example-m03tz))
92 |
93 | ```js
94 | import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
95 |
96 | const userNamesById = { '1': 'John' }
97 |
98 | const DynamicUserBreadcrumb = ({ match }) => (
99 | {userNamesById[match.params.userId]}
100 | );
101 |
102 | // define custom breadcrumbs for certain routes.
103 | // breadcumbs can be components or strings.
104 | const routes = [
105 | { path: '/users/:userId', breadcrumb: DynamicUserBreadcrumb },
106 | { path: '/example', breadcrumb: 'Custom Example' },
107 | ];
108 |
109 | // map, render, and wrap your breadcrumb components however you want.
110 | const Breadcrumbs = ({ breadcrumbs }) => (
111 |
112 | {breadcrumbs.map(({
113 | match,
114 | breadcrumb
115 | }) => (
116 |
117 | {breadcrumb}
118 |
119 | ))}
120 |
121 | );
122 |
123 | export default withBreadcrumbs(routes)(Breadcrumbs);
124 | ```
125 |
126 | For the above example...
127 |
128 | Pathname | Result
129 | --- | ---
130 | /users | Home / Users
131 | /users/1 | Home / Users / John
132 | /example | Home / Custom Example
133 |
134 | ## [Route config](https://reacttraining.com/react-router/web/example/route-config) compatibility
135 |
136 | Add breadcrumbs to your existing [route config](https://reacttraining.com/react-router/web/example/route-config). This is a great way to keep all routing config paths in a single place! If a path ever changes, you'll only have to change it in your main route config rather than maintaining a _separate_ config for `react-router-breadcrumbs-hoc`.
137 |
138 | For example...
139 |
140 | ```js
141 | const routeConfig = [
142 | {
143 | path: "/sandwiches",
144 | component: Sandwiches
145 | }
146 | ];
147 | ```
148 |
149 | becomes...
150 |
151 | ```js
152 | const routeConfig = [
153 | {
154 | path: "/sandwiches",
155 | component: Sandwiches,
156 | breadcrumb: 'I love sandwiches'
157 | }
158 | ];
159 | ```
160 |
161 | then you can just pass the whole route config right into the hook:
162 |
163 | ```js
164 | withBreadcrumbs(routeConfig)(MyComponent);
165 | ```
166 |
167 | ## Dynamic breadcrumbs
168 |
169 | If you pass a component as the `breadcrumb` prop it will be injected with react-router's [match](https://reacttraining.com/react-router/web/api/match) and [location](https://reacttraining.com/react-router/web/api/location) objects as props. These objects contain ids, hashes, queries, etc from the route that will allow you to map back to whatever you want to display in the breadcrumb.
170 |
171 | Let's use Redux as an example with the [match](https://reacttraining.com/react-router/web/api/match) object:
172 |
173 | ```js
174 | // UserBreadcrumb.jsx
175 | const PureUserBreadcrumb = ({ firstName }) => {firstName};
176 |
177 | // find the user in the store with the `id` from the route
178 | const mapStateToProps = (state, props) => ({
179 | firstName: state.userReducer.usersById[props.match.params.id].firstName,
180 | });
181 |
182 | export default connect(mapStateToProps)(PureUserBreadcrumb);
183 |
184 | // routes = [{ path: '/users/:id', breadcrumb: UserBreadcrumb }]
185 | // example.com/users/123 --> Home / Users / John
186 | ```
187 |
188 | Now we can pass this custom `redux` breadcrumb into the HOC:
189 |
190 | ```js
191 | withBreadcrumbs([{
192 | path: '/users/:id',
193 | breadcrumb: UserBreadcrumb
194 | }]);
195 | ```
196 |
197 | ----
198 |
199 | Similarly, the [location](https://reacttraining.com/react-router/web/api/location) object could be useful for displaying dynamic breadcrumbs based on the route's state:
200 |
201 | ```jsx
202 | // dynamically update EditorBreadcrumb based on state info
203 | const EditorBreadcrumb = ({ location: { state: { isNew } } }) => (
204 | {isNew ? 'Add New' : 'Update'}
205 | );
206 |
207 | // routes = [{ path: '/editor', breadcrumb: EditorBreadcrumb }]
208 |
209 | // upon navigation, breadcrumb will display: Update
210 | Edit
211 |
212 | // upon navigation, breadcrumb will display: Add New
213 | Add
214 | ```
215 |
216 | ## Options
217 |
218 | An options object can be passed as the 2nd argument to the hook.
219 |
220 | ```js
221 | withBreadcrumbs(routes, options)(Component);
222 | ```
223 |
224 | Option | Type | Description
225 | --- | --- | ---
226 | `disableDefaults` | `Boolean` | Disables all default generated breadcrumbs. |
227 | `excludePaths` | `Array` | Disables default generated breadcrumbs for specific paths. |
228 |
229 | ### Disabling default generated breadcrumbs
230 |
231 | This package will attempt to create breadcrumbs for you based on the route section. For example `/users` will automatically create the breadcrumb `"Users"`. There are two ways to disable default breadcrumbs for a path:
232 |
233 | **Option 1:** Disable _all_ default breadcrumb generation by passing `disableDefaults: true` in the `options` object
234 |
235 | `withBreadcrumbs(routes, { disableDefaults: true })`
236 |
237 | **Option 2:** Disable _individual_ default breadcrumbs by passing `breadcrumb: null` in route config:
238 |
239 | `{ path: '/a/b', breadcrumb: null }`
240 |
241 | **Option 3:** Disable _individual_ default breadcrumbs by passing an `excludePaths` array in the `options` object
242 |
243 | `withBreadcrumbs(routes, { excludePaths: ['/', '/no-breadcrumb/for-this-route'] })`
244 |
245 | ## Order matters!
246 |
247 | Consider the following route configs:
248 |
249 | ```js
250 | [
251 | { path: '/users/:id', breadcrumb: 'id-breadcrumb' },
252 | { path: '/users/create', breadcrumb: 'create-breadcrumb' },
253 | ]
254 |
255 | // example.com/users/create = 'id-breadcrumb' (because path: '/users/:id' will match first)
256 | // example.com/users/123 = 'id-breadcumb'
257 | ```
258 |
259 | To fix the issue above, just adjust the order of your routes:
260 |
261 | ```js
262 | [
263 | { path: '/users/create', breadcrumb: 'create-breadcrumb' },
264 | { path: '/users/:id', breadcrumb: 'id-breadcrumb' },
265 | ]
266 |
267 | // example.com/users/create = 'create-breadcrumb' (because path: '/users/create' will match first)
268 | // example.com/users/123 = 'id-breadcrumb'
269 | ```
270 |
271 | ## API
272 |
273 | ```js
274 | Route = {
275 | path: String
276 | breadcrumb?: String|Component // if not provided, a default breadcrumb will be returned
277 | matchOptions?: { // see: https://reacttraining.com/react-router/web/api/matchPath
278 | exact?: Boolean,
279 | strict?: Boolean,
280 | }
281 | }
282 |
283 | Options = {
284 | excludePaths?: string[] // disable default breadcrumb generation for specific paths
285 | disableDefaults?: Boolean // disable all default breadcrumb generation
286 | }
287 |
288 | // if routes are not passed, default breadcrumbs will be returned
289 | withBreadcrumbs(routes?: Route[], options?: Options): HigherOrderComponent
290 |
291 | // you shouldn't ever really have to use `getBreadcrumbs`, but it's
292 | // exported for convenience if you don't want to use the HOC
293 | getBreadcrumbs({
294 | routes: Route[],
295 | options: Options,
296 | }): Breadcrumb[]
297 | ```
298 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | module.exports = function(api) {
4 | api.cache(true);
5 |
6 | return {
7 | presets: [
8 | '@babel/preset-env',
9 | '@babel/preset-react',
10 | '@babel/preset-typescript'
11 | ],
12 | plugins: ['@babel/plugin-transform-runtime'],
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverage: true,
3 | coverageDirectory: './coverage',
4 | coveragePathIgnorePatterns: [
5 | '/coverage/',
6 | '/dist/',
7 | '/node_modules/',
8 | 'jest.config.js',
9 | 'jest.setup.js',
10 | ],
11 | coverageThreshold: {
12 | global: {
13 | branches: 100,
14 | functions: 100,
15 | lines: 100,
16 | statements: 100,
17 | },
18 | },
19 | setupFiles: ['./jest.setup.js'],
20 | };
21 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import 'jsdom-global/register';
2 | import Enzyme from 'enzyme';
3 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
4 |
5 | Enzyme.configure({ adapter: new Adapter() });
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router-breadcrumbs-hoc",
3 | "version": "4.1.0",
4 | "description": "small, flexible, higher order component for rendering breadcrumbs with react-router 4.x",
5 | "repository": "icd2k3/react-router-breadcrumbs-hoc",
6 | "main": "dist/cjs/index.js",
7 | "module": "dist/es/index.js",
8 | "umd": "dist/umd/index.js",
9 | "types": "dist/index.d.ts",
10 | "scripts": {
11 | "prepublishOnly": "npm run build && pinst --disable",
12 | "build": "rollup -c && yarn types",
13 | "test": "jest",
14 | "test-build": "sh ./scripts/test-build.sh",
15 | "types": "tsc -p tsconfig.json --declaration --emitDeclarationOnly",
16 | "lint": "eslint ./src/**",
17 | "postpublish": "pinst --enable",
18 | "prepare": "husky install"
19 | },
20 | "author": "Justin Schrader (me@justin.beer)",
21 | "license": "MIT",
22 | "peerDependencies": {
23 | "react": ">=16.8",
24 | "react-router-dom": ">=5"
25 | },
26 | "devDependencies": {
27 | "@babel/cli": "^7.14.5",
28 | "@babel/core": "^7.14.5",
29 | "@babel/plugin-transform-runtime": "^7.14.5",
30 | "@babel/preset-env": "^7.14.5",
31 | "@babel/preset-react": "^7.14.5",
32 | "@babel/preset-typescript": "^7.14.5",
33 | "@commitlint/cli": "^12.1.4",
34 | "@commitlint/config-conventional": "^12.1.4",
35 | "@rollup/plugin-babel": "^5.3.0",
36 | "@rollup/plugin-typescript": "^8.2.1",
37 | "@types/react": "^17.0.11",
38 | "@types/react-dom": "^17.0.7",
39 | "@types/react-router-dom": "^5.3.0",
40 | "@typescript-eslint/eslint-plugin": "^4.26.1",
41 | "@typescript-eslint/parser": "^4.26.1",
42 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
43 | "babel-eslint": "^10.1.0",
44 | "coveralls": "^3.1.0",
45 | "enzyme": "^3.11.0",
46 | "eslint": "^7.28.0",
47 | "eslint-config-airbnb": "^18.2.1",
48 | "eslint-plugin-import": "^2.23.4",
49 | "eslint-plugin-jsx-a11y": "^6.4.1",
50 | "eslint-plugin-react": "^7.24.0",
51 | "husky": "^6.0.0",
52 | "jest": "^27.0.4",
53 | "jsdom": "^16.6.0",
54 | "jsdom-global": "^3.0.2",
55 | "pinst": "^2.1.6",
56 | "prop-types": "^15.7.2",
57 | "react": "17.0.2",
58 | "react-dom": "17.0.2",
59 | "react-router-dom": "^5.3.0",
60 | "rollup": "^2.51.1",
61 | "rollup-plugin-size": "^0.2.2",
62 | "rollup-plugin-terser": "^7.0.2",
63 | "typescript": "4.3.2"
64 | },
65 | "keywords": [
66 | "react",
67 | "router",
68 | "breadcrumbs",
69 | "react-router",
70 | "react-router 4",
71 | "react-router 5",
72 | "react-router-dom"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import typescript from '@rollup/plugin-typescript';
3 | import size from 'rollup-plugin-size';
4 | import { terser } from 'rollup-plugin-terser';
5 |
6 | const pkg = require('./package.json');
7 |
8 | const external = Object.keys(pkg.peerDependencies).concat(/@babel\/runtime/);
9 |
10 | const extensions = ['.js', '.tsx'];
11 |
12 | const sharedPlugins = [
13 | typescript({ tsconfig: './tsconfig.json' }),
14 | babel({
15 | babelHelpers: 'runtime',
16 | exclude: 'node_modules/**',
17 | extensions,
18 | }),
19 | size(),
20 | ];
21 |
22 | const formats = [
23 | { format: 'umd', file: pkg.umd, plugins: sharedPlugins.concat([terser({ format: { comments: false } })]) },
24 | { format: 'cjs', file: pkg.main, plugins: sharedPlugins },
25 | { format: 'es', file: pkg.module, plugins: sharedPlugins },
26 | ];
27 |
28 | const globals = {
29 | react: 'React',
30 | 'react-router-dom': 'ReactRouterDom',
31 | };
32 |
33 | export default formats.map(({ plugins, file, format }) => ({
34 | input: 'src/index.tsx',
35 | plugins,
36 | external,
37 | output: {
38 | exports: 'named',
39 | file,
40 | format,
41 | name: 'react-router-breadcrumbs-hoc',
42 | globals: format !== 'umd'
43 | ? globals
44 | : {
45 | ...globals,
46 | '@babel/runtime/helpers/toConsumableArray': '_toConsumableArray',
47 | '@babel/runtime/helpers/defineProperty': '_defineProperty',
48 | '@babel/runtime/helpers/objectWithoutProperties': '_objectWithoutProperties',
49 | },
50 | },
51 | }));
52 |
--------------------------------------------------------------------------------
/scripts/test-build.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # runs the tests in ./src/index.test.js, but
4 | # replaces the import to target the compiled builds
5 | # in ./dist/es/index.js, ./dist/umd/index.js, and ./dist/cjs/index.js
6 | # this ensures that the act of compiling doesn't break the
7 | # expected behavior somehow.
8 |
9 | set -e
10 |
11 | printf "\n====\nTesting CJS dist build\n====\n" && \
12 | TEST_BUILD=cjs yarn test --coverage=0 && \
13 | printf "\n====\nTesting UMD dist build\n====\n" && \
14 | TEST_BUILD=umd yarn test --coverage=0 && \
15 | printf "\n====\nTesting ES dist build\n====\n" && \
16 | TEST_BUILD=es yarn test --coverage=0
17 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /* eslint-disable react/no-array-index-key */
3 | /* eslint-disable react/jsx-filename-extension */
4 | import React from 'react';
5 | import PropTypes from 'prop-types';
6 | import { mount } from 'enzyme';
7 | import { MemoryRouter as Router, NavLink } from 'react-router-dom';
8 | import withBreadcrumbs, { getBreadcrumbs } from './index.tsx';
9 |
10 | // imports to test compiled builds
11 | import withBreadcrumbsCompiledES, { getBreadcrumbs as getBreadcrumbsCompiledES } from '../dist/es/index';
12 | import withBreadcrumbsCompiledUMD, { getBreadcrumbs as getBreadcrumbsCompiledUMD } from '../dist/umd/index';
13 | import withBreadcrumbsCompiledCJS, { getBreadcrumbs as getBreadcrumbsCompiledCJS } from '../dist/cjs/index';
14 |
15 | const components = {
16 | Breadcrumbs: ({ breadcrumbs, ...forwardedProps }) => (
17 |
18 |
19 | {forwardedProps && Object.values(forwardedProps).filter((v) => typeof v === 'string').map((value) => (
20 | {value}
21 | ))}
22 |
23 |
24 | {breadcrumbs.map(({ breadcrumb, key }, index) => (
25 |
26 | {breadcrumb}
27 | {(index < breadcrumbs.length - 1) && / }
28 |
29 | ))}
30 |
31 |
32 | ),
33 | BreadcrumbMatchTest: ({ match }) => {match.params.number},
34 | BreadcrumbNavLinkTest: ({ match }) => Link,
35 | BreadcrumbLocationTest: ({ location: { state: { isLocationTest } } }) => (
36 |
37 | {isLocationTest ? 'pass' : 'fail'}
38 |
39 | ),
40 | BreadcrumbExtraPropsTest: ({ foo, bar }) => {foo}{bar},
41 | BreadcrumbMemoized: React.memo(() => Memoized),
42 | // eslint-disable-next-line react/prefer-stateless-function
43 | BreadcrumbClass: class BreadcrumbClass extends React.PureComponent {
44 | render() { return (Class); }
45 | },
46 | };
47 |
48 | const getHOC = () => {
49 | switch (process.env.TEST_BUILD) {
50 | case 'cjs':
51 | return withBreadcrumbsCompiledCJS;
52 | case 'umd':
53 | return withBreadcrumbsCompiledUMD;
54 | case 'es':
55 | return withBreadcrumbsCompiledES;
56 | default:
57 | return withBreadcrumbs;
58 | }
59 | };
60 |
61 | const getMethod = () => {
62 | switch (process.env.TEST_BUILD) {
63 | case 'cjs':
64 | return getBreadcrumbsCompiledCJS;
65 | case 'umd':
66 | return getBreadcrumbsCompiledUMD;
67 | case 'es':
68 | return getBreadcrumbsCompiledES;
69 | default:
70 | return getBreadcrumbs;
71 | }
72 | };
73 |
74 | const render = ({
75 | options,
76 | pathname,
77 | routes,
78 | state,
79 | props,
80 | }) => {
81 | const Breadcrumbs = getHOC()(routes, options)(components.Breadcrumbs);
82 | const wrapper = mount(
83 |
87 |
88 | ,
89 | );
90 |
91 | return {
92 | breadcrumbs: wrapper.find('.breadcrumbs-container').text(),
93 | forwardedProps: wrapper.find('.forwarded-props').text(),
94 | wrapper,
95 | };
96 | };
97 |
98 | const matchShape = {
99 | isExact: PropTypes.bool.isRequired,
100 | params: PropTypes.shape().isRequired,
101 | path: PropTypes.string.isRequired,
102 | url: PropTypes.string.isRequired,
103 | };
104 |
105 | components.Breadcrumbs.propTypes = {
106 | breadcrumbs: PropTypes.arrayOf(PropTypes.shape({
107 | breadcrumb: PropTypes.node.isRequired,
108 | match: PropTypes.shape().isRequired,
109 | location: PropTypes.shape.isRequired,
110 | })).isRequired,
111 | };
112 |
113 | components.BreadcrumbMatchTest.propTypes = {
114 | match: PropTypes.shape(matchShape).isRequired,
115 | };
116 |
117 | components.BreadcrumbNavLinkTest.propTypes = {
118 | match: PropTypes.shape(matchShape).isRequired,
119 | };
120 |
121 | components.BreadcrumbLocationTest.propTypes = {
122 | location: PropTypes.shape({
123 | state: PropTypes.shape({
124 | isLocationTest: PropTypes.bool.isRequired,
125 | }).isRequired,
126 | }).isRequired,
127 | };
128 |
129 | components.BreadcrumbExtraPropsTest.propTypes = {
130 | foo: PropTypes.string.isRequired,
131 | bar: PropTypes.string.isRequired,
132 | };
133 |
134 | describe('react-router-breadcrumbs-hoc', () => {
135 | describe('Valid routes', () => {
136 | it('Should render breadcrumb components as expected', () => {
137 | const routes = [
138 | // test home route
139 | { path: '/', breadcrumb: 'Home' },
140 | // test breadcrumb passed as string
141 | { path: '/1', breadcrumb: 'One' },
142 | // test simple breadcrumb component
143 | { path: '/1/2', breadcrumb: () => TWO },
144 | // test advanced breadcrumb component (user can use `match` however they wish)
145 | { path: '/1/2/:number', breadcrumb: components.BreadcrumbMatchTest },
146 | // test NavLink wrapped breadcrumb
147 | { path: '/1/2/:number/4', breadcrumb: components.BreadcrumbNavLinkTest },
148 | // test a no-match route
149 | { path: '/no-match', breadcrumb: 'no match' },
150 | ];
151 | const { breadcrumbs, wrapper } = render({ pathname: '/1/2/3/4', routes });
152 | expect(breadcrumbs).toBe('Home / One / TWO / 3 / Link');
153 | expect(wrapper.find('NavLink').props().to).toBe('/1/2/3/4');
154 | });
155 | });
156 |
157 | describe('Route order', () => {
158 | it('Should match the first breadcrumb in route array user/create', () => {
159 | const routes = [
160 | {
161 | path: '/user/create',
162 | breadcrumb: 'Add User',
163 | },
164 | {
165 | path: '/user/:id',
166 | breadcrumb: '1',
167 | },
168 | ];
169 | const { breadcrumbs } = render({ pathname: '/user/create', routes });
170 | expect(breadcrumbs).toBe('Home / User / Add User');
171 | });
172 |
173 | it('Should match the first breadcrumb in route array user/:id', () => {
174 | const routes = [
175 | {
176 | path: '/user/:id',
177 | breadcrumb: 'Oops',
178 | },
179 | {
180 | path: '/user/create',
181 | breadcrumb: 'Add User',
182 | },
183 | ];
184 | const { breadcrumbs } = render({ pathname: '/user/create', routes });
185 | expect(breadcrumbs).toBe('Home / User / Oops');
186 | });
187 | });
188 |
189 | describe('Different component types', () => {
190 | it('Should render memoized components', () => {
191 | const routes = [{ path: '/memo', breadcrumb: components.BreadcrumbMemoized }];
192 | const { breadcrumbs } = render({ pathname: '/memo', routes });
193 | expect(breadcrumbs).toBe('Home / Memoized');
194 | });
195 |
196 | it('Should render class components', () => {
197 | const routes = [{ path: '/class', breadcrumb: components.BreadcrumbClass }];
198 | const { breadcrumbs } = render({ pathname: '/class', routes });
199 | expect(breadcrumbs).toBe('Home / Class');
200 | });
201 | });
202 |
203 | describe('Custom match options', () => {
204 | it('Should allow `strict` rule', () => {
205 | const routes = [
206 | {
207 | path: '/one/',
208 | breadcrumb: '1',
209 | // not recommended, but supported
210 | matchOptions: { exact: false, strict: true },
211 | },
212 | ];
213 | const { breadcrumbs } = render({ pathname: '/one', routes });
214 | expect(breadcrumbs).toBe('');
215 | });
216 | });
217 |
218 | describe('When extending react-router config', () => {
219 | it('Should render expected breadcrumbs with sensible defaults', () => {
220 | const routes = [
221 | { path: '/one', breadcrumb: 'OneCustom' },
222 | { path: '/one/two' },
223 | ];
224 | const { breadcrumbs } = render({ pathname: '/one/two', routes });
225 | expect(breadcrumbs).toBe('Home / OneCustom / Two');
226 | });
227 |
228 | it('Should support nested routes', () => {
229 | const routes = [
230 | {
231 | path: '/one',
232 | routes: [
233 | {
234 | path: '/one/two',
235 | breadcrumb: 'TwoCustom',
236 | routes: [
237 | { path: '/one/two/three', breadcrumb: 'ThreeCustom' },
238 | ],
239 | },
240 | ],
241 | },
242 | ];
243 | const { breadcrumbs } = render({ pathname: '/one/two/three', routes });
244 | expect(breadcrumbs).toBe('Home / One / TwoCustom / ThreeCustom');
245 | });
246 | });
247 |
248 | describe('Defaults', () => {
249 | describe('No routes array', () => {
250 | it('Should automatically render breadcrumbs with default strings', () => {
251 | const { breadcrumbs } = render({ pathname: '/one/two' });
252 |
253 | expect(breadcrumbs).toBe('Home / One / Two');
254 | });
255 | });
256 |
257 | describe('Override defaults', () => {
258 | it('Should render user-provided breadcrumbs where possible and use defaults otherwise', () => {
259 | const routes = [{ path: '/one', breadcrumb: 'Override' }];
260 | const { breadcrumbs } = render({ pathname: '/one/two', routes });
261 |
262 | expect(breadcrumbs).toBe('Home / Override / Two');
263 | });
264 | });
265 |
266 | describe('No breadcrumb', () => {
267 | it('Should be possible to NOT render a breadcrumb', () => {
268 | const routes = [{ path: '/one', breadcrumb: null }];
269 | const { breadcrumbs } = render({ pathname: '/one/two', routes });
270 |
271 | expect(breadcrumbs).toBe('Home / Two');
272 | });
273 |
274 | it('Should be possible to NOT render a "Home" breadcrumb', () => {
275 | const routes = [{ path: '/', breadcrumb: null }];
276 | const { breadcrumbs } = render({ pathname: '/one/two', routes });
277 |
278 | expect(breadcrumbs).toBe('One / Two');
279 | });
280 | });
281 | });
282 |
283 | describe('When using the location object', () => {
284 | it('Should be provided in the rendered breadcrumb component', () => {
285 | const routes = [{ path: '/one', breadcrumb: components.BreadcrumbLocationTest }];
286 | const { breadcrumbs } = render({ pathname: '/one', state: { isLocationTest: true }, routes });
287 | expect(breadcrumbs).toBe('Home / pass');
288 | });
289 | });
290 |
291 | describe('When pathname includes query params', () => {
292 | it('Should not render query breadcrumb', () => {
293 | const { breadcrumbs } = render({ pathname: '/one?mock=query' });
294 | expect(breadcrumbs).toBe('Home / One');
295 | });
296 | });
297 |
298 | describe('When pathname includes a trailing slash', () => {
299 | it('Should ignore the trailing slash', () => {
300 | const { breadcrumbs } = render({ pathname: '/one/' });
301 | expect(breadcrumbs).toBe('Home / One');
302 | });
303 | });
304 |
305 | describe('When using additional props inside routes', () => {
306 | it('Should pass through extra props to user-provided components', () => {
307 | const routes = [
308 | {
309 | path: '/one',
310 | breadcrumb: components.BreadcrumbExtraPropsTest,
311 | foo: 'Pass through',
312 | bar: ' props',
313 | },
314 | ];
315 | const { breadcrumbs } = render({ pathname: '/one', routes });
316 | expect(breadcrumbs).toBe('Home / Pass through props');
317 | });
318 | });
319 |
320 | describe('Options', () => {
321 | describe('excludePaths', () => {
322 | it('Should not return breadcrumbs for specified paths', () => {
323 | const { breadcrumbs } = render({ pathname: '/one/two', options: { excludePaths: ['/', '/one'] } });
324 | expect(breadcrumbs).toBe('Two');
325 | });
326 |
327 | it('Should work with url params', () => {
328 | const routes = [
329 | { path: '/a' },
330 | { path: '/a/:b' },
331 | { path: '/a/:b/:c' },
332 | ];
333 | const { breadcrumbs } = render({
334 | pathname: '/a/b/c',
335 | routes,
336 | options: { excludePaths: ['/a/:b', '/a'] },
337 | });
338 | expect(breadcrumbs).toBe('Home / C');
339 | });
340 | });
341 |
342 | describe('options without routes array', () => {
343 | it('Should be able to set options without defining a routes array', () => {
344 | const { breadcrumbs } = render({ pathname: '/one/two', routes: null, options: { excludePaths: ['/', '/one'] } });
345 | expect(breadcrumbs).toBe('Two');
346 | });
347 | });
348 |
349 | describe('disableDefaults', () => {
350 | it('Should disable all default breadcrumb generation', () => {
351 | const routes = [{ path: '/one', breadcrumb: 'One' }, { path: '/one/two' }];
352 | const { breadcrumbs } = render({ pathname: '/one/two', routes, options: { disableDefaults: true } });
353 |
354 | expect(breadcrumbs).toBe('One');
355 | });
356 | });
357 | });
358 |
359 | describe('Invalid route object', () => {
360 | it('Should error if `path` is not provided', () => {
361 | expect(() => getMethod()({ routes: [{ breadcrumb: 'Yo' }], location: { pathname: '/1' } }))
362 | .toThrow('withBreadcrumbs: `path` must be provided in every route object');
363 | });
364 | });
365 |
366 | describe('DOM rendering', () => {
367 | it('Should not render props as element attributes on breadcrumbs', () => {
368 | const { wrapper } = render({ pathname: '/one' });
369 | expect(wrapper.html()).not.toContain('[object Object]');
370 | });
371 | });
372 |
373 | describe('HOC prop forwarding', () => {
374 | it('Should allow for forwarding props to the wrapped component', () => {
375 | const props = { testing: 'prop forwarding works' };
376 | const { forwardedProps } = render({ pathname: '/', props });
377 | expect(forwardedProps).toEqual('prop forwarding works');
378 | });
379 | });
380 |
381 | describe('Edge cases', () => {
382 | it('Should handle 2 slashes in a URL (site.com/sandwiches//tuna)', () => {
383 | const { breadcrumbs } = render({ pathname: '/sandwiches//tuna' });
384 | expect(breadcrumbs).toBe('Home / Sandwiches / Tuna');
385 | });
386 | });
387 | });
388 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This source code is licensed under the MIT license found in the
3 | * LICENSE file in the root directory of this source tree.
4 | *
5 | * This script exports a HOC that accepts a routes array of objects
6 | * and an options object.
7 | *
8 | * API:
9 | *
10 | * withBreadcrumbs(
11 | * routes?: Array,
12 | * options? Object,
13 | * ): HigherOrderComponent
14 | *
15 | * More Info:
16 | *
17 | * https://github.com/icd2k3/react-router-breadcrumbs-hoc
18 | *
19 | */
20 |
21 | import React, { createElement } from 'react';
22 | import { useLocation, matchPath } from 'react-router-dom';
23 |
24 | const DEFAULT_MATCH_OPTIONS = { exact: true };
25 | const NO_BREADCRUMB = 'NO_BREADCRUMB';
26 |
27 | export interface Options {
28 | currentSection?: string;
29 | disableDefaults?: boolean;
30 | excludePaths?: string[];
31 | pathSection?: string;
32 | }
33 |
34 | export interface Location {
35 | pathname: string
36 | }
37 |
38 | export interface MatchOptions {
39 | exact?: boolean;
40 | strict?: boolean;
41 | sensitive?: boolean;
42 | }
43 |
44 | export interface BreadcrumbsRoute {
45 | path: string;
46 | breadcrumb?: React.ComponentType | React.ElementType | string;
47 | matchOptions?: MatchOptions;
48 | routes?: BreadcrumbsRoute[];
49 | [x: string]: any;
50 | }
51 |
52 | /**
53 | * This method was "borrowed" from https://stackoverflow.com/a/28339742
54 | * we used to use the humanize-string package, but it added a lot of bundle
55 | * size and issues with compilation. This 4-liner seems to cover most cases.
56 | */
57 | const humanize = (str: string): string => str
58 | .replace(/^[\s_]+|[\s_]+$/g, '')
59 | .replace(/[_\s]+/g, ' ')
60 | .replace(/^[a-z]/, (m) => m.toUpperCase());
61 |
62 | /**
63 | * Renders and returns the breadcrumb complete
64 | * with `match`, `location`, and `key` props.
65 | */
66 | const render = ({
67 | breadcrumb: Breadcrumb,
68 | match,
69 | location,
70 | ...rest
71 | }: {
72 | breadcrumb: React.ComponentType | string,
73 | match: { url: string },
74 | location: Location
75 | }): {
76 | match: { url: string },
77 | location: Location,
78 | key: string,
79 | breadcrumb: React.ReactNode
80 | } => {
81 | const componentProps = { match, location, key: match.url, ...rest };
82 |
83 | return {
84 | ...componentProps,
85 | breadcrumb: typeof Breadcrumb === 'string'
86 | ? createElement('span', { key: componentProps.key }, Breadcrumb)
87 | : ,
88 | };
89 | };
90 |
91 | /**
92 | * Small helper method to get a default breadcrumb if the user hasn't provided one.
93 | */
94 | const getDefaultBreadcrumb = ({
95 | currentSection,
96 | location,
97 | pathSection,
98 | }: {
99 | currentSection: string,
100 | location: Location,
101 | pathSection: string,
102 | }) => {
103 | const match = matchPath(pathSection, { ...DEFAULT_MATCH_OPTIONS, path: pathSection })
104 | /* istanbul ignore next: this is hard to mock in jest :( */
105 | || { url: 'not-found' };
106 |
107 | return render({
108 | breadcrumb: humanize(currentSection),
109 | match,
110 | location,
111 | });
112 | };
113 |
114 | /**
115 | * Loops through the route array (if provided) and returns either a
116 | * user-provided breadcrumb OR a sensible default (if enabled)
117 | */
118 | const getBreadcrumbMatch = ({
119 | currentSection,
120 | disableDefaults,
121 | excludePaths,
122 | location,
123 | pathSection,
124 | routes,
125 | }: {
126 | currentSection: string,
127 | disableDefaults?: boolean,
128 | excludePaths?: string[],
129 | location: { pathname: string },
130 | pathSection: string,
131 | routes: BreadcrumbsRoute[]
132 | }) => {
133 | let breadcrumb;
134 |
135 | // Check the optional `excludePaths` option in `options` to see if the
136 | // current path should not include a breadcrumb.
137 | const getIsPathExcluded = (path: string) => matchPath(pathSection, {
138 | path,
139 | exact: true,
140 | strict: false,
141 | });
142 | if (excludePaths && excludePaths.some(getIsPathExcluded)) {
143 | return NO_BREADCRUMB;
144 | }
145 |
146 | // Loop through the route array and see if the user has provided a custom breadcrumb.
147 | routes.some(({ breadcrumb: userProvidedBreadcrumb, matchOptions, path, ...rest }) => {
148 | if (!path) {
149 | throw new Error('withBreadcrumbs: `path` must be provided in every route object');
150 | }
151 |
152 | const match = matchPath(pathSection, { ...(matchOptions || DEFAULT_MATCH_OPTIONS), path });
153 |
154 | // If user passed breadcrumb: null OR custom match options to suppress a breadcrumb
155 | // we need to know NOT to add it to the matches array
156 | // see: `if (breadcrumb !== NO_BREADCRUMB)` below.
157 | if ((match && userProvidedBreadcrumb === null) || (!match && matchOptions)) {
158 | breadcrumb = NO_BREADCRUMB;
159 | return true;
160 | }
161 |
162 | if (match) {
163 | // This covers the case where a user may be extending their react-router route
164 | // config with breadcrumbs, but also does not want default breadcrumbs to be
165 | // automatically generated (opt-in).
166 | if (!userProvidedBreadcrumb && disableDefaults) {
167 | breadcrumb = NO_BREADCRUMB;
168 | return true;
169 | }
170 |
171 | breadcrumb = render({
172 | // Although we have a match, the user may be passing their react-router config object
173 | // which we support. The route config object may not have a `breadcrumb` param specified.
174 | // If this is the case, we should provide a default via `humanize`.
175 | breadcrumb: userProvidedBreadcrumb || humanize(currentSection),
176 | match,
177 | location,
178 | ...rest,
179 | });
180 | return true;
181 | }
182 | return false;
183 | });
184 |
185 | // User provided a breadcrumb prop, or we generated one above.
186 | if (breadcrumb) {
187 | return breadcrumb;
188 | }
189 |
190 | // If there was no breadcrumb provided and user has disableDefaults turned on.
191 | if (disableDefaults) {
192 | return NO_BREADCRUMB;
193 | }
194 |
195 | // If the above conditionals don't fire, generate a default breadcrumb based on the path.
196 | return getDefaultBreadcrumb({
197 | pathSection,
198 | // include a "Home" breadcrumb by default (can be overrode or disabled in config).
199 | currentSection: pathSection === '/' ? 'Home' : currentSection,
200 | location,
201 | });
202 | };
203 |
204 | /**
205 | * Splits the pathname into sections, then search for matches in the routes
206 | * a user-provided breadcrumb OR a sensible default.
207 | */
208 | export const getBreadcrumbs = (
209 | {
210 | routes,
211 | location,
212 | options = {},
213 | }: {
214 | routes: BreadcrumbsRoute[],
215 | location: Location,
216 | options?: Options
217 | },
218 | ): Array => {
219 | const matches:Array = [];
220 | const { pathname } = location;
221 |
222 | pathname
223 | .split('?')[0]
224 | // Split pathname into sections.
225 | .split('/')
226 | // Reduce over the sections and call `getBreadcrumbMatch()` for each section.
227 | .reduce((previousSection: string, currentSection: string, index: number) => {
228 | // Combine the last route section with the currentSection.
229 | // For example, `pathname = /1/2/3` results in match checks for
230 | // `/1`, `/1/2`, `/1/2/3`.
231 | const pathSection = !currentSection ? '/' : `${previousSection}/${currentSection}`;
232 |
233 | // Ignore trailing slash or double slashes in the URL
234 | if (pathSection === '/' && index !== 0) {
235 | return '';
236 | }
237 |
238 | const breadcrumb = getBreadcrumbMatch({
239 | currentSection,
240 | location,
241 | pathSection,
242 | routes,
243 | ...options,
244 | });
245 |
246 | // Add the breadcrumb to the matches array
247 | // unless the user has explicitly passed.
248 | // { path: x, breadcrumb: null } to disable.
249 | if (breadcrumb !== NO_BREADCRUMB) {
250 | matches.push(breadcrumb);
251 | }
252 |
253 | return pathSection === '/' ? '' : pathSection;
254 | }, '');
255 |
256 | return matches;
257 | };
258 |
259 | /**
260 | * Takes a route array and recursively flattens it IF there are
261 | * nested routes in the config.
262 | */
263 | const flattenRoutes = (routes: BreadcrumbsRoute[]) => (routes)
264 | .reduce((arr, route: BreadcrumbsRoute): BreadcrumbsRoute[] => {
265 | if (route.routes) {
266 | return arr.concat([route, ...flattenRoutes(route.routes)]);
267 | }
268 | return arr.concat(route);
269 | }, [] as BreadcrumbsRoute[]);
270 |
271 | /**
272 | * Accepts optional routes array and options and returns an array of
273 | * breadcrumbs.
274 | *
275 | * @example
276 | * import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
277 | * const Breadcrumbs = ({ breadcrumbs }) => (
278 | * <>{breadcrumbs.map(({ breadcrumb }) => breadcrumb)}>
279 | * )
280 | * export default withBreadcrumbs()(Breadcrumbs);
281 | */
282 | const withBreadcrumbs = (
283 | routes?: BreadcrumbsRoute[],
284 | options?: Options,
285 | ) => (
286 | Component: React.ComponentType<{
287 | breadcrumbs: Array
288 | }>,
289 | ) => (props: any) => React.createElement(Component, {
290 | ...props,
291 | breadcrumbs: getBreadcrumbs({
292 | options,
293 | routes: flattenRoutes(routes || []),
294 | location: useLocation(),
295 | }),
296 | });
297 |
298 | export default withBreadcrumbs;
299 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "esModuleInterop": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "esnext",
8 | "moduleResolution": "node",
9 | "outDir": "dist",
10 | "removeComments": true,
11 | "strict": true,
12 | "target": "es6"
13 | },
14 | "files": [
15 | "src/index.tsx"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------