Skip to content

Commit

Permalink
Merge pull request #13327 from storybookjs/pocka/fix/vue-dynamic-sour…
Browse files Browse the repository at this point in the history
…ce-class-missing

Addon-Docs: Handle class attributes in Dynamic Source Rendering for Vue.js
  • Loading branch information
shilman committed Nov 29, 2020
2 parents 1cccad8 + 2e724f7 commit b747c5c
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
60 changes: 60 additions & 0 deletions addons/docs/src/frameworks/vue/sourceDecorator.test.ts
Expand Up @@ -30,6 +30,66 @@ describe('vnodeToString', () => {
).toMatchInlineSnapshot(`<button >Button</button>`);
});

it('static class', () => {
expect(
vnodeToString(
getVNode({
template: `<button class="foo bar">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button class="foo bar">Button</button>`);
});

it('string dynamic class', () => {
expect(
vnodeToString(
getVNode({
template: `<button :class="'foo'">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button class="foo">Button</button>`);
});

it('non-string dynamic class', () => {
expect(
vnodeToString(
getVNode({
template: `<button :class="1">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button >Button</button>`);
});

it('array dynamic class', () => {
expect(
vnodeToString(
getVNode({
template: `<button :class="['foo', null, false, 0, {bar: true, baz: false}]">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button class="foo bar">Button</button>`);
});

it('object dynamic class', () => {
expect(
vnodeToString(
getVNode({
template: `<button :class="{foo: true, bar: false}">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button class="foo">Button</button>`);
});

it('merge dynamic and static classes', () => {
expect(
vnodeToString(
getVNode({
template: `<button class="foo" :class="{bar: null, baz: 1}">Button</button>`,
})
)
).toMatchInlineSnapshot(`<button class="foo baz">Button</button>`);
});

it('attributes', () => {
const MyComponent: ComponentOptions<any, any, any> = {
props: ['propA', 'propB', 'propC', 'propD', 'propE', 'propF', 'propG'],
Expand Down
38 changes: 38 additions & 0 deletions addons/docs/src/frameworks/vue/sourceDecorator.ts
Expand Up @@ -76,6 +76,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) => {
export function vnodeToString(vnode: Vue.VNode): string {
const attrString = [
...(vnode.data?.slot ? ([['slot', vnode.data.slot]] as [string, any][]) : []),
['class', stringifyClassAttribute(vnode)],
...(vnode.componentOptions?.propsData ? Object.entries(vnode.componentOptions.propsData) : []),
...(vnode.data?.attrs ? Object.entries(vnode.data.attrs) : []),
]
Expand Down Expand Up @@ -122,6 +123,43 @@ export function vnodeToString(vnode: Vue.VNode): string {
.join('')}</${tag}>`;
}

function stringifyClassAttribute(vnode: Vue.VNode): string | undefined {
if (!vnode.data || (!vnode.data.staticClass && !vnode.data.class)) {
return undefined;
}

return (
[...(vnode.data.staticClass?.split(' ') ?? []), ...normalizeClassBinding(vnode.data.class)]
.filter(Boolean)
.join(' ') || undefined
);
}

// https://vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes
function normalizeClassBinding(binding: unknown): readonly string[] {
if (!binding) {
return [];
}

if (typeof binding === 'string') {
return [binding];
}

if (binding instanceof Array) {
// To handle an object-in-array binding smartly, we use recursion
return binding.map(normalizeClassBinding).reduce((a, b) => [...a, ...b], []);
}

if (typeof binding === 'object') {
return Object.entries(binding)
.filter(([, active]) => !!active)
.map(([className]) => className);
}

// Unknown class binding
return [];
}

function stringifyAttr(attrName: string, value?: any): string | null {
if (typeof value === 'undefined' || typeof value === 'function') {
return null;
Expand Down

0 comments on commit b747c5c

Please sign in to comment.