diff --git a/addons/knobs/README.md b/addons/knobs/README.md index f4d24e43eb00..9d5d53f8cd7c 100644 --- a/addons/knobs/README.md +++ b/addons/knobs/README.md @@ -37,7 +37,7 @@ Now, write your stories with knobs. ```js import { storiesOf } from '@storybook/react'; -import { withKnobs, text, boolean, number } from '@storybook/addon-knobs'; +import { addonKnobs, text, boolean, number } from '@storybook/addon-knobs'; const stories = storiesOf('Storybook Knobs', module); @@ -52,14 +52,15 @@ stories.add('with a button', () => ( )) +const options = {}; // Knobs as dynamic variables. -stories.add('as dynamic variables', () => { +stories.add('as dynamic variables', addonKnobs(options)(() => { const name = text('Name', 'Arunoda Susiripala'); const age = number('Age', 89); const content = `I am ${name} and I'm ${age} years old.`; return (
{content}
); -}); +})); ``` You can see your Knobs in a Storybook panel as shown below. diff --git a/addons/knobs/package.json b/addons/knobs/package.json index dd1036fcf75e..0471dfe49344 100644 --- a/addons/knobs/package.json +++ b/addons/knobs/package.json @@ -26,7 +26,8 @@ "prop-types": "^15.5.8", "react-color": "^2.11.4", "react-datetime": "^2.8.10", - "react-textarea-autosize": "^4.3.0" + "react-textarea-autosize": "^4.3.0", + "util-deprecate": "1.0.2" }, "devDependencies": { "@types/node": "^7.0.12", diff --git a/addons/knobs/src/KnobManager.js b/addons/knobs/src/KnobManager.js index 8604493d0ad2..2ec08c6de14b 100644 --- a/addons/knobs/src/KnobManager.js +++ b/addons/knobs/src/KnobManager.js @@ -53,38 +53,6 @@ export default class KnobManager { return knobStore.get(name).value; } - // wrapStory(channel, storyFn, context) { - // this.channel = channel; - // const key = `${context.kind}:::${context.story}`; - // let knobStore = this.knobStoreMap[key]; - - // if (!knobStore) { - // knobStore = this.knobStoreMap[key] = new KnobStore(); // eslint-disable-line - // } - - // this.knobStore = knobStore; - // knobStore.markAllUnused(); - // const initialContent = storyFn(context); - // const props = { context, storyFn, channel, knobStore, initialContent }; - - // if (window.STORYBOOK_ENV === 'vue') { - // channel.on('addon:knobs:knobChange', change => { - // const { name, value } = change; - // // Update the related knob and it's value. - // const knobOptions = knobStore.get(name); - // knobOptions.value = value; - - // knobStore.markAllUnused(); - - // initialContent.$forceUpdate(); - // }); - - // return initialContent; - // } - - // return ; - // } - _mayCallChannel() { // Re rendering of the story may cause changes to the knobStore. Some new knobs maybe added and // Some knobs may go unused. So we need to update the panel accordingly. For example remove the diff --git a/addons/knobs/src/index.js b/addons/knobs/src/index.js index 7b7b59755ea5..eebec91913a9 100644 --- a/addons/knobs/src/index.js +++ b/addons/knobs/src/index.js @@ -1,6 +1,9 @@ -// import { window } from 'global'; +import { window } from 'global'; +import deprecate from 'util-deprecate'; import addons from '@storybook/addons'; import KnobManager from './KnobManager'; +import { vueHandler } from './vue'; +import { reactHandler } from './react'; const manager = new KnobManager(); @@ -56,36 +59,54 @@ export function date(name, value = new Date()) { return manager.knob(name, { type: 'date', value: proxyValue }); } -// export function withKnobs(storyFn, context) { -// const channel = addons.getChannel(); -// return manager.wrapStory(channel, storyFn, context); +function oldKnobs(storyFn, context) { + const channel = addons.getChannel(); + manager.initStore(channel); + return reactHandler(channel, manager.knobStore)(storyFn)(context); +} -// export function withKnobsOptions(options = {}) { -// return (...args) => { -// const channel = addons.getChannel(); -// channel.emit('addon:knobs:setOptions', options); +function oldKnobsWithOptions(options = {}) { + return (...args) => { + const channel = addons.getChannel(); + channel.emit('addon:knobs:setOptions', options); -// return withKnobs(...args); -// }; -// } + return oldKnobs(...args); + }; +} -export function withKnobs() { +Object.defineProperty(exports, 'withKnobs', { + configurable: true, + enumerable: true, + get: deprecate( + () => oldKnobs, + '@storybook/addon-knobs withKnobs decorator is deprecated, use addonKnobs() instead. See https://github.com/storybooks/storybook/tree/master/addons/knobs' + ), +}); + +Object.defineProperty(exports, 'withKnobsOptions', { + configurable: true, + enumerable: true, + get: deprecate( + () => oldKnobsWithOptions, + '@storybook/addon-knobs withKnobsOptions decorator is deprecated, use addonKnobs() instead. See https://github.com/storybooks/storybook/tree/master/addons/knobs' + ), +}); + +export function addonKnobs(options) { const channel = addons.getChannel(); manager.initStore(channel); - return storyFn => context => ({ - render(h) { - const story = storyFn(context); - return h(typeof story === 'string' ? { template: story } : story); - }, - created() { - channel.on('addon:knobs:knobChange', change => { - const { name, value } = change; - // Update the related knob and it's value. - const knobOptions = manager.knobStore.get(name); - knobOptions.value = value; - this.$forceUpdate(); - }); - }, - }); + if (options) channel.emit('addon:knobs:setOptions', options); + + switch (window.STORYBOOK_ENV) { + case 'vue': { + return vueHandler(channel, manager.knobStore); + } + case 'react': { + return reactHandler(channel, manager.knobStore); + } + default: { + return reactHandler(channel, manager.knobStore); + } + } } diff --git a/addons/knobs/src/components/WrapStory.js b/addons/knobs/src/react/WrapStory.js similarity index 96% rename from addons/knobs/src/components/WrapStory.js rename to addons/knobs/src/react/WrapStory.js index 29568e2950ad..a23227978022 100644 --- a/addons/knobs/src/components/WrapStory.js +++ b/addons/knobs/src/react/WrapStory.js @@ -33,6 +33,10 @@ export default class WrapStory extends React.Component { this.props.channel.removeListener('addon:knobs:knobChange', this.knobChanged); this.props.channel.removeListener('addon:knobs:reset', this.resetKnobs); this.props.knobStore.unsubscribe(this.setPaneKnobs); + + // cleanup before leaving + this.props.knobStore.reset(); + this.setPaneKnobs(false); } setPaneKnobs(timestamp = +new Date()) { diff --git a/addons/knobs/src/react/index.js b/addons/knobs/src/react/index.js new file mode 100644 index 000000000000..0b093d446ddc --- /dev/null +++ b/addons/knobs/src/react/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import WrapStory from './WrapStory'; + +/** + * Handles a react story + */ +export const reactHandler = (channel, knobStore) => getStory => context => { + const initialContent = getStory(context); + const props = { context, storyFn: getStory, channel, knobStore, initialContent }; + return ; +}; diff --git a/addons/knobs/src/vue/index.js b/addons/knobs/src/vue/index.js new file mode 100644 index 000000000000..0b653f7c71c1 --- /dev/null +++ b/addons/knobs/src/vue/index.js @@ -0,0 +1,40 @@ +export const vueHandler = (channel, knobStore) => getStory => context => ({ + render(h) { + const story = getStory(context); + return h(typeof story === 'string' ? { template: story } : story); + }, + + methods: { + onKnobChange(change) { + const { name, value } = change; + // Update the related knob and it's value. + const knobOptions = knobStore.get(name); + knobOptions.value = value; + this.$forceUpdate(); + }, + + onKnobReset() { + knobStore.reset(); + this.setPaneKnobs(false); + this.$forceUpdate(); + }, + + setPaneKnobs(timestamp = +new Date()) { + channel.emit('addon:knobs:setKnobs', { knobs: knobStore.getAll(), timestamp }); + }, + }, + + created() { + channel.on('addon:knobs:reset', this.onKnobReset); + channel.on('addon:knobs:knobChange', this.onKnobChange); + knobStore.subscribe(this.setPaneKnobs); + }, + + beforeDestroy(){ + console.log('beforeDestroy'); + channel.removeListener('addon:knobs:reset', this.onKnobReset); + channel.removeListener('addon:knobs:knobChange', this.onKnobChange); + knobStore.unsubscribe(this.setPaneKnobs); + this.onKnobReset(); + } +}); \ No newline at end of file diff --git a/addons/notes/README.md b/addons/notes/README.md index dffefb10732a..fb8d896c28f0 100644 --- a/addons/notes/README.md +++ b/addons/notes/README.md @@ -32,10 +32,10 @@ Then write your stories like this: ```js import { storiesOf } from '@storybook/react'; -import { withNotes } from '@storybook/addon-notes'; +import { addonNotes } from '@storybook/addon-notes'; import Component from './Component'; storiesOf('Component', module) - .add('with some emoji', withNotes({ notes: 'A very simple component'})(() => )); + .add('with some emoji', addonNotes({ notes: 'A very simple component'})(() => )); ``` diff --git a/addons/notes/src/index.js b/addons/notes/src/index.js index 7aff6cb5cbf4..f825eac410d6 100644 --- a/addons/notes/src/index.js +++ b/addons/notes/src/index.js @@ -2,7 +2,7 @@ import deprecate from 'util-deprecate'; import addons from '@storybook/addons'; import { WithNotes as ReactWithNotes } from './react'; -export const withNotes = ({ notes }) => { +export const addonNotes = ({ notes }) => { const channel = addons.getChannel(); return getStory => () => { diff --git a/app/react/src/server/config/globals.js b/app/react/src/server/config/globals.js index ee4d9597bf2a..e95a663712b5 100644 --- a/app/react/src/server/config/globals.js +++ b/app/react/src/server/config/globals.js @@ -1,3 +1,4 @@ /* globals window */ window.STORYBOOK_REACT_CLASSES = {}; +window.STORYBOOK_ENV = 'react'; diff --git a/app/vue/src/client/preview/client_api.js b/app/vue/src/client/preview/client_api.js index 6cae429ada55..75d787d92e87 100644 --- a/app/vue/src/client/preview/client_api.js +++ b/app/vue/src/client/preview/client_api.js @@ -1,5 +1,6 @@ /* eslint no-underscore-dangle: 0 */ import Vue from 'vue'; + export default class ClientApi { constructor({ channel, storyStore }) { // channel can be null when running in node @@ -59,40 +60,18 @@ export default class ClientApi { throw new Error(`Story of "${kind}" named "${storyName}" already exists`); } - const parseStory = (context) => { - const element = getStory(context); - let component = element; - if (typeof component === 'string') { - component = { template: component }; - } else if (typeof component === 'function') { - component = { render: component }; - } - - return component; - } - // 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( + const getDecoratedStory = decorators.reduce( (decorated, decorator) => context => decorator(() => decorated(context), context), getStory ); - - const fnR = (context) => { - return new Vue({ - render(h) { - const story = parseStory(fn(context)); - return h('div', {attrs: { id: 'root' } }, [h(story)]); - }, - }); - } - // Add the fully decorated getStory function. - this._storyStore.addStory(kind, storyName,fnR); + this._storyStore.addStory(kind, storyName, getDecoratedStory); return api; }; diff --git a/app/vue/src/client/preview/render.js b/app/vue/src/client/preview/render.js index 8047f6dff2bf..653cf9ea362d 100644 --- a/app/vue/src/client/preview/render.js +++ b/app/vue/src/client/preview/render.js @@ -11,6 +11,7 @@ const logger = console; // let rootEl = null; let previousKind = ''; let previousStory = ''; +let app = null; export function renderError(error) { const properError = new Error(error.title); @@ -90,8 +91,15 @@ export function renderMain(data, storyStore) { // return renderError(error); // } - element.$mount('#root'); - return null; + if (app) app.$destroy(); + + app = new Vue({ + el: '#root', + render(h) { + const story = typeof element === 'string' ? { template: element } : element; + return h('div', {attrs: { id: 'root' } }, [h(story)]); + }, + }); } export default function renderPreview({ reduxStore, storyStore }) { diff --git a/examples/cra-kitchen-sink/src/stories/index.js b/examples/cra-kitchen-sink/src/stories/index.js index cda162e0773e..2c63eaccfd64 100644 --- a/examples/cra-kitchen-sink/src/stories/index.js +++ b/examples/cra-kitchen-sink/src/stories/index.js @@ -3,7 +3,8 @@ import EventEmiter from 'eventemitter3'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { withNotes, WithNotes } from '@storybook/addon-notes'; +import { addonNotes, WithNotes } from '@storybook/addon-notes'; +import { withKnobs, addonKnobs, text, number } from '@storybook/addon-knobs'; import { linkTo } from '@storybook/addon-links'; import WithEvents from '@storybook/addon-events'; import { withKnobs, text, number } from '@storybook/addon-knobs'; @@ -112,12 +113,12 @@ storiesOf('WithEvents', module) ) .add('Logger', () => ); -storiesOf('withNotes', module) - .add('with some text', withNotes({ notes: 'Hello guys' })(() =>
Hello copain
)) - .add('with some emoji', withNotes({ notes: 'My notes on emojies' })(() =>

🤔😳😯😮

)) +storiesOf('addonNotes', module) + .add('with some text', addonNotes({ notes: 'Hello guys' })(() =>
Hello copain
)) + .add('with some emoji', addonNotes({ notes: 'My notes on emojies' })(() =>

🤔😳😯😮

)) .add( 'with a button and some emoji', - withNotes({ notes: 'My notes on a button with emojies' })(() => + addonNotes({ notes: 'My notes on a button with emojies' })(() => ) ) @@ -126,3 +127,24 @@ storiesOf('withNotes', module) ); + +storiesOf('Addon Knobs deprecated Decorator', module) + .addDecorator(withKnobs) // test deprecated + .add('with dynamic variables deprecated', () => { + const name = text('Name', 'Arunoda Susiripala'); + const age = number('Age', 89); + + const content = `I am ${name} and I'm ${age} years old.`; + return
{content}
; + }); + +storiesOf('Addon Knobs', module).add( + 'with dynamic variables new method', + addonKnobs()(() => { + const name = text('Name', 'Arunoda Susiripala'); + const age = number('Age', 89); + + const content = `I am ${name} and I'm ${age} years old.`; + return
{content}
; + }) +); diff --git a/examples/vue/src/stories/index.js b/examples/vue/src/stories/index.js index 4a2bb37a86b2..d44bd989ad96 100644 --- a/examples/vue/src/stories/index.js +++ b/examples/vue/src/stories/index.js @@ -5,9 +5,9 @@ import { storiesOf } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { linkTo } from '@storybook/addon-links'; -import { withNotes } from '@storybook/addon-notes'; +import { addonNotes } from '@storybook/addon-notes'; -import { withKnobs, text, boolean, number } from '@storybook/addon-knobs'; +import { addonKnobs, text, boolean, number } from '@storybook/addon-knobs'; import MyButton from './Button.vue'; @@ -78,30 +78,30 @@ storiesOf('Other', module) storiesOf('Addon Notes', module) - .add('with some emoji', withNotes({notes: 'My notes on emojies'})(() => '

🤔😳😯😮

')) - .add('with some button', withNotes({ notes: 'My notes on some button' })(() => ({ + .add('with some emoji', addonNotes({notes: 'My notes on emojies'})(() => '

🤔😳😯😮

')) + .add('with some button', addonNotes({ notes: 'My notes on some button' })(() => ({ components: { MyButton }, template: 'rounded' }))) - .add('with some color', withNotes({ notes: 'Some notes on some colored component' })(() => ({ + .add('with some color', addonNotes({ notes: 'Some notes on some colored component' })(() => ({ render(h) { return h(MyButton, { props: { color: 'pink' } }, ['colorful']); } }))) - .add('with some text', withNotes({ notes: 'My notes on some text' })(() => ({ + .add('with some text', addonNotes({ notes: 'My notes on some text' })(() => ({ template: '
Text
' }) )) - .add('with some long text', withNotes({ notes: 'My notes on some long text' })( + .add('with some long text', addonNotes({ notes: 'My notes on some long text' })( () => '
A looooooooonnnnnnnggggggggggggg text
' )) - .add('with some bold text', withNotes({ notes: 'My notes on some bold text' })(() => ({ + .add('with some bold text', addonNotes({ notes: 'My notes on some bold text' })(() => ({ render: h => h('div', [h('strong', ['A very long text to display'])]) }))); storiesOf('Addon Knobs', module) - .add('With some name', withKnobs()(() => { + .add('With some name', addonKnobs()(() => { const name = text('Name', 'Arunoda Susiripala'); const age = number('Age', 89);