Skip to content

Commit

Permalink
Merge pull request #28787 from JoCa96/next
Browse files Browse the repository at this point in the history
Vue: Extend sourceDecorator to support v-bind and nested keys in slots
  • Loading branch information
kasperpeulen authored Dec 27, 2024
2 parents 413c45e + 2c88aa2 commit 82777d7
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 22 deletions.
2 changes: 1 addition & 1 deletion code/addons/docs/docs/props-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Storybook Docs automatically generates props tables for components in supported

## Usage

For framework-specific setup instructions, see the framework's README: [React](../react/README.md), [Vue3 ](../vue3/README.md), [Angular](../angular/README.md), [Web Components](../web-components/README.md), [Ember](../ember/README.md).
For framework-specific setup instructions, see the framework's README: [React](../react/README.md), [Vue3](../vue3/README.md), [Angular](../angular/README.md), [Web Components](../web-components/README.md), [Ember](../ember/README.md).

### DocsPage

Expand Down
12 changes: 8 additions & 4 deletions code/renderers/vue3/src/docs/sourceDecorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,20 @@ test('should generate source code for slots with bindings', () => {
type TestBindings = {
foo: string;
bar?: number;
boo: {
mimeType: string;
};
};

const slots = {
a: ({ foo, bar }: TestBindings) => `Slot with bindings ${foo} and ${bar}`,
b: ({ foo }: TestBindings) => h('a', { href: foo, target: foo }, `Test link: ${foo}`),
a: ({ foo, bar, boo }: TestBindings) => `Slot with bindings ${foo}, ${bar} and ${boo.mimeType}`,
b: ({ foo, boo }: TestBindings) =>
h('a', { href: foo, target: foo, type: boo.mimeType, ...boo }, `Test link: ${foo}`),
};

const expectedCode = `<template #a="{ foo, bar }">Slot with bindings {{ foo }} and {{ bar }}</template>
const expectedCode = `<template #a="{ foo, bar, boo }">Slot with bindings {{ foo }}, {{ bar }} and {{ boo.mimeType }}</template>
<template #b="{ foo }"><a :href="foo" :target="foo">Test link: {{ foo }}</a></template>`;
<template #b="{ foo, boo }"><a :href="foo" :target="foo" :type="boo.mimeType" v-bind="boo">Test link: {{ foo }}</a></template>`;

const actualCode = generateSlotSourceCode(slots, Object.keys(slots), {
imports: {},
Expand Down
84 changes: 69 additions & 15 deletions code/renderers/vue3/src/docs/sourceDecorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ export type SourceCodeGeneratorContext = {
imports: Record<string, Set<string>>;
};

/**
* Used to get the tracking data from the proxy. A symbol is unique, so when using it as a key it
* can't be accidentally accessed.
*/
const TRACKING_SYMBOL = Symbol('DEEP_ACCESS_SYMBOL');

type TrackingProxy = {
[TRACKING_SYMBOL]: true;
toString: () => string;
};

const isProxy = (obj: unknown): obj is TrackingProxy =>
!!(obj && typeof obj === 'object' && TRACKING_SYMBOL in obj);

/** Decorator to generate Vue source code for stories. */
export const sourceDecorator: Decorator = (storyFn, ctx) => {
const story = storyFn();
Expand Down Expand Up @@ -226,6 +240,10 @@ export const generatePropsSourceCode = (
return;
} // do not render undefined/null values // do not render undefined/null values

if (isProxy(value)) {
value = value.toString();
}

switch (typeof value) {
case 'string':
if (value === '') {
Expand Down Expand Up @@ -269,7 +287,7 @@ export const generatePropsSourceCode = (
case 'object': {
properties.push({
name: propName,
value: formatObject(value),
value: formatObject(value ?? {}),
// to follow Vue best practices, complex values like object and arrays are
// usually placed inside the <script setup> block instead of inlining them in the <template>
templateFn: undefined,
Expand Down Expand Up @@ -429,24 +447,60 @@ const generateSlotChildrenSourceCode = (
(param) => !['{', '}'].includes(param)
);

const parameters = paramNames.reduce<Record<string, string>>((obj, param) => {
obj[param] = `{{ ${param} }}`;
return obj;
}, {});

const returnValue = child(parameters);
let slotSourceCode = generateSlotChildrenSourceCode([returnValue], ctx);

// if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
// it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
// We create proxy to track how and which properties of a parameter are accessed
const parameters: Record<string, string> = {};
// TODO: it should be possible to extend the proxy logic here and maybe get rid of the `generatePropsSourceCode` and `getFunctionParamNames`
const proxied: Record<string, TrackingProxy> = {};
paramNames.forEach((param) => {
slotSourceCode = slotSourceCode.replaceAll(
new RegExp(` (\\S+)="{{ ${param} }}"`, 'g'),
` :$1="${param}"`
parameters[param] = `{{ ${param} }}`;
proxied[param] = new Proxy(
{
// we use the symbol to identify the proxy
[TRACKING_SYMBOL]: true,
} as TrackingProxy,
{
// getter is called when any prop of the parameter is read
get: (t, key) => {
if (key === TRACKING_SYMBOL) {
// allow retrieval of the tracking data
return t[TRACKING_SYMBOL];
}
if ([Symbol.toPrimitive, Symbol.toStringTag, 'toString'].includes(key)) {
// when the parameter is used as a string we return the parameter name
// we use the double brace notation as we don't know if the parameter is used in text or in a binding
return () => `{{ ${param} }}`;
}
if (key === 'v-bind') {
// if this key is returned we just return the parameter name
return `${param}`;
}
// otherwise a specific key of the parameter was accessed
// we use the double brace notation as we don't know if the parameter is used in text or in a binding
return `{{ ${param}.${key.toString()} }}`;
},
// ownKeys is called, among other uses, when an object is destructured
// in this case we assume the parameter is supposed to be bound using "v-bind"
// Therefore we only return one special key "v-bind" and the getter will be called afterwards with it
ownKeys: () => {
return [`v-bind`];
},
/** Called when destructured */
getOwnPropertyDescriptor: () => ({
configurable: true,
enumerable: true,
value: param,
writable: true,
}),
}
);
});

return slotSourceCode;
const returnValue = child(proxied);
const slotSourceCode = generateSlotChildrenSourceCode([returnValue], ctx);

// if slot bindings are used for properties of other components, our {{ paramName }} is incorrect because
// it would generate e.g. my-prop="{{ paramName }}", therefore, we replace it here to e.g. :my-prop="paramName"
return slotSourceCode.replaceAll(/ (\S+)="{{ (\S+) }}"/g, ` :$1="$2"`);
}

case 'bigint':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3';

import { h } from 'vue';

import Component from './template-slots/component.vue';

const meta = {
Expand All @@ -14,6 +16,12 @@ export const Default: Story = {
args: {
default: ({ num }) => `Default slot: num=${num}`,
named: ({ str }) => `Named slot: str=${str}`,
vbind: ({ num, str }) => `Named v-bind slot: num=${num}, str=${str}`,
vbind: ({ num, str, obj }) => [
`Named v-bind slot: num=${num}, str=${str}, obj.title=${obj.title}`,
h('br'),
h('button', obj, 'button'),
h('br'),
h('button', { disabled: true, ...obj }, 'merged props'),
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
<br />
<slot name="named" str="str"></slot>
<br />
<slot name="vbind" v-bind="{ num: 123, str: 'str' }"></slot>
<slot
name="vbind"
v-bind="{ num: 123, str: 'str', obj: { title: 'see me', style: { color: 'blue' } } }"
></slot>
</template>

0 comments on commit 82777d7

Please sign in to comment.