Skip to content

Commit

Permalink
Merge pull request #1267 from storybooks/add-app-vue
Browse files Browse the repository at this point in the history
WIP - add vue support
  • Loading branch information
shilman authored Jul 2, 2017
2 parents 80f7ab2 + 83bb874 commit cf94190
Show file tree
Hide file tree
Showing 107 changed files with 3,791 additions and 209 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ node_modules
app/**/demo/**
docs/public

vue

*.bundle.js
*.js.map

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ It allows you to browse a component library, view the different states of each c

Storybook runs outside of your app. This allows you to develop UI components in isolation, which can improve component reuse, testability, and development speed. You can build quickly without having to worry about application-specific dependencies.

Here are some featured examples that you can reference to see how Storybook works: https://storybook.js.org/examples/
Here are some featured examples that you can reference to see how Storybook works: <https://storybook.js.org/examples/>

Storybook comes with a lot of [addons](https://storybook.js.org/addons/introduction/) for component design, documentation, testing, interactivity, and so on. Storybook's easy-to-use API makes it easy to configure and extend in various ways. It has even been extended to support React Native development for mobile.

Expand Down Expand Up @@ -48,11 +48,13 @@ getstorybook
Once it's installed, you can `npm run storybook` and it will run the development server on your local machine, and give you a URL to browse some sample stories.

**Storybook v2.x migration note**: If you're using Storybook v2.x and want to shift to 3.x version the easiest way is:

```sh
npm i -g @storybook/cli
cd my-storybook-v2-app
getstorybook
```

It runs a codemod to update all package names. Read all migration details in our [Migration Guide](MIGRATION.md)

For full documentation on using Storybook visit: [storybook.js.org](https://storybook.js.org)
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- [See multiple (or all) stories in 1 preview.](#see-multiple-or-all-stories-in-1-preview)
- [Deeper level hierarchy](#deeper-level-hierarchy)
- [Supporting other frameworks and libraries](#supporting-other-frameworks-and-libraries)
- [Vue](#vue) (*in development*)
- [Vue](#vue)
- [Angular](#angular)
- [Webcomponents](#webcomponents)
- [Polymer](#polymer)
Expand Down
7 changes: 4 additions & 3 deletions addons/knobs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -52,14 +52,15 @@ stories.add('with a button', () => (
</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 (<div>{content}</div>);
});
}));
```

You can see your Knobs in a Storybook panel as shown below.
Expand Down
6 changes: 4 additions & 2 deletions addons/knobs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@storybook/addon-knobs",
"version": "3.1.6",
"version": "3.2.0-alpha.0",
"description": "Storybook Addon Prop Editor Component",
"license": "MIT",
"main": "dist/index.js",
Expand All @@ -25,9 +25,11 @@
"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": {
"vue": "2.3.4",
"@types/node": "^7.0.12",
"@types/react": "^15.0.21",
"git-url-parse": "^6.2.2",
Expand Down
25 changes: 3 additions & 22 deletions addons/knobs/src/KnobManager.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/* eslint no-underscore-dangle: 0 */

import React from 'react';
import deepEqual from 'deep-equal';
import WrapStory from './components/WrapStory';
import KnobStore from './KnobStore';

// This is used by _mayCallChannel to determine how long to wait to before triggering a panel update
const PANEL_UPDATE_INTERVAL = 400;

export default class KnobManager {
constructor() {
this.knobStore = null;
this.knobStoreMap = {};
constructor(channel) {
this.channel = channel;
this.knobStore = new KnobStore();
}

knob(name, options) {
Expand All @@ -37,22 +34,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 };
return <WrapStory {...props} />;
}

_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
Expand Down
21 changes: 0 additions & 21 deletions addons/knobs/src/KnobManager.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { shallow } from 'enzyme'; // eslint-disable-line
import KnobManager from './KnobManager';

Expand Down Expand Up @@ -74,24 +73,4 @@ describe('KnobManager', () => {
});
});
});

describe('wrapStory()', () => {
it('should contain the story and add correct props', () => {
const testManager = new KnobManager();

const testChannel = { emit: () => {} };
const testStory = () => <div id="test-story">Test Content</div>;
const testContext = {
kind: 'Foo',
story: 'bar baz',
};
const wrappedStory = testManager.wrapStory(testChannel, testStory, testContext);
const wrapper = shallow(wrappedStory);
expect(wrapper.find('#test-story').length).toBe(1);

const storyWrapperProps = wrappedStory.props;
expect(storyWrapperProps.channel).toEqual(testChannel);
expect(storyWrapperProps.context).toEqual(testContext);
});
});
});
7 changes: 7 additions & 0 deletions addons/knobs/src/components/Panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ export default class Panel extends React.Component {
this.loadedFromUrl = false;
this.props.channel.on('addon:knobs:setKnobs', this.setKnobs);
this.props.channel.on('addon:knobs:setOptions', this.setOptions);

this.stopListeningOnStory = this.props.api.onStory(() => {
this.setState({ knobs: [] });
this.props.channel.emit('addon:knobs:reset');
});
}

componentWillUnmount() {
this.props.channel.removeListener('addon:knobs:setKnobs', this.setKnobs);
this.stopListeningOnStory();
}

setOptions(options = { debounce: false, timestamps: false }) {
Expand Down Expand Up @@ -155,6 +161,7 @@ Panel.propTypes = {
}).isRequired,
onReset: PropTypes.object, // eslint-disable-line
api: PropTypes.shape({
onStory: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
Expand Down
15 changes: 14 additions & 1 deletion addons/knobs/src/components/__tests__/Panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import Panel from '../Panel';
describe('Panel', () => {
it('should subscribe to setKnobs event of channel', () => {
const testChannel = { on: jest.fn() };
shallow(<Panel channel={testChannel} />);
const testApi = { onStory: jest.fn() };
shallow(<Panel channel={testChannel} api={testApi} />);
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function));
});

it('should subscribe to onStory event', () => {
const testChannel = { on: jest.fn() };
const testApi = { onStory: jest.fn() };
shallow(<Panel channel={testChannel} api={testApi} />);

expect(testApi.onStory).toHaveBeenCalled();
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:setKnobs', jasmine.any(Function));
});

Expand All @@ -28,6 +38,7 @@ describe('Panel', () => {
const testApi = {
getQueryParam: key => testQueryParams[key],
setQueryParams: jest.fn(),
onStory: jest.fn(),
};

shallow(<Panel channel={testChannel} api={testApi} />);
Expand Down Expand Up @@ -74,6 +85,7 @@ describe('Panel', () => {
const testApi = {
getQueryParam: key => testQueryParams[key],
setQueryParams: jest.fn(),
onStory: jest.fn(),
};

const wrapper = shallow(<Panel channel={testChannel} api={testApi} />);
Expand Down Expand Up @@ -115,6 +127,7 @@ describe('Panel', () => {
const testApi = {
getQueryParam: jest.fn(),
setQueryParams: jest.fn(),
onStory: jest.fn(),
};

const wrapper = shallow(<Panel channel={testChannel} api={testApi} />);
Expand Down
51 changes: 44 additions & 7 deletions addons/knobs/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
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();
const channel = addons.getChannel();
const manager = new KnobManager(channel);

export function knob(name, options) {
return manager.knob(name, options);
Expand Down Expand Up @@ -55,16 +60,48 @@ 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) {
return reactHandler(channel, manager.knobStore)(storyFn)(context);
}

export function withKnobsOptions(options = {}) {
function oldKnobsWithOptions(options = {}) {
return (...args) => {
const channel = addons.getChannel();
channel.emit('addon:knobs:setOptions', options);

return withKnobs(...args);
return oldKnobs(...args);
};
}

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) {
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);
}
}
}
File renamed without changes.
11 changes: 11 additions & 0 deletions addons/knobs/src/react/index.js
Original file line number Diff line number Diff line change
@@ -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 <WrapStory {...props} />;
};
27 changes: 27 additions & 0 deletions addons/knobs/src/react/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { reactHandler } from './index';
import { shallow } from 'enzyme'; // eslint-disable-line
import KnobStore from '../KnobStore';

describe('React Handler', () => {
describe('wrapStory', () => {
it('should contain the story and add correct props', () => {
const testChannel = { emit: () => {} };
const testStory = () => <div id="test-story">Test Content</div>;
const testContext = {
kind: 'Foo',
story: 'bar baz',
};

const testStore = new KnobStore();

const wrappedStory = reactHandler(testChannel, testStore)(testStory)(testContext);
const wrapper = shallow(wrappedStory);
expect(wrapper.find('#test-story').length).toBe(1);

const storyWrapperProps = wrappedStory.props;
expect(storyWrapperProps.channel).toEqual(testChannel);
expect(storyWrapperProps.context).toEqual(testContext);
});
});
});
37 changes: 37 additions & 0 deletions addons/knobs/src/vue/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export const vueHandler = (channel, knobStore) => getStory => context => ({
render(h) {
return h(getStory(context));
},

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(){
channel.removeListener('addon:knobs:reset', this.onKnobReset);
channel.removeListener('addon:knobs:knobChange', this.onKnobChange);
knobStore.unsubscribe(this.setPaneKnobs);
}
});
Loading

0 comments on commit cf94190

Please sign in to comment.