Skip to content

Commit

Permalink
Implement a extension API (#258)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
arunoda authored Jun 20, 2016
1 parent b95d2e2 commit 484886a
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 72 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion dist/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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;
57 changes: 50 additions & 7 deletions dist/client/preview/client_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -62,7 +105,7 @@ var ClientApi = function () {
};

api.addDecorator = function (decorator) {
decorators.push(decorator);
localDecorators.push(decorator);
return api;
};

Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion dist/client/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions dist/manager.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/manager.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
@@ -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.
53 changes: 7 additions & 46 deletions docs/writing_stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
2 changes: 2 additions & 0 deletions src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading

0 comments on commit 484886a

Please sign in to comment.