31 | );
32 |
33 | storiesOf('Params', module)
34 | .addDecorator(StoryRouter())
35 | .add('params', () => );
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Gianni Valdambrini
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/examples/react-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook-react-router-examples",
3 | "description": "Storybook-router react-router examples",
4 | "license": "MIT",
5 | "author": "Gianni Valdambrini",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/gvaldambrini/storybook-router.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/gvaldambrini/storybook-router/issues"
12 | },
13 | "homepage": "https://github.com/gvaldambrini/storybook-router",
14 | "keywords": [
15 | "react",
16 | "storybook",
17 | "react-router"
18 | ],
19 | "dependencies": {
20 | "@storybook/addon-actions": "^4.0.9||^5.0.2||5.2.0-beta.13",
21 | "@storybook/addon-links": "^4.0.9||^5.0.2||5.2.0-beta.13",
22 | "@storybook/react": "^4.0.9||^5.0.2||5.2.0-beta.13",
23 | "prop-types": "^15.7.2",
24 | "react": "^16.8.4",
25 | "react-dom": "^16.8.4",
26 | "react-router": "^5.0.0",
27 | "react-router-dom": "^5.0.0",
28 | "react-scripts": "3.0.1",
29 | "storybook-react-router": "^1.0.8"
30 | },
31 | "scripts": {
32 | "storybook": "start-storybook -p 9001 -c .storybook"
33 | },
34 | "private": true,
35 | "version": "1.0.8"
36 | }
37 |
--------------------------------------------------------------------------------
/examples/react-router/BackForward.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { storiesOf } from '@storybook/react';
5 | import { Route, Link } from 'react-router-dom';
6 |
7 | import StoryRouter from 'storybook-react-router';
8 |
9 | const ChildLocation = ({ location }) => (
10 |
`,
64 | }));
65 |
66 | storiesOf('Links', module)
67 | .addDecorator(
68 | StoryRouter(
69 | { '/base/inner': linkTo('Links', 'target story') },
70 | { initialEntry: '/base' }
71 | )
72 | )
73 | .add('router-link relative path', () => ({
74 | template: `A relative path Link`,
75 | }));
76 |
--------------------------------------------------------------------------------
/packages/react/react.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { action } from '@storybook/addon-actions';
5 | import { MemoryRouter, matchPath, Route } from 'react-router';
6 |
7 | const match = (link, path) => {
8 | // If the new path matches with one of the keys defined in the links object, then
9 | // executes the given corresponding callback value with the path as argument.
10 | // As behind the scene matchProps uses path-to-regexp (https://goo.gl/xgzOaL)
11 | // you can use parameter names and regexp within the link keys.
12 | return matchPath(link, { path: path, exact: true });
13 | };
14 |
15 | const StoryRouter = ({ children, links, routerProps }) => (
16 | // Limitation: as MemoryRouter creates a new history object, you cannot pass it from
17 | // a story to another one and so you cannot implement a back or forward button which
18 | // works among stories.
19 |
20 | (
22 |
23 | {children}
24 |
25 | )}
26 | />
27 |
28 | );
29 |
30 | StoryRouter.propTypes = {
31 | links: PropTypes.object,
32 | routerProps: PropTypes.object,
33 | children: PropTypes.node,
34 | };
35 |
36 | class HistoryWatcher extends Component {
37 | constructor(props) {
38 | super(props);
39 | this.onHistoryChanged = this.onHistoryChanged.bind(this);
40 | }
41 |
42 | componentDidMount() {
43 | // React on every change to the history
44 | this.unlisten = this.props.history.listen(this.onHistoryChanged);
45 | }
46 |
47 | componentWillUnmount() {
48 | // If an exception occurs during a custom componentDidMount hook the
49 | // HistoryWatcher::componentDidMount method will not be called and so
50 | // the unlisten method will not be defined.
51 | if (!this.unlisten) {
52 | return;
53 | }
54 |
55 | this.unlisten();
56 | }
57 |
58 | onHistoryChanged(location, historyAction) {
59 | const path = location.pathname;
60 | const { links } = this.props;
61 |
62 | for (const link in links) {
63 | if (match(path, link)) {
64 | links[link](path);
65 | return;
66 | }
67 | }
68 | action(historyAction ? historyAction : location.action)(path);
69 | }
70 |
71 | render() {
72 | return this.props.children;
73 | }
74 | }
75 |
76 | HistoryWatcher.propTypes = {
77 | history: PropTypes.object.isRequired,
78 | location: PropTypes.object.isRequired,
79 | links: PropTypes.object,
80 | children: PropTypes.node,
81 | };
82 |
83 | const storyRouterDecorator = (links, routerProps) => {
84 | const s = story => (
85 |
86 | {story()}
87 |
88 | );
89 | s.displayName = 'StoryRouter';
90 | return s;
91 | };
92 |
93 | export { StoryRouter };
94 |
95 | export default storyRouterDecorator;
96 |
--------------------------------------------------------------------------------
/packages/vue/vue.js:
--------------------------------------------------------------------------------
1 | import VueRouter from 'vue-router';
2 | import Vue from 'vue';
3 |
4 | import { action } from '@storybook/addon-actions';
5 |
6 | Vue.use(VueRouter);
7 |
8 | const storyRouterDecorator = (links = {}, routerProps = {}) => {
9 | return story => {
10 | const router = new VueRouter(routerProps);
11 | router.replace(routerProps.initialEntry ? routerProps.initialEntry : '/');
12 |
13 | const getLocation = location => {
14 | // The location can be a simple string if you are using directly one of the
15 | // Router methods (https://router.vuejs.org/en/api/router-instance.html#methods)
16 | // or it can be an object, having the name or the path depending if you
17 | // are using named routes or not.
18 | if (typeof location === 'object') {
19 | return location.path ? location.path : `name: ${location.name}`;
20 | }
21 | return location;
22 | };
23 |
24 | let replaced;
25 |
26 | // We want to log every action performed on the navigation router with the only
27 | // exception of links replaced with the linkTo callback.
28 | // Unfortunately VueRouter does not perform any action if the target route is
29 | // the same of the current one (see the code at the url https://goo.gl/gGVxzq).
30 | // Replacing the original push / replace router methods workaround the issue
31 | // with the assumption that the afterEach global guard is called from those
32 | // methods.
33 | const originalPush = router.push.bind(router);
34 |
35 | router.push = (location, success, abort) => {
36 | replaced = false;
37 | originalPush(location, success, abort);
38 |
39 | if (!replaced) {
40 | action('PUSH')(getLocation(location));
41 | }
42 | };
43 |
44 | const originalReplace = router.replace.bind(router);
45 |
46 | router.replace = (location, success, abort) => {
47 | replaced = false;
48 | originalReplace(location, success, abort);
49 |
50 | if (!replaced) {
51 | action('REPLACE')(getLocation(location));
52 | }
53 | };
54 |
55 | if (routerProps.globalBeforeEach) {
56 | router.beforeEach(routerProps.globalBeforeEach);
57 | }
58 |
59 | router.afterEach(to => {
60 | for (const link in links) {
61 | if (to.fullPath === link) {
62 | links[link](to.fullPath);
63 | replaced = true;
64 | return;
65 | }
66 | }
67 | });
68 |
69 | const WrappedComponent = story();
70 | return Vue.extend({
71 | router,
72 | components: { WrappedComponent },
73 | template: '',
74 | beforeDestroy: function() {
75 | // Remove the afterEach callback from the router list to not
76 | // accumulate callbacks called for every route action (in practice
77 | // this means that without this the action is executed as many
78 | // times as the VueRouter instance has been created)
79 | this.$options.router.afterHooks = [];
80 | },
81 | });
82 | };
83 | };
84 |
85 | export default storyRouterDecorator;
86 |
--------------------------------------------------------------------------------
/packages/vue/README.md:
--------------------------------------------------------------------------------
1 | # storybook-router
2 |
3 | A [Storybook](https://storybook.js.org/) decorator that allows you to use your routing-aware components.
4 |
5 | ## Install
6 |
7 | npm install --save-dev storybook-vue-router
8 |
9 | ## The StoryRouter decorator
10 | The decorator is actually a function which wraps the `VueRouter` instance. It accepts two optional arguments that you can use if you want to build a prototype of your navigation within storybook or if you need more control over the router itself.
11 |
12 | In its default behavior the decorator just log every route action perfomed using the [storybook action logger](https://github.com/storybooks/storybook/tree/master/addons/actions). If you are fine with the default arguments you can add globally the `StoryRouter` decorator, however if you need to specify some of the arguments you have to use the decorator for every story that needs it.
13 |
14 | ## Usage
15 |
16 | Suppose you have a navigation bar that uses the vue-router `router-link`:
17 | ```js
18 | const NavBar = {
19 | template: `
20 |
21 | Home
22 | About
23 |
`
24 | };
25 | ```
26 | you can define a story for your component just like this:
27 |
28 | ```js
29 | import { storiesOf } from '@storybook/vue';
30 | import StoryRouter from 'storybook-vue-router';
31 |
32 | storiesOf('NavBar', module)
33 | .addDecorator(StoryRouter())
34 | .add('default', () => NavBar);
35 | ```
36 |
37 | or if you want to include in your story the target components (with a local navigation) you can write:
38 | ```js
39 | import { storiesOf } from '@storybook/vue';
40 | import StoryRouter from 'storybook-vue-router';
41 |
42 | const Home = {
43 | template: '
`
63 | }));
64 | ```
65 |
66 | ## StoryRouter arguments
67 |
68 | The **first argument** is an object that you can use to extend the default behavior.
69 | Every time that a key in the object matches with a path Storybook will call the callback specified for the corresponding value with the destination path as argument.
70 | This way you can for example link stories together using the [`links` addons](https://github.com/storybooks/storybook/tree/master/addons/links) with the linkTo function.
71 | The link keys need to be equal (`===`) to the fullPath of the destination route.
72 |
73 | The **second argument** is another object you can use to specify one of the [vue-router constructor options](https://router.vuejs.org/en/api/options.html) plus a couple of specific `StoryRouter` options:
74 | * initialEntry, the starting location [default `'/'`]
75 | * globalBeforeEach, a function which will be installed as a [global beforeEach guard](https://router.vuejs.org/en/advanced/navigation-guards.html)
76 |
77 |
78 | ## Advanced usage and examples
79 |
80 | You can find more examples in the provided [stories](https://github.com/gvaldambrini/storybook-router/tree/master/examples/vue-router).
81 | You can run them cloning this repository and executing (supposing you have installed globally [lerna](https://github.com/lerna/lerna)):
82 |
83 | yarn install && yarn bootstrap
84 | yarn storybook-vue-examples
85 |
86 | ## Limitations
87 |
88 | As the wrapped VueRouter uses the browser history API which is quite limited (for example, it is not possible to reset the history stack) the same limitations apply to the `StoryRouter` decorator.
89 |
--------------------------------------------------------------------------------
/packages/react/README.md:
--------------------------------------------------------------------------------
1 | # storybook-router
2 |
3 | A [Storybook](https://storybook.js.org/) decorator that allows you to use your routing-aware components.
4 |
5 | ## Install
6 |
7 | npm install --save-dev storybook-react-router
8 |
9 | ## The StoryRouter decorator
10 | The decorator is actually a function which wraps the `Router` instance. It accepts two optional arguments that you can use if you want to build a prototype of your navigation within storybook or if you need more control over the router itself.
11 |
12 | In its default behavior the decorator just log every route action perfomed using the [storybook action logger](https://github.com/storybooks/storybook/tree/master/addons/actions). If you are fine with the default arguments you can add globally the `StoryRouter` decorator, however if you need to specify some of the arguments you have to use the decorator for every story that needs it.
13 |
14 | ## Usage
15 |
16 | Suppose you have a component that uses react-router `Route` and `Link`:
17 |
18 | ```js
19 | import React from 'react';
20 | import { Route, Link } from 'react-router-dom';
21 |
22 | const ChildId = ({match}) => (
23 |
36 | );
37 |
38 | export default ComponentParams;
39 | ```
40 |
41 | you can add the `StoryRouter` decorator to your story this way:
42 |
43 | ```js
44 | import { storiesOf } from '@storybook/react';
45 | import StoryRouter from 'storybook-react-router';
46 |
47 | import ComponentParams from '/ComponentParams';
48 |
49 | storiesOf('Params', module)
50 | .addDecorator(StoryRouter())
51 | .add('params', () => (
52 |
53 | ));
54 | ```
55 |
56 | If you want to use `StoryRouter` in all your stories, you can also add it globally by editing your Storybook `config.js` file:
57 |
58 | ```js
59 | import { configure, addDecorator } from '@storybook/react';
60 | import StoryRouter from 'storybook-react-router';
61 |
62 | addDecorator(StoryRouter());
63 |
64 | // ...your config
65 |
66 | ```
67 |
68 | The important thing is to call `addDecorator` before calling `configure`, otherwise it will not work!
69 |
70 | ## StoryRouter arguments
71 |
72 | The **first argument** is an object that you can use to extend the default behavior.
73 | Every time that a key in the object matches with a path Storybook will call the callback specified for the corresponding value with the destination path as argument.
74 | This way you can for example link stories together using the [`links` addons](https://github.com/storybooks/storybook/tree/master/addons/links) with the linkTo function.
75 |
76 | The match is performed using the [path-to-regexp module](https://www.npmjs.com/package/path-to-regexp) so you can also use parameter names and regexp within the link keys.
77 |
78 | The **second argument** is another object which will be forwarded to the wrapped `MemoryRouter` as [props](https://reacttraining.com/react-router/web/api/MemoryRouter). This allows you to write stories having a specific url location or using advanced functionalities as asking the user confirmation before exiting from a location.
79 |
80 | ## Advanced usage and examples
81 | You can find more examples in the provided [stories](https://github.com/gvaldambrini/storybook-router/tree/master/examples/react-router).
82 | You can run them cloning this repository and executing (supposing you have installed globally [lerna](https://github.com/lerna/lerna)):
83 |
84 | yarn install && yarn bootstrap
85 | yarn storybook-react-examples
86 |
87 | ## Limitations
88 |
89 | As the wrapped Router creates a new history object for each story you cannot pass the history from a story to another one and so you cannot implement a back or forward button which works among stories.
90 |
--------------------------------------------------------------------------------
/examples/vue-router/Navigation.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue';
2 |
3 | import StoryRouter from 'storybook-vue-router';
4 |
5 | const Home = {
6 | template: '