Implement a extension API (#258)

* Implement the new addon API.

* Tested addon and decorator API with a real app.

* Add context support to decorators.

* Add kind to the api.

* Add extension documentation.

* Update extension docs.

* Fix a small typo.
This commit is contained in:
Arunoda Susiripala 2016-06-20 09:33:13 +05:30 committed by GitHub
parent b95d2e2027
commit 484886ac73
12 changed files with 391 additions and 72 deletions

View File

@ -123,6 +123,7 @@ There are many things you can do with React Storybook. You can explore them with
* [Writing Stories](docs/writing_stories.md)
* [Setting up for CSS](docs/setting_up_for_css.md)
* [Configuration APIs](docs/configure_storybook.md)
* [Extensions](docs/extensions.md)
* [Power Tools](https://voice.kadira.io/power-tools-for-react-storybook-d404d7b29b82#.4yodlbqi8)
* [How Storybook Works](docs/how_storybook_works.md)
* [Known Issues](docs/known_issues.md)

View File

@ -3,7 +3,7 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.configure = exports.linkTo = exports.action = exports.storiesOf = undefined;
exports.configure = exports.addDecorator = exports.setAddon = exports.linkTo = exports.action = exports.storiesOf = undefined;
var _preview = require('./preview');
@ -14,4 +14,6 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj;
var storiesOf = exports.storiesOf = previewApi.storiesOf;
var action = exports.action = previewApi.action;
var linkTo = exports.linkTo = previewApi.linkTo;
var setAddon = exports.setAddon = previewApi.setAddon;
var addDecorator = exports.addDecorator = previewApi.addDecorator;
var configure = exports.configure = previewApi.configure;

View File

@ -8,6 +8,14 @@ var _from = require('babel-runtime/core-js/array/from');
var _from2 = _interopRequireDefault(_from);
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
@ -30,9 +38,21 @@ var ClientApi = function () {
this._pageBus = pageBus;
this._storyStore = storyStore;
this._addons = {};
this._globalDecorators = [];
}
(0, _createClass3.default)(ClientApi, [{
key: 'setAddon',
value: function setAddon(addon) {
this._addons = (0, _extends3.default)({}, this._addons, addon);
}
}, {
key: 'addDecorator',
value: function addDecorator(decorator) {
this._globalDecorators.push(decorator);
}
}, {
key: 'storiesOf',
value: function storiesOf(kind, m) {
var _this = this;
@ -43,16 +63,39 @@ var ClientApi = function () {
});
}
var decorators = [];
var api = {};
var localDecorators = [];
var api = {
kind: kind
};
// apply addons
for (var name in this._addons) {
if (this._addons.hasOwnProperty(name)) {
(function () {
var addon = _this._addons[name];
api[name] = function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
addon.apply(api, args);
return api;
};
})();
}
}
api.add = function (storyName, getStory) {
// Wrap the getStory function with each decorator. The first
// decorator will wrap the story function. The second will
// wrap the first decorator and so on.
var decorators = [].concat(localDecorators, (0, _toConsumableArray3.default)(_this._globalDecorators));
var fn = decorators.reduce(function (decorated, decorator) {
return function () {
return decorator(decorated);
return function (context) {
return decorator(function () {
return decorated(context);
}, context);
};
}, getStory);
@ -62,7 +105,7 @@ var ClientApi = function () {
};
api.addDecorator = function (decorator) {
decorators.push(decorator);
localDecorators.push(decorator);
return api;
};
@ -74,8 +117,8 @@ var ClientApi = function () {
var pageBus = this._pageBus;
return function () {
for (var _len = arguments.length, _args = Array(_len), _key = 0; _key < _len; _key++) {
_args[_key] = arguments[_key];
for (var _len2 = arguments.length, _args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
_args[_key2] = arguments[_key2];
}
var args = (0, _from2.default)(_args);

View File

@ -3,7 +3,7 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.configure = exports.linkTo = exports.action = exports.storiesOf = undefined;
exports.configure = exports.addDecorator = exports.setAddon = exports.linkTo = exports.action = exports.storiesOf = undefined;
require('es6-shim');
@ -60,6 +60,8 @@ var configApi = new _config_api2.default(context);
var storiesOf = exports.storiesOf = clientApi.storiesOf.bind(clientApi);
var action = exports.action = clientApi.action.bind(clientApi);
var linkTo = exports.linkTo = clientApi.linkTo.bind(clientApi);
var setAddon = exports.setAddon = clientApi.setAddon.bind(clientApi);
var addDecorator = exports.addDecorator = clientApi.addDecorator.bind(clientApi);
var configure = exports.configure = configApi.configure.bind(configApi);
// initialize the UI

24
dist/manager.js vendored

File diff suppressed because one or more lines are too long

2
dist/manager.js.map vendored
View File

@ -1 +1 @@
{"version":3,"file":"manager.js","sources":["webpack:///manager.js","webpack:///"],"mappings":"AAAA;ACwoIA;;;;;;;;;;;;;;AA0+EA;;;;;;;;;;;AAqlFA;AAk8IA;;;;;;;;;AAgCA;AAyzEA;AAqkGA;AA26FA;AAg1EA;;;;;AAuBA;;;AAm4BA;AAuuHA;AA+8HA;AA2lHA","sourceRoot":""}
{"version":3,"file":"manager.js","sources":["webpack:///manager.js","webpack:///"],"mappings":"AAAA;ACwoIA;;;;;;;;;;;;;;AA0+EA;;;;;;;;;;;AAglFA;AAk8IA;;;;;;;;;AAgCA;AAyzEA;AAqkGA;AAw5FA;AAi3EA;;;;;AA0CA;;;AAm4BA;AAuuHA;AA+8HA;AA2lHA","sourceRoot":""}

95
docs/extensions.md Normal file
View File

@ -0,0 +1,95 @@
# React Storybook Extensions
React Storybook comes with an extensions API to customize the storybook experience. Let's have a look at them.
## TOC
* [API](#api)
* [Decorators](#decorators)
* [Addons](#addons)
* [Available Extensions](#available-extensions)
## API
### Decorators
A decorator is a way to wrap an story with a common set of component(s). Let's say you want to center all your stories. Then this is how we can do it with a decorator:
```js
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import MyComponent from '../my_component';
storiesOf('MyComponent', module)
.addDecorator((story) => (
<div style={{textAlign: 'center'}}>
{story()}
</div>
));
.add('without props', () => (<MyComponent />))
.add('with some props', () => (<MyComponent text="The Comp"/>));
```
Here we only add the decorator for the current set of stories for a given story kind.
But, you can add a decorator **globally** and it'll be applied to all the stories you create. This is how to add a decorator like that.
```js
import { configure, addDecorator } from '@kadira/storybook';
addDecorator((story) => (
<div style={{textAlign: 'center'}}>
{story()}
</div>
));
configure(function () {
...
}, module);
```
### Addons
With an addon, you can introduce new methods to the story creation API. For an example, you can achieve the above centered component functionality with an addon like this:
```js
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import MyComponent from '../my_component';
storiesOf('MyComponent', module)
.addWithCentered('without props', () => (<MyComponent />))
.addWithCentered('with some props', () => (<MyComponent text="The Comp"/>));
```
Here we are using a new API called `addWithCentered`. That's introduce by an addon.
This is how we set that addon.
```js
import { configure, setAddon } from '@kadira/storybook';
setAddon({
addWithCentered(storyName, storyFn) {
// You can access the .add and other API added by addons in here.
this.add(storyName, (context) => (
<div style={{textAlign: "center"}}>
{storyFn(context)}
</div>
));
}
});
configure(function () {
...
}, module);
```
## Available Extensions
Rather than creating extensions yourself, you can use extensions available below:
* [Centered Decorator](https://github.com/kadirahq/react-storybook-decorator-centered)
* [Info addon for displaying propTypes, source and more info](https://github.com/kadirahq/react-storybook-addon-info)
> Feel free to include your extension to the above list and share it with other. <br/>
> Just make it available on NPM (and GitHub) and send a PR to this page.

View File

@ -4,8 +4,8 @@
* [Basic API](#basic-api)
* [Creating Actions](#creating-actions)
* [Using Decorators](#using-decorators)
* [Linking Stories](#linking-stories)
* [Use Extensions](#use-extensions)
You need to write stories to show your components inside React Storybook. We've a set of APIs allows you to write stories and do more with them:
@ -76,51 +76,6 @@ Here we can see the name we've mentioned when creating the action. After that, w
> For simplicity, React Storybook does not show the actual object. Instead it will show `[SyntheticEvent]`.
## Using Decorators
In some apps, we need to wrap our components with a given context. Most of the time, you have to do this when you are using Material UI or Radium.
So, you need to write your stories like this:
```js
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import Theme from '../theme';
import MyComponent from '../my_component';
storiesOf('MyComponent', modules)
.add('without props', () => (
<Theme>
<MyComponent />
</Theme>
))
.add('with some props', () => (
<Theme>
<MyComponent name="Arunoda"/>
</Theme>
));
```
As you can see, you always need to wrap your components with the `Theme` component. But, there's a much better way. See following example with a decorator:
```js
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import Theme from '../theme';
import MyComponent from '../my_component';
storiesOf('MyComponent', modules)
.addDecorator((story) => (
<Theme>
{story()}
</Theme>
))
.add('without props', () => (<MyComponent />))
.add('with some props', () => (<MyComponent name="Arunoda"/>));
```
You can add as many as decorators you want, but make sure to call `.addDecorator()` before you call `.add()`.
## Linking Stories
Sometimes, we may need to link stories. With that, we could use Storybook as a prototype builder. (like [InVision](https://www.invisionapp.com/), [Framer.js](http://framerjs.com/)). Here's how to do that.
@ -153,3 +108,9 @@ With that, you can link an event prop to any story in the Storybook.
> You can also pass a function instead for any of above parameter. That function accepts arguments emitted by the event and it should return a string.
Have a look at [PR86](https://github.com/kadirahq/react-storybook/pull/86) for more information.
## Use Extensions
You can use [React Storybook Extensions](extensions.md) to group common functionalities and reduce the amount of code you need to write. You can also [re-use extensions](extensions.md#available-extensions) created by others.
Have a look at [React Storybook Extensions](extensions.md) for more information.

View File

@ -3,4 +3,6 @@ import * as previewApi from './preview';
export const storiesOf = previewApi.storiesOf;
export const action = previewApi.action;
export const linkTo = previewApi.linkTo;
export const setAddon = previewApi.setAddon;
export const addDecorator = previewApi.addDecorator;
export const configure = previewApi.configure;

View File

@ -0,0 +1,176 @@
import ClientAPI from '../client_api';
const { describe, it } = global;
import { expect } from 'chai';
class StoryStore {
constructor() {
this.stories = [];
}
addStory(kind, story, fn) {
this.stories.push({ kind, story, fn });
}
}
describe('preview.client_api', () => {
describe('setAddon', () => {
it('should register addons', () => {
const api = new ClientAPI({});
let data;
api.setAddon({
aa() {
data = 'foo';
},
});
api.storiesOf('none').aa();
expect(data).to.be.equal('foo');
});
it('should not remove previous addons', () => {
const api = new ClientAPI({});
const data = [];
api.setAddon({
aa() {
data.push('foo');
},
});
api.setAddon({
bb() {
data.push('bar');
},
});
api.storiesOf('none').aa().bb();
expect(data).to.deep.equal(['foo', 'bar']);
});
it('should call with the api context', () => {
const api = new ClientAPI({});
let data;
api.setAddon({
aa() {
data = typeof this.add;
},
});
api.storiesOf('none').aa();
expect(data).to.be.equal('function');
});
it('should be able to access addons added previously', () => {
const api = new ClientAPI({});
let data;
api.setAddon({
aa() {
data = 'foo';
},
});
api.setAddon({
bb() {
this.aa();
},
});
api.storiesOf('none').bb();
expect(data).to.be.equal('foo');
});
it('should be able to access the current kind', () => {
const api = new ClientAPI({});
const kind = 'dfdwf3e3';
let data;
api.setAddon({
aa() {
data = this.kind;
},
});
api.storiesOf(kind).aa();
expect(data).to.be.equal(kind);
});
});
describe('addDecorator', () => {
it('should add local decorators', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
const localApi = api.storiesOf('none');
localApi.addDecorator(function (fn) {
return `aa-${fn()}`;
});
localApi.add('storyName', () => ('Hello'));
expect(storyStore.stories[0].fn()).to.be.equal('aa-Hello');
});
it('should add global decorators', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
api.addDecorator(function (fn) {
return `bb-${fn()}`;
});
const localApi = api.storiesOf('none');
localApi.add('storyName', () => ('Hello'));
expect(storyStore.stories[0].fn()).to.be.equal('bb-Hello');
});
it('should utilize both decorators at once', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
const localApi = api.storiesOf('none');
api.addDecorator(function (fn) {
return `aa-${fn()}`;
});
localApi.addDecorator(function (fn) {
return `bb-${fn()}`;
});
localApi.add('storyName', () => ('Hello'));
expect(storyStore.stories[0].fn()).to.be.equal('aa-bb-Hello');
});
it('should pass the context', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
const localApi = api.storiesOf('none');
localApi.addDecorator(function (fn) {
return `aa-${fn()}`;
});
localApi.add('storyName', ({ kind, story }) => (`${kind}-${story}`));
const kind = 'dfdfd';
const story = 'ef349ff';
const result = storyStore.stories[0].fn({ kind, story });
expect(result).to.be.equal(`aa-${kind}-${story}`);
});
it('should have access to the context', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
const localApi = api.storiesOf('none');
localApi.addDecorator(function (fn, { kind, story }) {
return `${kind}-${story}-${fn()}`;
});
localApi.add('storyName', () => ('Hello'));
const kind = 'dfdfd';
const story = 'ef349ff';
const result = storyStore.stories[0].fn({ kind, story });
expect(result).to.be.equal(`${kind}-${story}-Hello`);
});
});
});

View File

@ -4,6 +4,19 @@ export default class ClientApi {
constructor({ pageBus, storyStore }) {
this._pageBus = pageBus;
this._storyStore = storyStore;
this._addons = {};
this._globalDecorators = [];
}
setAddon(addon) {
this._addons = {
...this._addons,
...addon,
};
}
addDecorator(decorator) {
this._globalDecorators.push(decorator);
}
storiesOf(kind, m) {
@ -13,15 +26,37 @@ export default class ClientApi {
});
}
const decorators = [];
const api = {};
const localDecorators = [];
const api = {
kind,
};
// apply addons
for (const name in this._addons) {
if (this._addons.hasOwnProperty(name)) {
const addon = this._addons[name];
api[name] = (...args) => {
addon.apply(api, args);
return api;
};
}
}
api.add = (storyName, getStory) => {
// Wrap the getStory function with each decorator. The first
// decorator will wrap the story function. The second will
// wrap the first decorator and so on.
const decorators = [
...localDecorators,
...this._globalDecorators,
];
const fn = decorators.reduce((decorated, decorator) => {
return () => decorator(decorated);
return (context) => {
return decorator(() => {
return decorated(context);
}, context);
};
}, getStory);
// Add the fully decorated getStory function.
@ -30,7 +65,7 @@ export default class ClientApi {
};
api.addDecorator = decorator => {
decorators.push(decorator);
localDecorators.push(decorator);
return api;
};

View File

@ -27,6 +27,8 @@ init(context);
export const storiesOf = clientApi.storiesOf.bind(clientApi);
export const action = clientApi.action.bind(clientApi);
export const linkTo = clientApi.linkTo.bind(clientApi);
export const setAddon = clientApi.setAddon.bind(clientApi);
export const addDecorator = clientApi.addDecorator.bind(clientApi);
export const configure = configApi.configure.bind(configApi);
// initialize the UI