From fe2a84b7dc8c73e90bdd601acf618b3a77040dbf Mon Sep 17 00:00:00 2001 From: Shota Fuji Date: Sun, 29 Nov 2020 03:15:30 +0000 Subject: [PATCH 1/2] fix(addon-docs): render Vue classes in dynamic source https://github.com/storybookjs/storybook/issues/13326 Both static class (`class="foo"`) and dynamic class (`:class="..."`) are ignored because Vue treats them as special attribute. They don't exist in normal vnode.data.attrs nor componentOptions.propsData. --- .../frameworks/vue/sourceDecorator.test.ts | 60 +++++++++++++++++++ .../src/frameworks/vue/sourceDecorator.ts | 24 ++++++++ 2 files changed, 84 insertions(+) diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.test.ts b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts index b02569d9aa62..c4aa381a3a4c 100644 --- a/addons/docs/src/frameworks/vue/sourceDecorator.test.ts +++ b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts @@ -30,6 +30,66 @@ describe('vnodeToString', () => { ).toMatchInlineSnapshot(``); }); + it('static class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('string dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('non-string dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('array dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('merge dynamic and static classes', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + + it('object dynamic class', () => { + expect( + vnodeToString( + getVNode({ + template: ``, + }) + ) + ).toMatchInlineSnapshot(``); + }); + it('attributes', () => { const MyComponent: ComponentOptions = { props: ['propA', 'propB', 'propC', 'propD', 'propE', 'propF', 'propG'], diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.ts b/addons/docs/src/frameworks/vue/sourceDecorator.ts index 04975f10420c..6178b60dc66f 100644 --- a/addons/docs/src/frameworks/vue/sourceDecorator.ts +++ b/addons/docs/src/frameworks/vue/sourceDecorator.ts @@ -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', normalizeClassAttribute(vnode)], ...(vnode.componentOptions?.propsData ? Object.entries(vnode.componentOptions.propsData) : []), ...(vnode.data?.attrs ? Object.entries(vnode.data.attrs) : []), ] @@ -122,6 +123,29 @@ export function vnodeToString(vnode: Vue.VNode): string { .join('')}`; } +function normalizeClassAttribute(vnode: Vue.VNode): string | undefined { + if (!vnode.data || (!vnode.data.staticClass && !vnode.data.class)) { + return undefined; + } + + let dynamicClass: readonly string[] = []; + + if (typeof vnode.data.class === 'string') { + dynamicClass = [vnode.data.class]; + } else if (vnode.data.class instanceof Array) { + dynamicClass = vnode.data.class; + } else if (typeof vnode.data.class === 'object') { + dynamicClass = Object.entries(vnode.data.class) + .filter(([, active]) => !!active) + .map(([className]) => className); + } + + return ( + [...(vnode.data.staticClass?.split(' ') ?? []), ...dynamicClass].filter(Boolean).join(' ') || + undefined + ); +} + function stringifyAttr(attrName: string, value?: any): string | null { if (typeof value === 'undefined' || typeof value === 'function') { return null; From 2e724f727bb8619009d91995164bc9b3df07a978 Mon Sep 17 00:00:00 2001 From: Shota Fuji Date: Sun, 29 Nov 2020 03:39:54 +0000 Subject: [PATCH 2/2] fix(addon-docs): Handle object-in-array class (Vue.js) https://vuejs.org/v2/guide/class-and-style.html#Array-Syntax Add support for below. ```vue
``` --- .../frameworks/vue/sourceDecorator.test.ts | 16 ++++---- .../src/frameworks/vue/sourceDecorator.ts | 40 +++++++++++++------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.test.ts b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts index c4aa381a3a4c..54695c1ea84f 100644 --- a/addons/docs/src/frameworks/vue/sourceDecorator.test.ts +++ b/addons/docs/src/frameworks/vue/sourceDecorator.test.ts @@ -64,30 +64,30 @@ describe('vnodeToString', () => { expect( vnodeToString( getVNode({ - template: ``, + template: ``, }) ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); - it('merge dynamic and static classes', () => { + it('object dynamic class', () => { expect( vnodeToString( getVNode({ - template: ``, + template: ``, }) ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); - it('object dynamic class', () => { + it('merge dynamic and static classes', () => { expect( vnodeToString( getVNode({ - template: ``, + template: ``, }) ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); it('attributes', () => { diff --git a/addons/docs/src/frameworks/vue/sourceDecorator.ts b/addons/docs/src/frameworks/vue/sourceDecorator.ts index 6178b60dc66f..40831a68f70c 100644 --- a/addons/docs/src/frameworks/vue/sourceDecorator.ts +++ b/addons/docs/src/frameworks/vue/sourceDecorator.ts @@ -76,7 +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', normalizeClassAttribute(vnode)], + ['class', stringifyClassAttribute(vnode)], ...(vnode.componentOptions?.propsData ? Object.entries(vnode.componentOptions.propsData) : []), ...(vnode.data?.attrs ? Object.entries(vnode.data.attrs) : []), ] @@ -123,27 +123,41 @@ export function vnodeToString(vnode: Vue.VNode): string { .join('')}`; } -function normalizeClassAttribute(vnode: Vue.VNode): string | undefined { +function stringifyClassAttribute(vnode: Vue.VNode): string | undefined { if (!vnode.data || (!vnode.data.staticClass && !vnode.data.class)) { return undefined; } - let dynamicClass: readonly string[] = []; + return ( + [...(vnode.data.staticClass?.split(' ') ?? []), ...normalizeClassBinding(vnode.data.class)] + .filter(Boolean) + .join(' ') || undefined + ); +} - if (typeof vnode.data.class === 'string') { - dynamicClass = [vnode.data.class]; - } else if (vnode.data.class instanceof Array) { - dynamicClass = vnode.data.class; - } else if (typeof vnode.data.class === 'object') { - dynamicClass = Object.entries(vnode.data.class) +// 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); } - return ( - [...(vnode.data.staticClass?.split(' ') ?? []), ...dynamicClass].filter(Boolean).join(' ') || - undefined - ); + // Unknown class binding + return []; } function stringifyAttr(attrName: string, value?: any): string | null {