Skip to content

Commit

Permalink
withActions decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypnosphi committed May 2, 2018
1 parent 6df6a3c commit 100985b
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 20 deletions.
23 changes: 21 additions & 2 deletions addons/actions/README.md
Expand Up @@ -55,10 +55,10 @@ import { actions } from '@storybook/addon-actions';
import Button from './button';

// This will lead to { onClick: action('onClick'), ... }
const eventsFromNames = actions('onClick', 'onDoubleClick');
const eventsFromNames = actions('onClick', 'onMouseOver');

// This will lead to { onClick: action('clicked'), ... }
const eventsFromObject = actions({ onClick: 'clicked', onDoubleClick: 'double clicked' });
const eventsFromObject = actions({ onClick: 'clicked', onMouseOver: 'hovered' });

storiesOf('Button', module)
.add('default view', () => <Button {...eventsFromNames}>Hello World!</Button>)
Expand Down Expand Up @@ -123,3 +123,22 @@ action('my-action', {
|`depth`|Number|Configures the transfered depth of any logged objects.|`10`|
|`clearOnStoryChange`|Boolean|Flag whether to clear the action logger when switching away from the current story.|`true`|
|`limit`|Number|Limits the number of items logged in the action logger|`50`|

## withActions decorator

You can define action handles in a declarative way using `withActions` decorators. It accepts the same arguments as [`actions`](#multiple-actions)
Keys have `'<eventName> <selector>'` format, e.g. `'click .btn'`. Selector is optional. This can be used with any framework but is especially useful for `@storybook/html`.

```js
import { storiesOf } from '@storybook/html';
import { withActions } from '@storybook/addon-actions';

storiesOf('button', module)
// Log mousovers on entire story and clicks on .btn
.addDecorator(withActions('mouseover', 'click .btn'))
.add('with actions', () => `
<div>
Clicks on this button will be logged: <button class="btn" type="button">Button</button>
</div>
`);
```
2 changes: 2 additions & 0 deletions addons/actions/package.json
Expand Up @@ -22,11 +22,13 @@
"dependencies": {
"@storybook/addons": "4.0.0-alpha.4",
"@storybook/components": "4.0.0-alpha.4",
"@storybook/core-events": "4.0.0-alpha.4",
"babel-runtime": "^6.26.0",
"deep-equal": "^1.0.1",
"glamor": "^2.20.40",
"glamorous": "^4.12.5",
"global": "^4.3.2",
"lodash.isequal": "^4.5.0",
"make-error": "^1.3.4",
"prop-types": "^15.6.1",
"react-inspector": "^2.3.0",
Expand Down
11 changes: 9 additions & 2 deletions addons/actions/src/index.js
@@ -1,8 +1,15 @@
import { action, actions, decorate, configureActions, decorateAction } from './preview';
import {
action,
actions,
decorate,
configureActions,
decorateAction,
withActions,
} from './preview';

// addons, panels and events get unique names using a prefix
export const ADDON_ID = 'storybook/actions';
export const PANEL_ID = `${ADDON_ID}/actions-panel`;
export const EVENT_ID = `${ADDON_ID}/action-event`;

export { action, actions, decorate, configureActions, decorateAction };
export { action, actions, decorate, configureActions, decorateAction, withActions };
19 changes: 11 additions & 8 deletions addons/actions/src/preview/decorateAction.js
@@ -1,5 +1,6 @@
import action from './action';
import actions from './actions';
import { createDecorator } from './withActions';

function applyDecorators(decorators, actionCallback) {
return (..._args) => {
Expand All @@ -17,15 +18,17 @@ export function decorateAction(decorators) {

export function decorate(decorators) {
const decorated = decorateAction(decorators);
const decoratedActions = (...args) => {
const rawActions = actions(...args);
const actionsObject = {};
Object.keys(rawActions).forEach(name => {
actionsObject[name] = applyDecorators(decorators, rawActions[name]);
});
return actionsObject;
};
return {
action: decorated,
actions: (...args) => {
const rawActions = actions(...args);
const decoratedActions = {};
Object.keys(rawActions).forEach(name => {
decoratedActions[name] = applyDecorators(decorators, rawActions[name]);
});
return decoratedActions;
},
actions: decoratedActions,
withActions: createDecorator(decoratedActions),
};
}
1 change: 1 addition & 0 deletions addons/actions/src/preview/index.js
Expand Up @@ -2,3 +2,4 @@ export { default as action } from './action';
export { default as actions } from './actions';
export { configureActions } from './configureActions';
export { decorateAction, decorate } from './decorateAction';
export { default as withActions } from './withActions';
64 changes: 64 additions & 0 deletions addons/actions/src/preview/withActions.js
@@ -0,0 +1,64 @@
// Based on http://backbonejs.org/docs/backbone.html#section-164
import { document, Element } from 'global';
import isEqual from 'lodash.isequal';
import addons from '@storybook/addons';
import Events from '@storybook/core-events';

import actions from './actions';

let lastSubscription;
let lastArgs;

const delegateEventSplitter = /^(\S+)\s*(.*)$/;

const isIE = !Element.prototype.matches;
const matchesMethod = isIE ? 'msMatchesSelector' : 'matches';

const root = document.getElementById('root');

const hasMatchInAncestry = (element, selector) => {
if (element[matchesMethod](selector)) {
return true;
}
const parent = element.parentElement;
if (!parent) {
return false;
}
return hasMatchInAncestry(parent, selector);
};

const createHandlers = (actionsFn, ...args) => {
const actionsObject = actionsFn(...args);
return Object.entries(actionsObject).map(([key, action]) => {
// eslint-disable-next-line no-unused-vars
const [_, eventName, selector] = key.match(delegateEventSplitter);
return {
eventName,
handler: e => {
if (!selector || hasMatchInAncestry(e.target, selector)) {
action(e);
}
},
};
});
};

const actionsSubscription = (...args) => {
if (!isEqual(args, lastArgs)) {
lastArgs = args;
const handlers = createHandlers(...args);
lastSubscription = () => {
handlers.forEach(({ eventName, handler }) => root.addEventListener(eventName, handler));
return () =>
handlers.forEach(({ eventName, handler }) => root.removeEventListener(eventName, handler));
};
}
return lastSubscription;
};

export const createDecorator = actionsFn => (...args) => story => {
addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, actionsSubscription(actionsFn, ...args));
return story();
};

export default createDecorator(actions);
4 changes: 2 additions & 2 deletions app/html/src/client/preview/render.js
Expand Up @@ -14,8 +14,8 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai
rootElement.appendChild(component);
} else {
showError({
message: `Expecting an HTML snippet or DOM node from the story: "${selectedStory}" of "${selectedKind}".`,
stack: stripIndents`
title: `Expecting an HTML snippet or DOM node from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the HTML snippet from the story?
Use "() => <your snippet or node>" or when defining the story.
`,
Expand Down
4 changes: 2 additions & 2 deletions app/polymer/src/client/preview/render.js
Expand Up @@ -9,8 +9,8 @@ export default function renderMain({ story, selectedKind, selectedStory, showMai

if (!component) {
showError({
message: `Expecting a Polymer component from the story: "${selectedStory}" of "${selectedKind}".`,
stack: stripIndents`
title: `Expecting a Polymer component from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the Polymer component from the story?
Use "() => '&lt;your-component-name&gt;&lt;/your-component-name\&gt;'" when defining the story.
`,
Expand Down
4 changes: 2 additions & 2 deletions app/vue/src/client/preview/render.js
Expand Up @@ -23,8 +23,8 @@ export default function render({

if (!component) {
showError({
message: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`,
stack: stripIndents`
title: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`,
description: stripIndents`
Did you forget to return the Vue component from the story?
Use "() => ({ template: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" when defining the story.
`,
Expand Down
34 changes: 34 additions & 0 deletions examples/html-kitchen-sink/stories/addon-actions.stories.js
@@ -0,0 +1,34 @@
import { storiesOf } from '@storybook/html';
import { withActions, decorate } from '@storybook/addon-actions';

const pickTarget = decorate([args => [args[0].target]]);

const button = () => `<button type="button">Hello World</button>`;

storiesOf('Addons|Actions', module)
.add('Hello World', () => withActions('click')(button))
.add('Multiple actions', () => withActions('click', 'contextmenu')(button))
.add('Multiple actions + config', () =>
withActions('click', 'contextmenu', { clearOnStoryChange: false })(button)
)
.add('Multiple actions, object', () =>
withActions({ click: 'clicked', contextmenu: 'right clicked' })(button)
)
.add('Multiple actions, selector', () =>
withActions({ 'click .btn': 'clicked', contextmenu: 'right clicked' })(
() => `
<div>
Clicks on this button will be logged: <button class="btn" type="button">Button</button>
</div>
`
)
)
.add('Multiple actions, object + config', () =>
withActions({ click: 'clicked', contextmenu: 'right clicked' }, { clearOnStoryChange: false })(
button
)
)
.add('Decorated actions', () => pickTarget.withActions('click', 'contextmenu')(button))
.add('Decorated actions + config', () =>
pickTarget.withActions('click', 'contextmenu', { clearOnStoryChange: false })(button)
);
3 changes: 1 addition & 2 deletions examples/official-storybook/stories/addon-actions.stories.js
@@ -1,4 +1,3 @@
/* global window */
import React from 'react';
import { storiesOf } from '@storybook/react';
import {
Expand All @@ -10,7 +9,7 @@ import {
} from '@storybook/addon-actions';
import { setOptions } from '@storybook/addon-options';
import { Button } from '@storybook/react/demo';
import { File } from 'global';
import { window, File } from 'global';

const pickNative = decorate([args => [args[0].nativeEvent]]);
const pickNativeAction = decorateAction([args => [args[0].nativeEvent]]);
Expand Down
4 changes: 4 additions & 0 deletions yarn.lock
Expand Up @@ -9909,6 +9909,10 @@ lodash.isequal@^3.0:
lodash._baseisequal "^3.0.0"
lodash._bindcallback "^3.0.0"

lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"

lodash.isobject@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
Expand Down

0 comments on commit 100985b

Please sign in to comment.