mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 16:42:19 +08:00
commit
7e9621af6a
@ -2,12 +2,15 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
function getRenderedTree(story, context) {
|
function getRenderedTree(story, context) {
|
||||||
const storyElement = story.render(context);
|
const component = story.render(context);
|
||||||
|
|
||||||
const Constructor = Vue.extend(storyElement);
|
const vm = new Vue({
|
||||||
const vm = new Constructor().$mount();
|
render(h) {
|
||||||
|
return h(component);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return vm.$el;
|
return vm.$mount().$el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getRenderedTree;
|
export default getRenderedTree;
|
||||||
|
@ -29,3 +29,20 @@ You can also build a [static version](https://storybook.js.org/basics/exporting-
|
|||||||
## Vue Notes
|
## Vue Notes
|
||||||
|
|
||||||
- When using global custom components or extension (e.g `Vue.use`). You will need to declare those in the `./storybook/config.js`.
|
- When using global custom components or extension (e.g `Vue.use`). You will need to declare those in the `./storybook/config.js`.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
In Storybook story and decorator components you can not access the Vue instance
|
||||||
|
in factory functions for default prop values:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
foo: {
|
||||||
|
default() {
|
||||||
|
return this.bar; // does not work!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -1,30 +1,66 @@
|
|||||||
import { start } from '@storybook/core/client';
|
import { start } from '@storybook/core/client';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
import './globals';
|
import './globals';
|
||||||
import render from './render';
|
import render, { VALUES } from './render';
|
||||||
|
import { extractProps } from './util';
|
||||||
|
|
||||||
const createWrapperComponent = Target => ({
|
export const WRAPS = 'STORYBOOK_WRAPS';
|
||||||
functional: true,
|
|
||||||
render(h, c) {
|
|
||||||
return h(Target, c.data, c.children);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const decorateStory = (getStory, decorators) =>
|
|
||||||
decorators.reduce(
|
|
||||||
(decorated, decorator) => context => {
|
|
||||||
const story = () => decorated(context);
|
|
||||||
let decoratedStory = decorator(story, context);
|
|
||||||
|
|
||||||
if (typeof decoratedStory === 'string') {
|
function prepare(rawStory, innerStory) {
|
||||||
decoratedStory = { template: decoratedStory };
|
let story = rawStory;
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
if (!story._isVue) {
|
||||||
|
if (typeof story === 'string') {
|
||||||
|
story = { template: story };
|
||||||
|
}
|
||||||
|
if (innerStory) {
|
||||||
|
story.components = { ...(story.components || {}), story: innerStory };
|
||||||
|
}
|
||||||
|
story = Vue.extend(story);
|
||||||
|
} else if (story.options[WRAPS]) {
|
||||||
|
return story;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoratedStory.components = decoratedStory.components || {};
|
return Vue.extend({
|
||||||
decoratedStory.components.story = createWrapperComponent(story());
|
[WRAPS]: story,
|
||||||
return decoratedStory;
|
[VALUES]: { ...(innerStory ? innerStory.options[VALUES] : {}), ...extractProps(story) },
|
||||||
|
functional: true,
|
||||||
|
render(h, { data, parent, children }) {
|
||||||
|
return h(
|
||||||
|
story,
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
props: { ...(data.props || {}), ...parent.$root[VALUES] },
|
||||||
},
|
},
|
||||||
getStory
|
children
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorateStory(getStory, decorators) {
|
||||||
|
return decorators.reduce(
|
||||||
|
(decorated, decorator) => context => {
|
||||||
|
let story;
|
||||||
|
const decoratedStory = decorator(() => {
|
||||||
|
story = decorated(context);
|
||||||
|
return story;
|
||||||
|
}, context);
|
||||||
|
|
||||||
|
if (!story) {
|
||||||
|
story = decorated(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoratedStory === story) {
|
||||||
|
return story;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepare(decoratedStory, story);
|
||||||
|
},
|
||||||
|
context => prepare(getStory(context))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { clientApi, configApi, forceReRender } = start(render, { decorateStory });
|
const { clientApi, configApi, forceReRender } = start(render, { decorateStory });
|
||||||
|
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
import { stripIndents } from 'common-tags';
|
import { stripIndents } from 'common-tags';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
let root = null;
|
export const COMPONENT = 'STORYBOOK_COMPONENT';
|
||||||
|
export const VALUES = 'STORYBOOK_VALUES';
|
||||||
|
|
||||||
function getComponentProxy(component) {
|
const root = new Vue({
|
||||||
return Object.entries(component.props || {})
|
data() {
|
||||||
.map(([name, def]) => ({ [name]: def.default }))
|
return {
|
||||||
.reduce((wrap, prop) => ({ ...wrap, ...prop }), {});
|
[COMPONENT]: undefined,
|
||||||
}
|
[VALUES]: {},
|
||||||
|
};
|
||||||
function renderRoot(component, proxy) {
|
|
||||||
root = new Vue({
|
|
||||||
el: '#root',
|
|
||||||
beforeCreate() {
|
|
||||||
this.proxy = proxy;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render(h) {
|
render(h) {
|
||||||
const props = this.proxy;
|
const children = this[COMPONENT] ? [h(this[COMPONENT])] : undefined;
|
||||||
return h('div', { attrs: { id: 'root' } }, [h(component, { props })]);
|
return h('div', { attrs: { id: 'root' } }, children);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export default function render({
|
export default function render({
|
||||||
story,
|
story,
|
||||||
@ -49,15 +43,14 @@ export default function render({
|
|||||||
|
|
||||||
showMain();
|
showMain();
|
||||||
|
|
||||||
const proxy = getComponentProxy(component);
|
|
||||||
|
|
||||||
// at component creation || refresh by HMR
|
// at component creation || refresh by HMR
|
||||||
if (!root || !forceRender) {
|
if (!root[COMPONENT] || !forceRender) {
|
||||||
if (root) root.$destroy();
|
root[COMPONENT] = component;
|
||||||
|
}
|
||||||
|
|
||||||
renderRoot(component, proxy);
|
root[VALUES] = component.options[VALUES];
|
||||||
} else {
|
|
||||||
root.proxy = proxy;
|
if (!root.$el) {
|
||||||
root.$forceUpdate();
|
root.$mount('#root');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
app/vue/src/client/preview/util.js
Normal file
20
app/vue/src/client/preview/util.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
function getType(fn) {
|
||||||
|
const match = fn && fn.toString().match(/^\s*function (\w+)/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/vuejs/vue/blob/dev/src/core/util/props.js#L92
|
||||||
|
function resolveDefault({ type, default: def }) {
|
||||||
|
if (typeof def === 'function' && getType(type) !== 'Function') {
|
||||||
|
// known limitation: we dont have the component instance to pass
|
||||||
|
return def.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractProps(component) {
|
||||||
|
return Object.entries(component.options.props || {})
|
||||||
|
.map(([name, prop]) => ({ [name]: resolveDefault(prop) }))
|
||||||
|
.reduce((wrap, prop) => ({ ...wrap, ...prop }), {});
|
||||||
|
}
|
@ -1,18 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Storyshots Addon|Centered rounded 1`] = `
|
exports[`Storyshots Addon|Centered rounded 1`] = `
|
||||||
<div
|
|
||||||
style="position: fixed; top: 0px; left: 0px; bottom: 0px; right: 0px; display: flex; align-items: center; overflow: auto;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="margin: auto; max-height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="position: fixed; top: 0px; left: 0px; bottom: 0px; right: 0px; display: flex; align-items: center; overflow: auto;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="margin: auto; max-height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style="position: fixed; top: 0px; left: 0px; bottom: 0px; right: 0px; display: flex; align-items: center; overflow: auto;"
|
style="position: fixed; top: 0px; left: 0px; bottom: 0px; right: 0px; display: flex; align-items: center; overflow: auto;"
|
||||||
>
|
>
|
||||||
@ -27,8 +15,4 @@ exports[`Storyshots Addon|Centered rounded 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`Storyshots Addon|Knobs All knobs 1`] = `
|
exports[`Storyshots Addon|Knobs All knobs 1`] = `
|
||||||
<div
|
<div
|
||||||
style="padding: 8px 22px; border-radius: 8px;"
|
style="border: 2px dotted; padding: 8px 22px; border-radius: 8px;"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
My name is Jane,
|
My name is Jane,
|
||||||
@ -46,8 +46,6 @@ exports[`Storyshots Addon|Knobs Simple 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Addon|Knobs XSS safety 1`] = `
|
exports[`Storyshots Addon|Knobs XSS safety 1`] = `
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<img src=x onerror="alert('XSS Attack')" >
|
<img src=x onerror="alert('XSS Attack')" >
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Storyshots Custom|Decorator for Vue render 1`] = `
|
exports[`Storyshots Custom|Decorator for Vue render 1`] = `
|
||||||
<div
|
|
||||||
style="border: medium solid blue;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="border: medium solid blue;"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style="border: medium solid blue;"
|
style="border: medium solid blue;"
|
||||||
>
|
>
|
||||||
@ -20,17 +14,9 @@ exports[`Storyshots Custom|Decorator for Vue render 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Custom|Decorator for Vue template 1`] = `
|
exports[`Storyshots Custom|Decorator for Vue template 1`] = `
|
||||||
<div
|
|
||||||
style="border: medium solid blue;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="border: medium solid blue;"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style="border: medium solid blue;"
|
style="border: medium solid blue;"
|
||||||
>
|
>
|
||||||
@ -45,6 +31,4 @@ exports[`Storyshots Custom|Decorator for Vue template 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -38,53 +38,72 @@ storiesOf('Addon|Knobs', module)
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.add('All knobs', () => {
|
.add('All knobs', () => {
|
||||||
const name = text('Name', 'Jane');
|
|
||||||
const stock = number('Stock', 20, {
|
|
||||||
range: true,
|
|
||||||
min: 0,
|
|
||||||
max: 30,
|
|
||||||
step: 5,
|
|
||||||
});
|
|
||||||
const fruits = {
|
const fruits = {
|
||||||
Apple: 'apples',
|
Apple: 'apples',
|
||||||
Banana: 'bananas',
|
Banana: 'bananas',
|
||||||
Cherry: 'cherries',
|
Cherry: 'cherries',
|
||||||
};
|
};
|
||||||
const fruit = select('Fruit', fruits, 'apples');
|
|
||||||
const price = number('Price', 2.25);
|
|
||||||
|
|
||||||
const colour = color('Border', 'deeppink');
|
|
||||||
const today = date('Today', new Date('Jan 20 2017 GMT+0'));
|
|
||||||
const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
|
|
||||||
const nice = boolean('Nice', true);
|
|
||||||
|
|
||||||
const stockMessage = stock
|
|
||||||
? `I have a stock of ${stock} ${fruit}, costing $${price} each.`
|
|
||||||
: `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
|
|
||||||
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
|
|
||||||
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
|
|
||||||
|
|
||||||
button('Arbitrary action', action('You clicked it!'));
|
button('Arbitrary action', action('You clicked it!'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
props: {
|
||||||
|
name: { default: text('Name', 'Jane') },
|
||||||
|
stock: {
|
||||||
|
default: number('Stock', 20, {
|
||||||
|
range: true,
|
||||||
|
min: 0,
|
||||||
|
max: 30,
|
||||||
|
step: 5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
fruit: { default: select('Fruit', fruits, 'apples') },
|
||||||
|
price: { default: number('Price', 2.25) },
|
||||||
|
colour: { default: color('Border', 'deeppink') },
|
||||||
|
today: { default: date('Today', new Date('Jan 20 2017 GMT+0')) },
|
||||||
|
// this is necessary, because we cant use arrays/objects directly in vue prop default values
|
||||||
|
// a factory function is required, but we need to make sure the knob is only called once
|
||||||
|
items: { default: (items => () => items)(array('Items', ['Laptop', 'Book', 'Whiskey'])) },
|
||||||
|
nice: { default: boolean('Nice', true) },
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
dateOptions: { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' },
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
stockMessage() {
|
||||||
|
return this.stock
|
||||||
|
? `I have a stock of ${this.stock} ${this.fruit}, costing $${this.price} each.`
|
||||||
|
: `I'm out of ${this.fruit}${this.nice ? ', Sorry!' : '.'}`;
|
||||||
|
},
|
||||||
|
salutation() {
|
||||||
|
return this.nice ? 'Nice to meet you!' : 'Leave me alone!';
|
||||||
|
},
|
||||||
|
formattedDate() {
|
||||||
|
return new Date(this.today).toLocaleDateString('en-US', this.dateOptions);
|
||||||
|
},
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
'border-color': this.colour,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
template: `
|
template: `
|
||||||
<div style="border:2px dotted ${colour}; padding: 8px 22px; border-radius: 8px">
|
<div style="border: 2px dotted; padding: 8px 22px; border-radius: 8px" :style="style">
|
||||||
<h1>My name is ${name},</h1>
|
<h1>My name is {{ name }},</h1>
|
||||||
<h3>today is ${new Date(today).toLocaleDateString('en-US', dateOptions)}</h3>
|
<h3>today is {{ formattedDate }}</h3>
|
||||||
<p>${stockMessage}</p>
|
<p>{{ stockMessage }}</p>
|
||||||
<p>Also, I have:</p>
|
<p>Also, I have:</p>
|
||||||
<ul>
|
<ul>
|
||||||
${items.map(item => `<li key=${item}>${item}</li>`).join('')}
|
<li v-for="item in items" :key="item">{{ item }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>${salutation}</p>
|
<p>{{ salutation }}</p>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.add('XSS safety', () => ({
|
.add('XSS safety', () => ({
|
||||||
template: `
|
props: {
|
||||||
<div>
|
text: { default: text('Rendered string', '<img src=x onerror="alert(\'XSS Attack\')" >') },
|
||||||
${text('Rendered string', '<img src=x onerror="alert(\'XSS Attack\')" >')}
|
},
|
||||||
</div>
|
template: '<div v-html="text"></div>',
|
||||||
`,
|
|
||||||
}));
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user