From 1cb81e8b82a7bc6f9deab44c807dc4cfd8bda2a2 Mon Sep 17 00:00:00 2001 From: Philipp Sonntag Date: Wed, 14 Sep 2022 10:02:49 +0200 Subject: [PATCH 0001/1428] sb-button web component: Added prefix slot in example * Changed extraction order of attributes from manifest. --- .../custom-elements.json | 6 +++++ .../src/components/sb-button.stories.ts | 25 ++++++++++++------- .../src/components/sb-button.ts | 2 +- .../src/docs/custom-elements.ts | 17 ++++++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/code/examples/web-components-kitchen-sink/custom-elements.json b/code/examples/web-components-kitchen-sink/custom-elements.json index f776c812fe36..ca582f2942f3 100644 --- a/code/examples/web-components-kitchen-sink/custom-elements.json +++ b/code/examples/web-components-kitchen-sink/custom-elements.json @@ -38,6 +38,12 @@ "default": "#1ea7fd" } ], + "slots": [ + { + "name": "prefix", + "description": "Label prefix" + } + ], "members": [ { "kind": "field", diff --git a/code/examples/web-components-kitchen-sink/src/components/sb-button.stories.ts b/code/examples/web-components-kitchen-sink/src/components/sb-button.stories.ts index 8022ff06f6a8..f5aa26ddc618 100644 --- a/code/examples/web-components-kitchen-sink/src/components/sb-button.stories.ts +++ b/code/examples/web-components-kitchen-sink/src/components/sb-button.stories.ts @@ -5,45 +5,52 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import './sb-button'; import type { SbButton } from './sb-button'; +/** + * Slots aren't defined as JS properties or within the class implementation of the component. + * To allow the user to fill in values and test them anyway, we extended the interface + * of the button to prevent type issues within the stories. + */ +interface SbButtonSlots extends SbButton { + prefix: string; +} + export default { title: 'Example/Button', // Need to set the tag to make addon-docs works properly with CustomElementsManifest component: 'sb-button', - argTypes: { - onClick: { action: 'onClick' }, - }, parameters: { actions: { handles: ['click', 'sb-button:click'], }, }, } as Meta; -const Template: Story = ({ primary, backgroundColor, size, label }) => +const Template: Story = ({ primary, backgroundColor, size, label, prefix }) => html``; + >${prefix}`; -export const Primary: Story = Template.bind({}); +export const Primary: Story = Template.bind({}); Primary.args = { primary: true, label: 'Button', }; -export const Secondary: Story = Template.bind({}); +export const Secondary: Story = Template.bind({}); Secondary.args = { label: 'Button', }; -export const Large: Story = Template.bind({}); +export const Large: Story = Template.bind({}); Large.args = { size: 'large', label: 'Button', }; -export const Small: Story = Template.bind({}); +export const Small: Story = Template.bind({}); Small.args = { size: 'small', label: 'Button', diff --git a/code/examples/web-components-kitchen-sink/src/components/sb-button.ts b/code/examples/web-components-kitchen-sink/src/components/sb-button.ts index 0c4dd313d22e..6f012bdda060 100644 --- a/code/examples/web-components-kitchen-sink/src/components/sb-button.ts +++ b/code/examples/web-components-kitchen-sink/src/components/sb-button.ts @@ -94,7 +94,7 @@ export class SbButton extends LitElement { style=${style} @click="${this.onClick}" > - ${this.label} + ${this.label} `; } diff --git a/code/renderers/web-components/src/docs/custom-elements.ts b/code/renderers/web-components/src/docs/custom-elements.ts index f81f19665006..dcfe033a3540 100644 --- a/code/renderers/web-components/src/docs/custom-elements.ts +++ b/code/renderers/web-components/src/docs/custom-elements.ts @@ -47,8 +47,19 @@ interface Sections { } function mapItem(item: TagItem, category: string): ArgType { - const type = - category === 'properties' ? { name: item.type?.text || item.type } : { name: 'void' }; + let type; + switch (category) { + case 'attributes': + case 'properties': + type = { name: item.type?.text || item.type }; + break; + case 'slots': + type = { name: 'string' }; + break; + default: + type = { name: 'void' }; + break; + } return { name: item.name, @@ -145,9 +156,9 @@ export const extractArgTypesFromElements = (tagName: string, customElements: Cus const metaData = getMetaData(tagName, customElements); return ( metaData && { - ...mapData(metaData.attributes, 'attributes'), ...mapData(metaData.members, 'properties'), ...mapData(metaData.properties, 'properties'), + ...mapData(metaData.attributes, 'attributes'), ...mapData(metaData.events, 'events'), ...mapData(metaData.slots, 'slots'), ...mapData(metaData.cssProperties, 'css custom properties'), From 12b80b2ca81c9985786bb0860d2923ed6c7219d2 Mon Sep 17 00:00:00 2001 From: Philipp Sonntag Date: Wed, 14 Sep 2022 18:36:23 +0200 Subject: [PATCH 0002/1428] Updated tests for web components. --- .../lit-element-demo-card/properties.snapshot | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/renderers/web-components/src/docs/__testfixtures__/lit-element-demo-card/properties.snapshot b/code/renderers/web-components/src/docs/__testfixtures__/lit-element-demo-card/properties.snapshot index 127bc8704a4a..7f1b194d16b6 100644 --- a/code/renderers/web-components/src/docs/__testfixtures__/lit-element-demo-card/properties.snapshot +++ b/code/renderers/web-components/src/docs/__testfixtures__/lit-element-demo-card/properties.snapshot @@ -84,7 +84,7 @@ Object { }, }, "type": Object { - "name": "void", + "name": "boolean", }, }, "backSide": Object { @@ -126,7 +126,7 @@ Object { "name": "header", "required": false, "table": Object { - "category": "properties", + "category": "attributes", "defaultValue": Object { "summary": "\\"Your Message\\"", }, @@ -152,7 +152,7 @@ Object { "name": "rows", "required": false, "table": Object { - "category": "properties", + "category": "attributes", "defaultValue": Object { "summary": "[]", }, From 0609970d7854455df8ca16c9d95211d2c081319d Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Mon, 30 Jan 2023 20:29:35 +0000 Subject: [PATCH 0003/1428] Adds shard option to the docs --- docs/writing-tests/test-runner.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/writing-tests/test-runner.md b/docs/writing-tests/test-runner.md index b4ca1507b36d..2d010b3ee428 100644 --- a/docs/writing-tests/test-runner.md +++ b/docs/writing-tests/test-runner.md @@ -95,6 +95,7 @@ If you're already using any of those flags in your project, you should be able t | `-u`, `--updateSnapshot` | Use this flag to re-record every snapshot that fails during this test run
`test-storybook -u` | | `--eject` | Creates a local configuration file to override defaults of the test-runner
`test-storybook --eject` | | `--coverage` | Runs [coverage tests](./test-coverage.md) on your stories and components
`test-storybook --coverage` | +| `--shard [index/count]` | Requires CI. Splits the test suite execution into multiple machines
`test-storybook --shard=1/8` | From 445285dae68438b4c9bfbe7619a2fe279ba5c259 Mon Sep 17 00:00:00 2001 From: Neretin Artem Date: Sat, 11 Feb 2023 19:54:41 +0600 Subject: [PATCH 0004/1428] docs: Fix snippets for globalTypes --- .../common/storybook-preview-configure-globaltypes.js.mdx | 5 ++--- .../common/storybook-preview-locales-globaltype.js.mdx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/snippets/common/storybook-preview-configure-globaltypes.js.mdx b/docs/snippets/common/storybook-preview-configure-globaltypes.js.mdx index 65d78e77448b..4a935a39e75d 100644 --- a/docs/snippets/common/storybook-preview-configure-globaltypes.js.mdx +++ b/docs/snippets/common/storybook-preview-configure-globaltypes.js.mdx @@ -3,15 +3,14 @@ export const globalTypes = { theme: { - name: 'Theme', description: 'Global theme for components', defaultValue: 'light', toolbar: { + // The label to show for this toolbar item + title: 'Theme', icon: 'circlehollow', // Array of plain string values or MenuItem shape (see below) items: ['light', 'dark'], - // Property that specifies if the name of the item will be displayed - showName: true, // Change title based on selected value dynamicTitle: true, }, diff --git a/docs/snippets/common/storybook-preview-locales-globaltype.js.mdx b/docs/snippets/common/storybook-preview-locales-globaltype.js.mdx index 34c01ad84d1d..7f3e861c70a2 100644 --- a/docs/snippets/common/storybook-preview-locales-globaltype.js.mdx +++ b/docs/snippets/common/storybook-preview-locales-globaltype.js.mdx @@ -3,7 +3,6 @@ export const globalTypes = { locale: { - name: 'Locale', description: 'Internationalization locale', defaultValue: 'en', toolbar: { From 60cd3ae4bbe5b4010bedf38b09019b22ec654aba Mon Sep 17 00:00:00 2001 From: Mauricio Rivera Date: Sat, 18 Feb 2023 17:09:12 -0500 Subject: [PATCH 0005/1428] Added solid to frameworks list. --- docs/frameworks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frameworks.js b/docs/frameworks.js index f37ffd6cd8cc..f98084101d8c 100644 --- a/docs/frameworks.js +++ b/docs/frameworks.js @@ -1,6 +1,6 @@ module.exports = { coreFrameworks: ['react', 'vue', 'angular', 'web-components'], - communityFrameworks: ['ember', 'html', 'svelte', 'preact', 'qwik'], + communityFrameworks: ['ember', 'html', 'svelte', 'preact', 'qwik', 'solid'], featureGroups: [ { name: 'Essentials', From e998e3be0e0a3f63fffd497abbbab30776a9b65f Mon Sep 17 00:00:00 2001 From: Mauricio Rivera Date: Sat, 18 Feb 2023 17:09:48 -0500 Subject: [PATCH 0006/1428] Added helper script for synchronizing docs with frontpage repo. --- docs/sync.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/sync.js diff --git a/docs/sync.js b/docs/sync.js new file mode 100644 index 000000000000..e7bddb54eb94 --- /dev/null +++ b/docs/sync.js @@ -0,0 +1,74 @@ +/** + * Util script for synchronizing docs with frontpage repository. + * For running it: + * - cd /storybook/docs + * - node sync.js + */ +const fs = require('fs'); +const readline = require('readline'); +const path = require('path'); + +const askQuestion = (query) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise(resolve => rl.question(`${query}\n`, ans => { + rl.close(); + resolve(ans); + })) +} + +const run = async () => { + let frontpageDocsPath = '/src/content/docs' + + const frontpageAbsPath = await askQuestion('Provide the frontpage project absolute path:') + frontpageDocsPath = `${frontpageAbsPath}${frontpageDocsPath}`; + + if (!fs.existsSync(frontpageDocsPath)) { + console.error(`The directory ${frontpageDocsPath} doesn't exists`); + process.exit(1) + } + + console.log(`Synchronizing files from: \n${__dirname} \nto: \n${frontpageDocsPath}`) + + fs.watch(__dirname , {recursive: true}, (_, filename) => { + const srcFilePath = path.join(__dirname, filename); + const targetFilePath = path.join(frontpageDocsPath, filename); + const targetDir = targetFilePath.split('/').slice(0, -1).join('/'); + + if (filename === 'sync.js') return; + + //Syncs create file + if (!fs.existsSync(targetFilePath)) { + fs.mkdirSync(targetDir, {recursive: true}) + + try { + fs.closeSync(fs.openSync(targetFilePath, 'w')); + console.log(`Created ${filename}.`); + } catch (error) { + throw error; + } + } + + //Syncs remove file + if (!fs.existsSync(srcFilePath)) { + try { + fs.unlinkSync(targetFilePath); + console.log(`Removed ${filename}.`); + } catch (error) { + throw error; + } + return; + } + + //Syncs update file + fs.copyFile(srcFilePath, targetFilePath, (err) => { + console.log(`Updated ${filename}.`); + if (err) throw err; + }); + }) +} + +run(); \ No newline at end of file From ab4e73e288895ccd56625a3c3f18d424968e3123 Mon Sep 17 00:00:00 2001 From: Mauricio Rivera Date: Sat, 18 Feb 2023 17:23:05 -0500 Subject: [PATCH 0007/1428] Added basic button story docs. --- docs/get-started/install.md | 2 ++ .../installation-command-section/solid.mdx | 15 ++++++++++ .../installation-problems/solid.mdx | 11 +++++++ docs/get-started/whats-a-story.md | 4 +++ .../solid/button-story-with-args.js.mdx | 21 ++++++++++++++ .../solid/button-story-with-args.ts-4-9.mdx | 27 +++++++++++++++++ .../solid/button-story-with-args.ts.mdx | 26 +++++++++++++++++ docs/snippets/solid/button-story.js.mdx | 23 +++++++++++++++ docs/snippets/solid/button-story.ts-4-9.mdx | 29 +++++++++++++++++++ docs/snippets/solid/button-story.ts.mdx | 28 ++++++++++++++++++ 10 files changed, 186 insertions(+) create mode 100644 docs/get-started/installation-command-section/solid.mdx create mode 100644 docs/get-started/installation-problems/solid.mdx create mode 100644 docs/snippets/solid/button-story-with-args.js.mdx create mode 100644 docs/snippets/solid/button-story-with-args.ts-4-9.mdx create mode 100644 docs/snippets/solid/button-story-with-args.ts.mdx create mode 100644 docs/snippets/solid/button-story.js.mdx create mode 100644 docs/snippets/solid/button-story.ts-4-9.mdx create mode 100644 docs/snippets/solid/button-story.ts.mdx diff --git a/docs/get-started/install.md b/docs/get-started/install.md index 77db823a856b..6fcd24f8001f 100644 --- a/docs/get-started/install.md +++ b/docs/get-started/install.md @@ -15,6 +15,7 @@ title: 'Install Storybook' 'get-started/installation-command-section/vue.mdx', 'get-started/installation-command-section/web-components.mdx', 'get-started/installation-command-section/qwik.mdx', + 'get-started/installation-command-section/solid.mdx', ]} /> @@ -88,6 +89,7 @@ Below are some of the most common installation issues and instructions on how to 'get-started/installation-problems/vue.mdx', 'get-started/installation-problems/web-components.mdx', 'get-started/installation-problems/qwik.mdx', + 'get-started/installation-problems/solid.mdx', ]} /> diff --git a/docs/get-started/installation-command-section/solid.mdx b/docs/get-started/installation-command-section/solid.mdx new file mode 100644 index 000000000000..41e5c040cc9d --- /dev/null +++ b/docs/get-started/installation-command-section/solid.mdx @@ -0,0 +1,15 @@ +Use the Storybook CLI to install it in a single command. Run this inside your _existing project’s_ root directory: + +- With npm: + +```shell +npx storybook init +``` + +- With pnpm: + +```shell +pnpm dlx storybook init +``` + +If you run into issues with the installation, check the [Troubleshooting section](#troubleshooting) below for guidance on how to solve it. diff --git a/docs/get-started/installation-problems/solid.mdx b/docs/get-started/installation-problems/solid.mdx new file mode 100644 index 000000000000..9308bb9b154a --- /dev/null +++ b/docs/get-started/installation-problems/solid.mdx @@ -0,0 +1,11 @@ +- Add the `--type solid` flag to the installation command to set up Storybook manually: + + ```shell + npx storybook init --type solid + ``` + +- Storybook's CLI provides support for [Yarn](https://yarnpkg.com/), [npm](https://www.npmjs.com/), and [pnpm](https://pnpm.io/) package managers. If you have Yarn installed in your environment but prefer to use another as your default package manager add the `--package-manager` flag to your installation command. For example: + + ```shell + npx storybook init --package-manager=npm + ``` diff --git a/docs/get-started/whats-a-story.md b/docs/get-started/whats-a-story.md index 52e04afae91f..ed9e8689e905 100644 --- a/docs/get-started/whats-a-story.md +++ b/docs/get-started/whats-a-story.md @@ -26,6 +26,8 @@ Let’s start with the `Button` component. A story is a function that describes 'html/button-story.js.mdx', 'html/button-story.ts.mdx', 'preact/button-story.js.mdx', + 'solid/button-story.js.mdx', + 'solid/button-story.ts.mdx', ]} usesCsf3 csf2Path="get-started/whats-a-story#snippet-button-story" @@ -56,6 +58,8 @@ The above story definition can be further improved to take advantage of [Storybo 'html/button-story-with-args.js.mdx', 'html/button-story-with-args.ts.mdx', 'preact/button-story-with-args.js.mdx', + 'solid/button-story-with-args.js.mdx', + 'solid/button-story-with-args.ts.mdx', ]} usesCsf3 csf2Path="get-started/whats-a-story#snippet-button-story-with-args" diff --git a/docs/snippets/solid/button-story-with-args.js.mdx b/docs/snippets/solid/button-story-with-args.js.mdx new file mode 100644 index 000000000000..119f049a57cd --- /dev/null +++ b/docs/snippets/solid/button-story-with-args.js.mdx @@ -0,0 +1,21 @@ +```js +// Button.stories.js|jsx + +import { Button } from './Button'; + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + component: Button, +}; + +export const Primary = { + args: { + label: 'Button', + primary: true, + }, +}; +``` diff --git a/docs/snippets/solid/button-story-with-args.ts-4-9.mdx b/docs/snippets/solid/button-story-with-args.ts-4-9.mdx new file mode 100644 index 000000000000..498436f4f995 --- /dev/null +++ b/docs/snippets/solid/button-story-with-args.ts-4-9.mdx @@ -0,0 +1,27 @@ +```tsx +// Button.stories.ts|tsx + +import type { Meta, StoryObj } from '@storybook/solid'; + +import { Button, ButtonProps } from './Button'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + + component: Button, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; +``` diff --git a/docs/snippets/solid/button-story-with-args.ts.mdx b/docs/snippets/solid/button-story-with-args.ts.mdx new file mode 100644 index 000000000000..60b45603ae58 --- /dev/null +++ b/docs/snippets/solid/button-story-with-args.ts.mdx @@ -0,0 +1,26 @@ +```tsx +// Button.stories.ts|tsx + +import type { Meta, StoryObj } from '@storybook/solid'; + +import { Button, ButtonProps } from './Button'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + component: Button, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; +``` diff --git a/docs/snippets/solid/button-story.js.mdx b/docs/snippets/solid/button-story.js.mdx new file mode 100644 index 000000000000..6a3c4c9adedb --- /dev/null +++ b/docs/snippets/solid/button-story.js.mdx @@ -0,0 +1,23 @@ +```js +// Button.stories.js|jsx + +import { Button } from './Button'; + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + component: Button, +}; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const Primary = { + render: () => , +}; +``` diff --git a/docs/snippets/solid/button-story-with-addon-example.ts-4-9.mdx b/docs/snippets/solid/button-story-with-addon-example.ts-4-9.mdx new file mode 100644 index 000000000000..f067347d3780 --- /dev/null +++ b/docs/snippets/solid/button-story-with-addon-example.ts-4-9.mdx @@ -0,0 +1,36 @@ +```tsx +// Button.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { Button } from './Button'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + + component: Button, + + //πŸ‘‡ Creates specific parameters for the story + parameters: { + myAddon: { + data: 'this data is passed to the addon', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const Basic: Story = { + render: () => , +}; +``` diff --git a/docs/snippets/solid/button-story-with-addon-example.ts.mdx b/docs/snippets/solid/button-story-with-addon-example.ts.mdx new file mode 100644 index 000000000000..90d87c014d76 --- /dev/null +++ b/docs/snippets/solid/button-story-with-addon-example.ts.mdx @@ -0,0 +1,34 @@ +```tsx +// Button.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { Button } from './Button'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + component: Button, + //πŸ‘‡ Creates specific parameters for the story + parameters: { + myAddon: { + data: 'this data is passed to the addon', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const Basic: Story = { + render: () => , +}; +``` From 697ae4a99ab5efc54275fae314fd1682c03644e1 Mon Sep 17 00:00:00 2001 From: Mauricio Rivera Date: Wed, 22 Feb 2023 09:41:12 -0500 Subject: [PATCH 0020/1428] Added docs code snippets. --- docs/configure/environment-variables.md | 2 ++ docs/configure/frameworks.md | 2 +- docs/configure/images-and-assets.md | 6 ++++ docs/configure/telemetry.md | 2 +- .../component-story-static-asset-cdn.js.mdx | 22 ++++++++++++ ...omponent-story-static-asset-cdn.ts-4-9.mdx | 24 +++++++++++++ .../component-story-static-asset-cdn.ts.mdx | 24 +++++++++++++ ...nent-story-static-asset-with-import.js.mdx | 27 +++++++++++++++ ...-story-static-asset-with-import.ts-4-9.mdx | 34 +++++++++++++++++++ ...nent-story-static-asset-with-import.ts.mdx | 34 +++++++++++++++++++ ...t-story-static-asset-without-import.js.mdx | 16 +++++++++ ...ory-static-asset-without-import.ts-4-9.mdx | 23 +++++++++++++ ...t-story-static-asset-without-import.ts.mdx | 23 +++++++++++++ .../my-component-with-env-variables.js.mdx | 20 +++++++++++ ...my-component-with-env-variables.ts-4-9.mdx | 26 ++++++++++++++ .../my-component-with-env-variables.ts.mdx | 25 ++++++++++++++ 16 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 docs/snippets/solid/component-story-static-asset-cdn.js.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-cdn.ts-4-9.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-cdn.ts.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-with-import.js.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-with-import.ts-4-9.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-with-import.ts.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-without-import.js.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-without-import.ts-4-9.mdx create mode 100644 docs/snippets/solid/component-story-static-asset-without-import.ts.mdx create mode 100644 docs/snippets/solid/my-component-with-env-variables.js.mdx create mode 100644 docs/snippets/solid/my-component-with-env-variables.ts-4-9.mdx create mode 100644 docs/snippets/solid/my-component-with-env-variables.ts.mdx diff --git a/docs/configure/environment-variables.md b/docs/configure/environment-variables.md index fe3eb4ad0b0a..91bb89ea0dc5 100644 --- a/docs/configure/environment-variables.md +++ b/docs/configure/environment-variables.md @@ -59,6 +59,8 @@ Then you can access this environment variable anywhere, even within your stories 'web-components/my-component-with-env-variables.js.mdx', 'web-components/my-component-with-env-variables.ts.mdx', 'svelte/my-component-with-env-variables.js.mdx', + 'solid/my-component-with-env-variables.js.mdx', + 'solid/my-component-with-env-variables.ts.mdx', ]} usesCsf3 csf2Path="configure/environment-variables#snippet-my-component-with-env-variables" diff --git a/docs/configure/frameworks.md b/docs/configure/frameworks.md index 079f4b3b0de4..4928420ccd23 100644 --- a/docs/configure/frameworks.md +++ b/docs/configure/frameworks.md @@ -15,7 +15,7 @@ Storybook provides support for the leading industry builders and frameworks. How | Builder | Framework | | ------- | ------------------------------------------------------------------------ | | Webpack | React, Angular, Vue, Web Components, NextJS, HTML, Ember, Preact, Svelte | -| Vite | React, Vue, Web Components, HTML, Svelte, SvelteKit, Qwik | +| Vite | React, Vue, Web Components, HTML, Svelte, SvelteKit, Qwik, Solid | ## Configure diff --git a/docs/configure/images-and-assets.md b/docs/configure/images-and-assets.md index 63dd3dc26c97..e7e21826e2c3 100644 --- a/docs/configure/images-and-assets.md +++ b/docs/configure/images-and-assets.md @@ -24,6 +24,8 @@ Afterward, you can use any asset in your stories: 'svelte/component-story-static-asset-with-import.js.mdx', 'web-components/component-story-static-asset-with-import.js.mdx', 'web-components/component-story-static-asset-with-import.ts.mdx', + 'solid/component-story-static-asset-with-import.js.mdx', + 'solid/component-story-static-asset-with-import.ts.mdx', ]} usesCsf3 csf2Path="configure/images-and-assets#snippet-component-story-static-asset-with-import" @@ -62,6 +64,8 @@ Here `../public` is your static directory. Now use it in a component or story li 'svelte/component-story-static-asset-without-import.js.mdx', 'web-components/component-story-static-asset-without-import.js.mdx', 'web-components/component-story-static-asset-without-import.ts.mdx', + 'solid/component-story-static-asset-without-import.js.mdx', + 'solid/component-story-static-asset-without-import.ts.mdx', ]} usesCsf3 csf2Path="configure/images-and-assets#snippet-component-story-static-asset-without-import" @@ -115,6 +119,8 @@ Upload your files to an online CDN and reference them. In this example, we’re 'svelte/component-story-static-asset-cdn.js.mdx', 'web-components/component-story-static-asset-cdn.js.mdx', 'web-components/component-story-static-asset-cdn.ts.mdx', + 'solid/component-story-static-asset-cdn.js.mdx', + 'solid/component-story-static-asset-cdn.ts.mdx', ]} usesCsf3 csf2Path="configure/images-and-assets#snippet-component-story-static-asset-cdn" diff --git a/docs/configure/telemetry.md b/docs/configure/telemetry.md index 3ac997b7128d..d6c4265a04f8 100644 --- a/docs/configure/telemetry.md +++ b/docs/configure/telemetry.md @@ -8,7 +8,7 @@ Storybook collects completely anonymous data to help us improve user experience. Hundreds of thousands of developers use Storybook daily to build, test, and document components. Storybook is framework agnostic and integrates with the front-end ecosystem: -- **JavaScript frameworks** such as [React](https://reactjs.org/), [Vue](https://vuejs.org/), and [Svelte](https://svelte.dev/) +- **JavaScript frameworks** such as [React](https://reactjs.org/), [Vue](https://vuejs.org/), [Svelte](https://svelte.dev/) and [Solid](https://www.solidjs.com/) - **Libraries** such as [Styled-Components](https://styled-components.com/), [Tailwind](https://tailwindcss.com/), [Redux](https://redux.js.org/) - **Design tools** such as [Figma](https://figma.com/), [Sketch](https://www.sketch.com/), [Zeplin](https://zeplin.io/) and [InVision](https://www.invisionapp.com/) - **Workflow tools** such as [Notion](https://www.notion.so/product), [Confluence](https://www.atlassian.com/software/confluence), and [Jira](https://www.atlassian.com/software/jira) diff --git a/docs/snippets/solid/component-story-static-asset-cdn.js.mdx b/docs/snippets/solid/component-story-static-asset-cdn.js.mdx new file mode 100644 index 000000000000..2f8c7f3db332 --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-cdn.js.mdx @@ -0,0 +1,22 @@ +```js +// MyComponent.stories.js|jsx|ts|tsx + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const WithAnImage = { + render: () => ( + My CDN placeholder + ), +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-cdn.ts-4-9.mdx b/docs/snippets/solid/component-story-static-asset-cdn.ts-4-9.mdx new file mode 100644 index 000000000000..57bfb86222ea --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-cdn.ts-4-9.mdx @@ -0,0 +1,24 @@ +```tsx +// MyComponent.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithAnImage: Story = { + render: () => ( + My CDN placeholder + ), +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-cdn.ts.mdx b/docs/snippets/solid/component-story-static-asset-cdn.ts.mdx new file mode 100644 index 000000000000..336e99a0abcb --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-cdn.ts.mdx @@ -0,0 +1,24 @@ +```tsx +// MyComponent.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +export default meta; +type Story = StoryObj; + +export const WithAnImage: Story = { + render: () => ( + My CDN placeholder + ), +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-with-import.js.mdx b/docs/snippets/solid/component-story-static-asset-with-import.js.mdx new file mode 100644 index 000000000000..163bba9ed83b --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-with-import.js.mdx @@ -0,0 +1,27 @@ +```js +// MyComponent.stories.js|jsx|ts|tsx + +import imageFile from './static/image.png'; + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +const image = { + src: imageFile, + alt: 'my image', +}; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const WithAnImage = { + render: () => {image.alt}, +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-with-import.ts-4-9.mdx b/docs/snippets/solid/component-story-static-asset-with-import.ts-4-9.mdx new file mode 100644 index 000000000000..b98463450c01 --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-with-import.ts-4-9.mdx @@ -0,0 +1,34 @@ +```tsx +// MyComponent.stories.ts|tsx + +import imageFile from './static/image.png'; + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const image = { + src: imageFile, + alt: 'my image', +}; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const WithAnImage: Story = { + render: () => {image.alt}, +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-with-import.ts.mdx b/docs/snippets/solid/component-story-static-asset-with-import.ts.mdx new file mode 100644 index 000000000000..77064765117a --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-with-import.ts.mdx @@ -0,0 +1,34 @@ +```tsx +// MyComponent.stories.ts|tsx + +import imageFile from './static/image.png'; + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +export default meta; +type Story = StoryObj; + +const image = { + src: imageFile, + alt: 'my image', +}; + +/* + *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/7.0/solid/api/csf + * to learn how to use render functions. + */ +export const WithAnImage: Story = { + render: () => {image.alt}, +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-without-import.js.mdx b/docs/snippets/solid/component-story-static-asset-without-import.js.mdx new file mode 100644 index 000000000000..aada809e54ea --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-without-import.js.mdx @@ -0,0 +1,16 @@ +```js +// MyComponent.stories.js|jsx|ts|tsx + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +// Assume image.png is located in the "public" directory. +export const WithAnImage = { + render: () => my image, +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-without-import.ts-4-9.mdx b/docs/snippets/solid/component-story-static-asset-without-import.ts-4-9.mdx new file mode 100644 index 000000000000..81fe5daf9982 --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-without-import.ts-4-9.mdx @@ -0,0 +1,23 @@ +```tsx +// MyComponent.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Assume image.png is located in the "public" directory. +export const WithAnImage: Story = { + render: () => my image, +}; +``` diff --git a/docs/snippets/solid/component-story-static-asset-without-import.ts.mdx b/docs/snippets/solid/component-story-static-asset-without-import.ts.mdx new file mode 100644 index 000000000000..c51e1ea80737 --- /dev/null +++ b/docs/snippets/solid/component-story-static-asset-without-import.ts.mdx @@ -0,0 +1,23 @@ +```tsx +// MyComponent.stories.ts|tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'img', +}; + +export default meta; +type Story = StoryObj; + +// Assume image.png is located in the "public" directory. +export const WithAnImage: Story = { + render: () => my image, +}; +``` diff --git a/docs/snippets/solid/my-component-with-env-variables.js.mdx b/docs/snippets/solid/my-component-with-env-variables.js.mdx new file mode 100644 index 000000000000..00fdd19ee49c --- /dev/null +++ b/docs/snippets/solid/my-component-with-env-variables.js.mdx @@ -0,0 +1,20 @@ +```js +// MyComponent.stories.js|jsx + +import { MyComponent } from './MyComponent'; + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'MyComponent', + component: MyComponent, +}; + +export const ExampleStory = { + args: { + propertyA: process.env.STORYBOOK_DATA_KEY, + }, +}; +``` diff --git a/docs/snippets/solid/my-component-with-env-variables.ts-4-9.mdx b/docs/snippets/solid/my-component-with-env-variables.ts-4-9.mdx new file mode 100644 index 000000000000..0e780ce37568 --- /dev/null +++ b/docs/snippets/solid/my-component-with-env-variables.ts-4-9.mdx @@ -0,0 +1,26 @@ +```tsx +// MyComponent.stories.ts| tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'MyComponent', + + component: MyComponent, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ExampleStory: Story = { + args: { + propertyA: process.env.STORYBOOK_DATA_KEY, + }, +}; +``` diff --git a/docs/snippets/solid/my-component-with-env-variables.ts.mdx b/docs/snippets/solid/my-component-with-env-variables.ts.mdx new file mode 100644 index 000000000000..aac9f319c808 --- /dev/null +++ b/docs/snippets/solid/my-component-with-env-variables.ts.mdx @@ -0,0 +1,25 @@ +```tsx +// MyComponent.stories.ts| tsx + +import type { Meta, StoryObj } from 'storybook-solidjs'; + +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'MyComponent', + component: MyComponent, +}; + +export default meta; +type Story = StoryObj; + +export const ExampleStory: Story = { + args: { + propertyA: process.env.STORYBOOK_DATA_KEY, + }, +}; +``` From d7ccdb4ad7c4387ede8ffa4e8a9ef5733d845764 Mon Sep 17 00:00:00 2001 From: Mauricio Rivera Date: Wed, 22 Feb 2023 10:59:39 -0500 Subject: [PATCH 0021/1428] Added api and contribute snippets. --- docs/api/csf.md | 21 +++++++++ docs/contribute/new-snippets.md | 10 +++-- .../button-story-click-handler-args.js.mdx | 26 +++++++++++ ...button-story-click-handler-args.ts-4-9.mdx | 30 +++++++++++++ .../button-story-click-handler-args.ts.mdx | 29 +++++++++++++ ...n-story-click-handler-simplificated.js.mdx | 18 ++++++++ ...ory-click-handler-simplificated.ts-4-9.mdx | 24 +++++++++++ ...n-story-click-handler-simplificated.ts.mdx | 23 ++++++++++ .../solid/button-story-click-handler.js.mdx | 20 +++++++++ .../button-story-click-handler.ts-4-9.mdx | 26 +++++++++++ .../solid/button-story-click-handler.ts.mdx | 25 +++++++++++ .../solid/button-story-with-sample.js.mdx | 23 ++++++++++ ...t-story-with-custom-render-function.js.mdx | 32 ++++++++++++++ ...ory-with-custom-render-function.ts-4-9.mdx | 36 ++++++++++++++++ ...t-story-with-custom-render-function.ts.mdx | 35 +++++++++++++++ .../solid/csf-2-example-starter.js.mdx | 13 ++++++ .../solid/csf-2-example-starter.ts.mdx | 15 +++++++ .../snippets/solid/csf-2-example-story.js.mdx | 6 +++ .../snippets/solid/csf-2-example-story.ts.mdx | 6 +++ .../solid/csf-3-example-render.js.mdx | 8 ++++ .../solid/csf-3-example-render.ts.mdx | 8 ++++ .../solid/csf-3-example-starter.ts-4-9.mdx | 16 +++++++ .../solid/csf-3-example-starter.ts.mdx | 14 ++++++ .../my-component-story-basic-and-props.js.mdx | 20 +++++++++ ...component-story-basic-and-props.ts-4-9.mdx | 26 +++++++++++ .../my-component-story-basic-and-props.ts.mdx | 25 +++++++++++ .../my-component-story-with-nonstory.js.mdx | 33 ++++++++++++++ ...y-component-story-with-nonstory.ts-4-9.mdx | 43 +++++++++++++++++++ .../my-component-story-with-nonstory.ts.mdx | 38 ++++++++++++++++ 29 files changed, 646 insertions(+), 3 deletions(-) create mode 100644 docs/snippets/solid/button-story-click-handler-args.js.mdx create mode 100644 docs/snippets/solid/button-story-click-handler-args.ts-4-9.mdx create mode 100644 docs/snippets/solid/button-story-click-handler-args.ts.mdx create mode 100644 docs/snippets/solid/button-story-click-handler-simplificated.js.mdx create mode 100644 docs/snippets/solid/button-story-click-handler-simplificated.ts-4-9.mdx create mode 100644 docs/snippets/solid/button-story-click-handler-simplificated.ts.mdx create mode 100644 docs/snippets/solid/button-story-click-handler.js.mdx create mode 100644 docs/snippets/solid/button-story-click-handler.ts-4-9.mdx create mode 100644 docs/snippets/solid/button-story-click-handler.ts.mdx create mode 100644 docs/snippets/solid/button-story-with-sample.js.mdx create mode 100644 docs/snippets/solid/component-story-with-custom-render-function.js.mdx create mode 100644 docs/snippets/solid/component-story-with-custom-render-function.ts-4-9.mdx create mode 100644 docs/snippets/solid/component-story-with-custom-render-function.ts.mdx create mode 100644 docs/snippets/solid/csf-2-example-starter.js.mdx create mode 100644 docs/snippets/solid/csf-2-example-starter.ts.mdx create mode 100644 docs/snippets/solid/csf-2-example-story.js.mdx create mode 100644 docs/snippets/solid/csf-2-example-story.ts.mdx create mode 100644 docs/snippets/solid/csf-3-example-render.js.mdx create mode 100644 docs/snippets/solid/csf-3-example-render.ts.mdx create mode 100644 docs/snippets/solid/csf-3-example-starter.ts-4-9.mdx create mode 100644 docs/snippets/solid/csf-3-example-starter.ts.mdx create mode 100644 docs/snippets/solid/my-component-story-basic-and-props.js.mdx create mode 100644 docs/snippets/solid/my-component-story-basic-and-props.ts-4-9.mdx create mode 100644 docs/snippets/solid/my-component-story-basic-and-props.ts.mdx create mode 100644 docs/snippets/solid/my-component-story-with-nonstory.js.mdx create mode 100644 docs/snippets/solid/my-component-story-with-nonstory.ts-4-9.mdx create mode 100644 docs/snippets/solid/my-component-story-with-nonstory.ts.mdx diff --git a/docs/api/csf.md b/docs/api/csf.md index c935f45e389a..02604e69a175 100644 --- a/docs/api/csf.md +++ b/docs/api/csf.md @@ -51,6 +51,8 @@ With CSF, every named export in the file represents a story object by default. 'angular/my-component-story-basic-and-props.ts.mdx', 'web-components/my-component-story-basic-and-props.js.mdx', 'web-components/my-component-story-basic-and-props.ts.mdx', + 'solid/my-component-story-basic-and-props.js.mdx', + 'solid/my-component-story-basic-and-props.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-my-component-story-basic-and-props" @@ -107,6 +109,8 @@ Consider Storybook’s ["Button" example](../writing-stories/introduction.md#def 'angular/button-story-click-handler.ts.mdx', 'web-components/button-story-click-handler.js.mdx', 'web-components/button-story-click-handler.ts.mdx', + 'solid/button-story-click-handler.js.mdx', + 'solid/button-story-click-handler.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-button-story-click-handler" @@ -130,6 +134,8 @@ Now consider the same example, re-written with args: 'svelte/button-story-click-handler-args.js.mdx', 'web-components/button-story-click-handler-args.js.mdx', 'web-components/button-story-click-handler-args.ts.mdx', + 'solid/button-story-click-handler-args.js.mdx', + 'solid/button-story-click-handler-args.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-button-story-click-handler-args" @@ -150,6 +156,8 @@ Or even more simply: 'vue/button-story-click-handler-simplificated.ts.mdx', 'web-components/button-story-click-handler-simplificated.js.mdx', 'web-components/button-story-click-handler-simplificated.ts.mdx', + 'solid/button-story-click-handler-simplificated.js.mdx', + 'solid/button-story-click-handler-simplificated.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-button-story-click-handler-simplificated" @@ -181,6 +189,8 @@ A good use case for the `play` function is a form component. With previous Story 'web-components/login-form-with-play-function.js.mdx', 'web-components/login-form-with-play-function.ts.mdx', 'svelte/login-form-with-play-function.js.mdx', + 'solid/login-form-with-play-function.js.mdx', + 'solid/login-form-with-play-function.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-login-form-with-play-function" @@ -206,6 +216,8 @@ Starting in Storybook 6.4, you can write your stories as JavaScript objects, red 'preact/component-story-with-custom-render-function.js.mdx', 'web-components/component-story-with-custom-render-function.js.mdx', 'web-components/component-story-with-custom-render-function.ts.mdx', + 'solid/component-story-with-custom-render-function.js.mdx', + 'solid/component-story-with-custom-render-function.ts.mdx', ]} usesCsf3 /> @@ -261,6 +273,8 @@ Consider the following story file: 'angular/my-component-story-with-nonstory.ts.mdx', 'web-components/my-component-story-with-nonstory.js.mdx', 'web-components/my-component-story-with-nonstory.ts.mdx', + 'solid/my-component-story-with-nonstory.js.mdx', + 'solid/my-component-story-with-nonstory.ts.mdx', ]} usesCsf3 csf2Path="api/csf#snippet-my-component-story-with-nonstory" @@ -298,6 +312,8 @@ In CSF 2, the named exports are always functions that instantiate a component, a 'angular/csf-2-example-starter.ts.mdx', 'web-components/csf-2-example-starter.js.mdx', 'web-components/csf-2-example-starter.ts.mdx', + 'solid/csf-2-example-starter.js.mdx', + 'solid/csf-2-example-starter.ts.mdx', ]} /> @@ -318,6 +334,7 @@ Here's the CSF 3 equivalent: 'angular/csf-3-example-starter.ts.mdx', 'web-components/csf-3-example-starter.js.mdx', 'web-components/csf-3-example-starter.ts.mdx', + 'solid/csf-3-example-starter.ts.mdx', ]} /> @@ -378,6 +395,8 @@ Let's start with a simple CSF 2 story function: 'angular/csf-2-example-story.ts.mdx', 'web-components/csf-2-example-story.js.mdx', 'web-components/csf-2-example-story.ts.mdx', + 'solid/csf-2-example-story.js.mdx', + 'solid/csf-2-example-story.ts.mdx', ]} /> @@ -398,6 +417,8 @@ Now, let's rewrite it as a story object in CSF 3 with an explicit `render` funct 'angular/csf-3-example-render.ts.mdx', 'web-components/csf-3-example-render.js.mdx', 'web-components/csf-3-example-render.ts.mdx', + 'solid/csf-3-example-render.js.mdx', + 'solid/csf-3-example-render.ts.mdx', ]} /> diff --git a/docs/contribute/new-snippets.md b/docs/contribute/new-snippets.md index 9b70c7e4250d..8a36a0d67702 100644 --- a/docs/contribute/new-snippets.md +++ b/docs/contribute/new-snippets.md @@ -10,9 +10,9 @@ Storybook maintains code snippets for a [variety of frameworks](./../api/framewo We welcome community contributions to the code snippets. Here's a matrix of the frameworks we have snippets for. Help us add snippets for your favorite framework. -| React | Vue | Angular | Web Components | Svelte | Ember | HTML | Preact | -| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----- | ---- | ------ | -| [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/react) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/vue) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/angular) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/web-components) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/svelte) | ❌ | ❌ | ❌ | +| React | Vue | Angular | Web Components | Svelte | Solid | Ember | HTML | Preact | +| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----- | ---- | ------ | +| [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/react) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/vue) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/angular) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/web-components) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/svelte) | [βœ…](https://github.com/storybookjs/storybook/tree/next/docs/snippets/solid) | ❌ | ❌ | ❌ | ## Setup @@ -44,6 +44,8 @@ Browse the documentation and look for the code snippets you're willing to contri 'vue/your-component.3.js.mdx', 'svelte/your-component.js.mdx', 'web-components/your-component.js.mdx', + 'solid/your-component.js.mdx', + 'solid/your-component.ts.mdx', ]} /> @@ -66,6 +68,8 @@ Create the file `ember/your-component.js.mdx`, similar to the other frameworks, 'vue/your-component.3.js.mdx', 'svelte/your-component.js.mdx', 'web-components/your-component.js.mdx', + 'solid/your-component.js.mdx', + 'solid/your-component.ts.mdx', 'ember/your-component.js.mdx', //πŸ‘ˆπŸΌ The code snippet you created. ]} /> diff --git a/docs/snippets/solid/button-story-click-handler-args.js.mdx b/docs/snippets/solid/button-story-click-handler-args.js.mdx new file mode 100644 index 000000000000..68a3262dce72 --- /dev/null +++ b/docs/snippets/solid/button-story-click-handler-args.js.mdx @@ -0,0 +1,26 @@ +```js +// Button.stories.js|jsx + +import solid from 'solid'; + +import { action } from '@storybook/addon-actions'; + +import { Button } from './Button'; + +export default { + /* πŸ‘‡ The title prop is optional. + * See https://storybook.js.org/docs/7.0/solid/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'Button', + component: Button, +}; + +export const Text = { + args: { + label: 'Hello', + onClick: action('clicked'), + }, + render: ({ label, onClick }) => + + {{args.label}}`, + }); + const storyFnString = storyFn.toString(); + console.log(' storyFn ', storyFnString); + const { template } = storyFn(); + const ast = baseParse(template); + const { children, type } = ast; + console.log({ children, type }); + expect({ booleanProp: true }).toEqual({ booleanProp: true }); + }); +}); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 6c2c68559812..19e095bc5367 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -2,7 +2,7 @@ import { createApp, h, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; -import type { StoryFnVueReturnType, VueRenderer } from './types'; +import type { VueRenderer } from './types'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -22,24 +22,23 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any } + { vueApp: ReturnType; reactiveArgs: any; rootComponent: any } >(); -const elementMap = new Map(); - export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { // fetch the story with the updated context (with reactive args) + const existingApp = map.get(canvasElement); + storyContext.args = reactive(storyContext.args); - const element: StoryFnVueReturnType = storyFn(); - elementMap.set(canvasElement, element); + const rootComponent: any = storyFn(); // !existingApp ? storyFn() : existingApp.rootComponent(); - const props = (element as any).render?.().props; - const reactiveArgs = props ? reactive(props) : storyContext.args; + const appProps = + rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); + const reactiveArgs = Object.keys(appProps).length > 0 ? reactive(appProps) : storyContext.args; - const existingApp = map.get(canvasElement); if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, reactiveArgs); return () => { @@ -51,10 +50,8 @@ export function renderToCanvas( const storybookApp = createApp({ render() { - const renderedElement: any = elementMap.get(canvasElement); - const current = renderedElement && renderedElement.template ? renderedElement : element; - map.set(canvasElement, { vueApp: storybookApp, reactiveArgs }); - return h(current, reactiveArgs); + map.set(canvasElement, { vueApp: storybookApp, reactiveArgs, rootComponent }); + return h(rootComponent, reactiveArgs); }, }); @@ -89,11 +86,10 @@ function getSlots(props: Args, context: StoryContext) { * @param nextArgs * @returns */ -function updateArgs(reactiveArgs: Args, nextArgs: Args) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (!nextArgs) return; - Object.keys(reactiveArgs).forEach((key) => { - delete reactiveArgs[key]; - }); + + Object.keys(reactiveArgs).forEach((key) => delete reactiveArgs[key]); Object.assign(reactiveArgs, nextArgs); } From c416f708809781534b67a29494ebee6b2faaf2f8 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 23 Feb 2023 21:37:00 +0400 Subject: [PATCH 0027/1428] some experimental --- .../vue3/src/docs/sourceDecorator.ts | 95 ++++++------------- 1 file changed, 28 insertions(+), 67 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index b8e8f9cb4be1..1a59b1e4c783 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -13,11 +13,7 @@ import type { TextNode, InterpolationNode, } from '@vue/compiler-core'; -import { - baseParse, - // ExpressionNode, - NodeTypes, -} from '@vue/compiler-core'; +import { baseParse } from '@vue/compiler-core'; import { h } from 'vue'; type ArgEntries = [string, any][]; @@ -46,22 +42,6 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; -/** - * Extract a component name. - * - * @param component Component - */ -function getComponentNameAndChildren(component: any): { - name: string | null; - children: any; - attributes: any; -} { - return { - name: component?.name || component?.__name || component?.__docgenInfo?.__name || null, - children: component?.children || null, - attributes: component?.attributes || component?.attrs || null, - }; -} /** * * @param _args @@ -69,39 +49,23 @@ function getComponentNameAndChildren(component: any): { * @param byRef */ function generateAttributesSource( - args: (AttributeNode | DirectiveNode)[], + tempArgs: (AttributeNode | DirectiveNode)[], + args: Args, argTypes: ArgTypes, byRef?: boolean ): string { - return Object.keys(args) - .map((key: any) => args[key].loc.source) + return Object.keys(tempArgs) + .map((key: any) => { + const arg = tempArgs[key]; + console.log(`---- arg ${key} `, arg); + const storyArg = args[arg.arg.content]; + console.log(`---- arg.name ${arg.arg.content} `, args); + console.log(`---- storyArg ${arg.arg} `, storyArg); + return tempArgs[key].loc.source; + }) .join(' '); } -function generateAttributeSource( - key: string, - value: Args[keyof Args], - argType: ArgTypes[keyof ArgTypes] -): string { - if (!value) { - return ''; - } - - if (value === true) { - return key; - } - - if (key.startsWith('v-on:')) { - return `${key}='() => {}'`; - } - - if (typeof value === 'string') { - return `${key}='${value}'`; - } - - return `:${key}='${JSON.stringify(value)}'`; -} - /** * * @param args generate script setup from args @@ -131,7 +95,6 @@ function getTemplates(renderFn: any): TemplateChildNode[] { return components; } catch (e) { // console.error(e); - console.log(' no template '); return []; } } @@ -151,8 +114,6 @@ export function generateSource( byRef?: boolean | undefined ): string | null { const generateComponentSource = (component: TemplateChildNode) => { - // const { name, children, attributes } = getComponentNameAndChildren(component); - let attributes; let name; let children; @@ -172,9 +133,8 @@ export function generateSource( const child = component as TextNode; content = child.content; } - - if (typeof (component as any).render === 'function') { - // children = child.children; + const concreteComponent = component as any; + if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; @@ -182,12 +142,12 @@ export function generateSource( attributes = attributesNode; } - name = vnode.type.__docgenInfo.displayName; + name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; - const argsIn = attributes ?? []; // keep only args that are in attributes - const props = generateAttributesSource(argsIn, argTypes, byRef); + const templateAttrs = attributes ?? []; // keep only args that are in attributes + const props = generateAttributesSource(templateAttrs, args, argTypes, byRef); if (name) source += `<${name} ${props} >`; if (children) { @@ -219,16 +179,17 @@ function mapAttributesAndDirectives(props: Args) { ? `:${key}='${value}'` : eventDirective(key, value); - return Object.keys(props) - .map((key) => ({ - name: key, - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, - arg: { content: key, loc: { source: key } }, - loc: { source: source(key, props[key]) }, - exp: { isStatic: false, loc: { source: props[key] } }, - modifiers: [''], - })) - .concat(); + return Object.keys(props).map( + (key) => + ({ + name: 'bind', + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, + arg: { content: key, loc: { source: key } }, + loc: { source: source(key, props[key]) }, + exp: { isStatic: false, loc: { source: props[key] } }, + modifiers: [''], + } as unknown as AttributeNode) + ); } /** From 3dc96b1aca1b2ad495893bca960b1cd44b43d97f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 13:43:09 +0400 Subject: [PATCH 0028/1428] start writing propre tests for source decorator --- .../vue3/src/docs/sourceDecorator.test.ts | 149 +++++++++--------- 1 file changed, 74 insertions(+), 75 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index d3189e9eb767..dd04efa27886 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from '@jest/globals'; import type { Args } from '@storybook/types'; -import { generateSource } from './sourceDecorator'; + +import { generateSource, getComponentsFromTemplate } from './sourceDecorator'; expect.addSnapshotSerializer({ print: (val: any) => val, @@ -12,84 +13,82 @@ function generateArgTypes(args: Args, slotProps: string[] | undefined) { return acc; }, {} as Record); } -function generateForArgs(args: Args, slotProps: string[] | undefined = undefined) { - return generateSource({ name: 'Component' }, args, generateArgTypes(args, slotProps), true); -} -function generateMultiComponentForArgs(args: Args, slotProps: string[] | undefined = undefined) { - return generateSource( - [{ name: 'Component' }, { name: 'Component' }], - args, - generateArgTypes(args, slotProps), - true - ); + +function generateForArgs( + args: Args, + slotProps: string[] | undefined = undefined, + template = '' +) { + const components = getComponentsFromTemplate(template); + return generateSource(components, args, generateArgTypes(args, slotProps), true); } -describe('generateSource Vue3', () => { +describe('generateSource snippet Vue3', () => { test('boolean true', () => { - expect(generateForArgs({ booleanProp: true })).toMatchInlineSnapshot( - `` - ); + expect( + generateForArgs({ booleanProp: true }, [], ``) + ).toMatchInlineSnapshot(``); }); test('boolean false', () => { - expect(generateForArgs({ booleanProp: false })).toMatchInlineSnapshot( - `` - ); - }); - test('null property', () => { - expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( - `` - ); - }); - test('string property', () => { - expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( - `` - ); - }); - test('number property', () => { - expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( - `` - ); - }); - test('object property', () => { - expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot( - `` - ); - }); - test('multiple properties', () => { - expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); - }); - test('1 slot property', () => { - expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content'])).toMatchInlineSnapshot(` - - {{ content }} - - `); - }); - test('multiple slot property with second slot value not set', () => { - expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) - .toMatchInlineSnapshot(` - - {{ content }} - - `); - }); - test('multiple slot property with second slot value is set', () => { - expect(generateForArgs({ content: 'xyz', footer: 'foo', myProp: 'abc' }, ['content', 'footer'])) - .toMatchInlineSnapshot(` - - - - - `); - }); - // test mutil components - test('multi component with boolean true', () => { - expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` - - - `); - }); - test('component is not set', () => { - expect(generateSource(null, {}, {})).toBeNull(); + expect( + generateForArgs({ booleanProp: false }, [], ``) + ).toMatchInlineSnapshot(``); }); + // test('null property', () => { + // expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('string property', () => { + // expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('number property', () => { + // expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('object property', () => { + // expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot( + // `` + // ); + // }); + // test('multiple properties', () => { + // expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); + // }); + // test('1 slot property', () => { + // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content'])).toMatchInlineSnapshot(` + // + // {{ content }} + // + // `); + // }); + // test('multiple slot property with second slot value not set', () => { + // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) + // .toMatchInlineSnapshot(` + // + // {{ content }} + // + // `); + // }); + // test('multiple slot property with second slot value is set', () => { + // expect(generateForArgs({ content: 'xyz', footer: 'foo', myProp: 'abc' }, ['content', 'footer'])) + // .toMatchInlineSnapshot(` + // + // + // + // + // `); + // }); + // // test mutil components + // test('multi component with boolean true', () => { + // expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` + // + // + // `); + // }); + // test('component is not set', () => { + // expect(generateSource(null, {}, {})).toBeNull(); + // }); }); From 207ab63e8d9cf88f6161b633d8455936abd1eb1d Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 16:00:32 +0400 Subject: [PATCH 0029/1428] write tests for 2 main src decorator functions --- .../vue3/src/docs/sourceDecorator.test.ts | 330 ++++++++++++++---- 1 file changed, 265 insertions(+), 65 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index dd04efa27886..18c1296f4d19 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from '@jest/globals'; import type { Args } from '@storybook/types'; -import { generateSource, getComponentsFromTemplate } from './sourceDecorator'; +import type { ArgsType } from 'jest-mock'; +import { + generateSource, + getComponentsFromTemplate, + mapAttributesAndDirectives, + generateAttributesSource, +} from './sourceDecorator'; expect.addSnapshotSerializer({ print: (val: any) => val, @@ -23,72 +29,266 @@ function generateForArgs( return generateSource(components, args, generateArgTypes(args, slotProps), true); } -describe('generateSource snippet Vue3', () => { - test('boolean true', () => { +describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { + test('camelCase boolean Arg', () => { + expect(mapAttributesAndDirectives({ camelCaseBooleanArg: true })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-boolean-arg, + loc: Object { + source: camel-case-boolean-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :camel-case-boolean-arg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('camelCase string Arg', () => { + expect(mapAttributesAndDirectives({ camelCaseStringArg: 'foo' })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-string-arg, + loc: Object { + source: camel-case-string-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: foo, + }, + }, + loc: Object { + source: camel-case-string-arg='foo', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('boolean arg', () => { + expect(mapAttributesAndDirectives({ booleanarg: true })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: booleanarg, + loc: Object { + source: booleanarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :booleanarg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('string arg', () => { + expect(mapAttributesAndDirectives({ stringarg: 'bar' })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: stringarg, + loc: Object { + source: stringarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: bar, + }, + }, + loc: Object { + source: stringarg='bar', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('number arg', () => { + expect(mapAttributesAndDirectives({ numberarg: 2023 })).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: numberarg, + loc: Object { + source: numberarg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: 2023, + }, + }, + loc: Object { + source: :numberarg='2023', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); + }); + test('camelCase boolean, string, and number Args', () => { expect( - generateForArgs({ booleanProp: true }, [], ``) - ).toMatchInlineSnapshot(``); + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + arg: Object { + content: camel-case-boolean-arg, + loc: Object { + source: camel-case-boolean-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: true, + }, + }, + loc: Object { + source: :camel-case-boolean-arg='true', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + Object { + arg: Object { + content: camel-case-string-arg, + loc: Object { + source: camel-case-string-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: foo, + }, + }, + loc: Object { + source: camel-case-string-arg='foo', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + Object { + arg: Object { + content: came-case-number-arg, + loc: Object { + source: came-case-number-arg, + }, + }, + exp: Object { + isStatic: false, + loc: Object { + source: 2023, + }, + }, + loc: Object { + source: :came-case-number-arg='2023', + }, + modifiers: Array [ + , + ], + name: bind, + type: 6, + }, + ] + `); }); - test('boolean false', () => { +}); + +describe('Vue3: sourceDecorator->generateAttributesSource()', () => { + test('camelCase boolean Arg', () => { expect( - generateForArgs({ booleanProp: false }, [], ``) - ).toMatchInlineSnapshot(``); + generateAttributesSource( + mapAttributesAndDirectives({ camelCaseBooleanArg: true }), + { camelCaseBooleanArg: true }, + [{ camelCaseBooleanArg: { type: 'boolean' } }] as ArgsType + ) + ).toMatchInlineSnapshot(`:camel-case-boolean-arg='true'`); + }); + test('camelCase string Arg', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ camelCaseStringArg: 'foo' }), + { camelCaseStringArg: 'foo' }, + [{ camelCaseStringArg: { type: 'string' } }] as ArgsType + ) + ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); + }); + + test('camelCase boolean, string, and number Args', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }), + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }, + [] as ArgsType + ) + ).toMatchInlineSnapshot( + `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` + ); }); - // test('null property', () => { - // expect(generateForArgs({ nullProp: null })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('string property', () => { - // expect(generateForArgs({ stringProp: 'mystr' })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('number property', () => { - // expect(generateForArgs({ numberProp: 42 })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('object property', () => { - // expect(generateForArgs({ objProp: { x: true } })).toMatchInlineSnapshot( - // `` - // ); - // }); - // test('multiple properties', () => { - // expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(``); - // }); - // test('1 slot property', () => { - // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content'])).toMatchInlineSnapshot(` - // - // {{ content }} - // - // `); - // }); - // test('multiple slot property with second slot value not set', () => { - // expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, ['content', 'footer'])) - // .toMatchInlineSnapshot(` - // - // {{ content }} - // - // `); - // }); - // test('multiple slot property with second slot value is set', () => { - // expect(generateForArgs({ content: 'xyz', footer: 'foo', myProp: 'abc' }, ['content', 'footer'])) - // .toMatchInlineSnapshot(` - // - // - // - // - // `); - // }); - // // test mutil components - // test('multi component with boolean true', () => { - // expect(generateMultiComponentForArgs({ booleanProp: true })).toMatchInlineSnapshot(` - // - // - // `); - // }); - // test('component is not set', () => { - // expect(generateSource(null, {}, {})).toBeNull(); - // }); }); + +describe('generateSource snippet Vue3', () => {}); From 6ab9fc1c364d33c0e2b35c81ee492503dfb31dda Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 24 Feb 2023 16:01:40 +0400 Subject: [PATCH 0030/1428] add new tests --- .../vue3/src/docs/sourceDecorator.ts | 166 +++++++----------- code/renderers/vue3/src/docs/tests/Button.vue | 50 ------ .../vue3/src/docs/tests/source.test.ts | 26 --- 3 files changed, 65 insertions(+), 177 deletions(-) delete mode 100644 code/renderers/vue3/src/docs/tests/Button.vue delete mode 100644 code/renderers/vue3/src/docs/tests/source.test.ts diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 1a59b1e4c783..e1569d49222e 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -6,23 +6,18 @@ import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { - TemplateChildNode, ElementNode, AttributeNode, DirectiveNode, TextNode, InterpolationNode, + TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; +import type { Component } from 'vue'; import { h } from 'vue'; +import { kebabCase } from 'lodash'; -type ArgEntries = [string, any][]; -type Attribute = { - name: string; - value: string; - sourceSpan?: any; - valueSpan?: any; -} & Record; /** * Check if the sourcecode should be generated. * @@ -42,13 +37,22 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const directiveSource = (key: string, value: unknown) => + typeof value === 'function' + ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` + : `${key}='${value}'`; + +const attributeSource = (key: string, value: unknown) => + ['boolean', 'number', 'object'].includes(typeof value) + ? `:${key}='${JSON.stringify(value)}'` + : directiveSource(key, value); /** * * @param _args * @param argTypes * @param byRef */ -function generateAttributesSource( +export function generateAttributesSource( tempArgs: (AttributeNode | DirectiveNode)[], args: Args, argTypes: ArgTypes, @@ -57,10 +61,15 @@ function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; - console.log(`---- arg ${key} `, arg); - const storyArg = args[arg.arg.content]; - console.log(`---- arg.name ${arg.arg.content} `, args); - console.log(`---- storyArg ${arg.arg} `, storyArg); + if (arg.type === 7) { + const { exp, arg: argName } = arg; + const argKey = argName?.content; + const argExpValue = exp?.content; + const argValue = argKey ? args[argKey] : JSON.stringify(args); + return argKey + ? attributeSource(argKey, argValue) + : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + } return tempArgs[key].loc.source; }) .join(' '); @@ -86,15 +95,19 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * get component templates one or more * @param renderFn */ -function getTemplates(renderFn: any): TemplateChildNode[] { +function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { + const { template } = renderFn(); + if (!template) return []; + return getComponentsFromTemplate(template); +} + +export function getComponentsFromTemplate(template: string): TemplateChildNode[] { try { - const { template } = renderFn(); const ast = baseParse(template); const components = ast?.children; if (!components) return []; return components; } catch (e) { - // console.error(e); return []; } } @@ -108,41 +121,43 @@ function getTemplates(renderFn: any): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ export function generateSource( - componentOrNode: TemplateChildNode[], + componentOrNode: (TemplateChildNode | (Component & { type?: number }))[], args: Args, argTypes: ArgTypes, byRef?: boolean | undefined ): string | null { - const generateComponentSource = (component: TemplateChildNode) => { + const generateComponentSource = ( + component: TemplateChildNode | (Component & { type?: number }) + ) => { let attributes; let name; let children; let content; - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - - if (component.type === 2) { - const child = component as TextNode; - content = child.content; + if (component) { + if (component.type === 1) { + const child = component as ElementNode; + attributes = child.props; + name = child.tag; + children = child.children; + } + if (component.type === 5) { + const child = component as InterpolationNode; + content = child.content; + } + if (component.type === 2) { + const child = component as TextNode; + content = child.content; + } } - const concreteComponent = component as any; + const concreteComponent = component as Component & { render: any }; if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; - const attributesNode = mapAttributesAndDirectives(props); - - attributes = attributesNode; + attributes = mapAttributesAndDirectives(props); } - name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + name = + vnode.type || concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; @@ -151,7 +166,7 @@ export function generateSource( if (name) source += `<${name} ${props} >`; if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).concat(''); + source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(' '); } if (content) { if (typeof content !== 'string') content = args[content.content.toString().split('.')[1]]; @@ -161,79 +176,26 @@ export function generateSource( return source; }; - const source = Object.keys(componentOrNode) - .map((key: any) => generateComponentSource(componentOrNode[key])) - .join(' '); + if (Array.isArray(componentOrNode)) { + return componentOrNode.map((node) => generateComponentSource(node)).join(' '); + } - return source; + return null; } -function mapAttributesAndDirectives(props: Args) { - const eventDirective = (key: string, value: unknown) => - typeof value === 'function' - ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` - : `${key}='${value}'`; - - const source = (key: string, value: unknown) => - ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${value}'` - : eventDirective(key, value); - +export function mapAttributesAndDirectives(props: Args) { return Object.keys(props).map( (key) => ({ name: 'bind', - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, - arg: { content: key, loc: { source: key } }, - loc: { source: source(key, props[key]) }, - exp: { isStatic: false, loc: { source: props[key] } }, + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive + arg: { content: kebabCase(key), loc: { source: kebabCase(key) } }, // attribute name or directive name (v-bind, v-on, v-model) + loc: { source: attributeSource(kebabCase(key), props[key]) }, // attribute value or directive value + exp: { isStatic: false, loc: { source: props[key] } }, // directive expression modifiers: [''], } as unknown as AttributeNode) ); } - -/** - * create Named Slots content in source - * @param slotProps - * @param slotArgs - */ - -function createNamedSlots(slotArgs: ArgEntries, slotProps: ArgEntries, byRef?: boolean) { - if (!slotArgs) return ''; - const many = slotProps.length > 1; - return slotArgs - .map(([key, value]) => { - const content = !byRef ? JSON.stringify(value) : `{{ ${key} }}`; - return many ? ` ` : ` ${content}`; - }) - .join('\n'); -} - -function getArgsInAttrs(args: Args, attributes: Attribute[]) { - return Object.keys(args).reduce((acc, prop) => { - if (attributes?.find((attr: any) => attr.name === 'v-bind')) { - acc[prop] = args[prop]; - } - const attribute = attributes?.find( - (attr: any) => attr.name === prop || attr.name === `:${prop}` - ); - if (attribute) { - acc[prop] = attribute.name === `:${prop}` ? args[prop] : attribute.value; - } - if (Object.keys(acc).length === 0) { - attributes?.forEach((attr: any) => { - acc[attr.name] = JSON.parse(JSON.stringify(attr.value)); - }); - } - return acc; - }, {} as Record); -} - -/** - * format prettier for vue - * @param source - */ - /** * source decorator. * @param storyFn Fn @@ -259,7 +221,9 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - const components = getTemplates(context?.originalStoryFn); + const components = getComponentsFromRenderFn(context?.originalStoryFn); + + console.log(' components ', components); const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; diff --git a/code/renderers/vue3/src/docs/tests/Button.vue b/code/renderers/vue3/src/docs/tests/Button.vue deleted file mode 100644 index eb99ffbc5af2..000000000000 --- a/code/renderers/vue3/src/docs/tests/Button.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - \ No newline at end of file diff --git a/code/renderers/vue3/src/docs/tests/source.test.ts b/code/renderers/vue3/src/docs/tests/source.test.ts deleted file mode 100644 index 4b11b4e6589c..000000000000 --- a/code/renderers/vue3/src/docs/tests/source.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { baseParse } from '@vue/compiler-core'; -import Button from './Button.vue'; - -describe('generateSource Vue3', () => { - test('get template from storyFn :', () => { - const storyFn = (args: any) => ({ - components: { Button }, - setup() { - return { - args, - }; - }, - template: `
- - {{args.label}}`, - }); - const storyFnString = storyFn.toString(); - console.log(' storyFn ', storyFnString); - const { template } = storyFn(); - const ast = baseParse(template); - const { children, type } = ast; - console.log({ children, type }); - expect({ booleanProp: true }).toEqual({ booleanProp: true }); - }); -}); From 46307ba9905199b4f69173da80eb685c19026552 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:13:08 +0400 Subject: [PATCH 0031/1428] adjusting the render with tests --- code/renderers/vue3/src/decorateStory.ts | 6 +- .../vue3/src/docs/sourceDecorator.ts | 116 ++++++++++++++---- code/renderers/vue3/src/render.ts | 4 +- 3 files changed, 98 insertions(+), 28 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index e2a56ec7204b..575842f16b9c 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -33,11 +33,7 @@ function prepare( }; } - return { - render() { - return h(story); - }, - }; + return () => h(story); } export function decorateStory( diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e1569d49222e..ca24568bd6b8 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons, useEffect } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { DocgenInfo, getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, @@ -15,8 +15,8 @@ import type { } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; import type { Component } from 'vue'; -import { h } from 'vue'; -import { kebabCase } from 'lodash'; +import { toDisplayString, h } from 'vue'; +import { camelCase, kebabCase } from 'lodash'; /** * Check if the sourcecode should be generated. @@ -37,14 +37,25 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const displayObject = (obj: Args) => { + const a = Object.keys(obj).map((key) => `${key}:"${obj[key]}"`); + return `{${a.join(',')}}`; +}; +const htmlEventAttributeToVueEventAttribute = (key: string) => { + return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; +}; +// html event attribute to vue event attribute +// is html event attribute + const directiveSource = (key: string, value: unknown) => - typeof value === 'function' - ? `${key.replace(/on([A-Z][a-z]+)/g, '@$1').toLowerCase()}='()=>{}'` + key.includes('on') + ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` : `${key}='${value}'`; const attributeSource = (key: string, value: unknown) => + // convert html event key to vue event key ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${JSON.stringify(value)}'` + ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` : directiveSource(key, value); /** * @@ -61,14 +72,17 @@ export function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; + console.log('------> arg:', arg); if (arg.type === 7) { const { exp, arg: argName } = arg; const argKey = argName?.content; const argExpValue = exp?.content; - const argValue = argKey ? args[argKey] : JSON.stringify(args); + const propValue = args[camelCase(argKey)]; + console.log('-->argKey', argKey, 'argExpValue :', argExpValue, 'propValue :', propValue); + const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + : toDisplayString(tempArgs[key].loc.source); // tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); } return tempArgs[key].loc.source; }) @@ -96,12 +110,16 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * @param renderFn */ function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { - const { template } = renderFn(); - if (!template) return []; - return getComponentsFromTemplate(template); + try { + const { template } = renderFn(); + if (!template) return []; + return getComponentsFromTemplate(template); + } catch (e) { + return []; + } } -export function getComponentsFromTemplate(template: string): TemplateChildNode[] { +function getComponentsFromTemplate(template: string): TemplateChildNode[] { try { const ast = baseParse(template); const components = ast?.children; @@ -149,15 +167,35 @@ export function generateSource( content = child.content; } } - const concreteComponent = component as Component & { render: any }; + const concreteComponent = component as Component & { + render: any; + props: any; + slots: any; + tag?: string; + name?: string; + __name?: string; + }; if (typeof concreteComponent.render === 'function') { const vnode = h(component, args); if (vnode.props) { const { props } = vnode; - attributes = mapAttributesAndDirectives(props); + concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); + const slotsProps = {} as Args; + const attrsProps = { ...props } as Args; + Object.keys(props).forEach((prop: any) => { + const isSlot = concreteComponent.slots.find( + ({ name: slotName }: { name: string }) => slotName === prop + ); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); + + attributes = mapAttributesAndDirectives(attrsProps); + children = mapSlots(slotsProps); } - name = - vnode.type || concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; } let source = ''; @@ -166,10 +204,11 @@ export function generateSource( if (name) source += `<${name} ${props} >`; if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(' '); + source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(''); } if (content) { - if (typeof content !== 'string') content = args[content.content.toString().split('.')[1]]; + // eslint-disable-next-line no-eval + if (typeof content !== 'string') content = eval(content.loc.source); // it's a binding safe to eval source += content; } if (name) source += ``; @@ -183,14 +222,15 @@ export function generateSource( return null; } -export function mapAttributesAndDirectives(props: Args) { +function mapAttributesAndDirectives(props: Args) { + const tranformKey = (key: string) => (key.startsWith('on') ? key : kebabCase(key)); return Object.keys(props).map( (key) => ({ name: 'bind', type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive - arg: { content: kebabCase(key), loc: { source: kebabCase(key) } }, // attribute name or directive name (v-bind, v-on, v-model) - loc: { source: attributeSource(kebabCase(key), props[key]) }, // attribute value or directive value + arg: { content: tranformKey(key), loc: { source: tranformKey(key) } }, // attribute name or directive name (v-bind, v-on, v-model) + loc: { source: attributeSource(tranformKey(key), props[key]) }, // attribute value or directive value exp: { isStatic: false, loc: { source: props[key] } }, // directive expression modifiers: [''], } as unknown as AttributeNode) @@ -238,3 +278,37 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; +// export local function for testing purpose +export { + generateScriptSetup, + getComponentsFromRenderFn, + getComponentsFromTemplate, + mapAttributesAndDirectives, + attributeSource, + htmlEventAttributeToVueEventAttribute, +}; + +function mapSlots(slotsProps: Args): TextNode[] { + return Object.keys(slotsProps).map((key) => { + const slot = slotsProps[key]; + let slotContent = ''; + if (typeof slot === 'function') slotContent = ``; + if (key === 'default') { + slotContent = JSON.stringify(slot); + } + slotContent = ``; + + const txt: TextNode = { + type: 2, + content: slotContent, + loc: { + source: slotContent, + start: { offset: 0, line: 1, column: 0 }, + end: { offset: 0, line: 1, column: 0 }, + }, + }; + return txt; + }); + + // TODO: handle other cases (array, object, html,etc) +} diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 19e095bc5367..968976eda4b9 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -11,7 +11,7 @@ export const render: ArgsStoryFn = (props, context) => { `Unable to render story ${id} as the component annotation is missing from the default export` ); } - + console.log(' props ', props); return h(Component, props, getSlots(props, context)); }; @@ -33,7 +33,7 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); storyContext.args = reactive(storyContext.args); - const rootComponent: any = storyFn(); // !existingApp ? storyFn() : existingApp.rootComponent(); + const rootComponent: any = !existingApp ? storyFn() : existingApp.rootComponent; const appProps = rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); From 4c14b109bd24e9d439dc102106fc19ee4c188fb5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:14:21 +0400 Subject: [PATCH 0032/1428] add more tests for sources decorator --- .../vue3/src/docs/sourceDecorator.test.ts | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 18c1296f4d19..3ca9aed946a1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -7,6 +7,8 @@ import { getComponentsFromTemplate, mapAttributesAndDirectives, generateAttributesSource, + attributeSource, + htmlEventAttributeToVueEventAttribute as htmlEventToVueEvent, } from './sourceDecorator'; expect.addSnapshotSerializer({ @@ -291,4 +293,107 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { }); }); -describe('generateSource snippet Vue3', () => {}); +describe('Vue3: generateSource() snippet', () => { + test('template component camelCase string Arg', () => { + expect( + generateForArgs( + { + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase bool Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase bool, string Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot( + `` + ); + }); + + test('template component camelCase object Arg', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); + + test('template component camelCase object Arg and Slot', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + ` SLOT ` + ) + ).toMatchInlineSnapshot(` SLOT `); + }); + + test('template component camelCase object Arg and dynamic Slot content', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + camelCaseStringSlotArg: 'foo', + }, + [] as ArgsType, + ` SLOT {{args.camelCaseStringSlotArg}}` + ) + ).toMatchInlineSnapshot( + ` SLOT foo` + ); + }); +}); + +describe('Vue3: sourceDecorator->attributeSoure()', () => { + test('camelCase boolean Arg', () => { + expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); + }); + + test('html event attribute should convert to vue event directive', () => { + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + }); + test('normal html attribute should not convert to vue event directive', () => { + expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + }); + test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { + const htmlEventAttributeToVueEventAttribute = (attribute: string) => { + return htmlEventToVueEvent(attribute); + }; + expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); + expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); + expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); + expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); + expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); + expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); + }); +}); From c8b7e68c9f6461aaea4914f160c01fd38c019755 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 26 Feb 2023 01:15:17 +0400 Subject: [PATCH 0033/1428] testing the render --- code/renderers/vue3/src/docs/sourceDecorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index ca24568bd6b8..53b45b140d3e 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons, useEffect } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { DocgenInfo, getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, From c8e157eca62da102991141ef0d037d882eaa6e9a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 15:35:40 +0400 Subject: [PATCH 0034/1428] vue3 fixing prepareStory for decotators --- code/renderers/vue3/src/decorateStory.ts | 20 +++++- .../vue3/src/docs/sourceDecorator.ts | 8 +-- code/renderers/vue3/src/render.ts | 65 +++++++++++-------- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 575842f16b9c..37192c41c8d4 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -30,10 +30,26 @@ function prepare( // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, + renderTracked(event) { + console.log('innerStory renderTracked', event); + }, + renderTriggered(event) { + console.log('innerStory renderTriggered', event); + }, }; } - return () => h(story); + return { + render() { + return h(story, this.$props); + }, + renderTracked(event) { + console.log('story renderTracked', event); + }, + renderTriggered(event) { + console.log('story renderTriggered', event); + }, + }; } export function decorateStory( @@ -60,7 +76,7 @@ export function decorateStory( return story; } - return prepare(decoratedStory, h(story, context.args)) as VueRenderer['storyResult']; + return prepare(decoratedStory, story) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 53b45b140d3e..b35afeb0271b 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -72,13 +72,13 @@ export function generateAttributesSource( return Object.keys(tempArgs) .map((key: any) => { const arg = tempArgs[key]; - console.log('------> arg:', arg); + if (arg.type === 7) { - const { exp, arg: argName } = arg; + const { arg: argName } = arg; const argKey = argName?.content; - const argExpValue = exp?.content; + // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; - console.log('-->argKey', argKey, 'argExpValue :', argExpValue, 'propValue :', propValue); + const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 968976eda4b9..ff326fee847f 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -import { createApp, h, reactive } from 'vue'; +import { createApp, h, isReactive, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; import type { VueRenderer } from './types'; @@ -11,7 +10,7 @@ export const render: ArgsStoryFn = (props, context) => { `Unable to render story ${id} as the component annotation is missing from the default export` ); } - console.log(' props ', props); + return h(Component, props, getSlots(props, context)); }; @@ -22,46 +21,56 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any; rootComponent: any } + { vueApp: ReturnType; reactiveArgs: any } >(); export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { - // fetch the story with the updated context (with reactive args) const existingApp = map.get(canvasElement); - - storyContext.args = reactive(storyContext.args); - const rootComponent: any = !existingApp ? storyFn() : existingApp.rootComponent; - - const appProps = - rootComponent.props ?? (typeof rootComponent === 'function' ? rootComponent().props : {}); - const reactiveArgs = Object.keys(appProps).length > 0 ? reactive(appProps) : storyContext.args; - + const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - updateArgs(existingApp.reactiveArgs, reactiveArgs); + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); }; } - if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement); - const storybookApp = createApp({ - render() { - map.set(canvasElement, { vueApp: storybookApp, reactiveArgs, rootComponent }); - return h(rootComponent, reactiveArgs); + // create vue app for the story + const vueStoryApp = createApp({ + setup() { + let { args } = storyContext; + args = reactive(reactiveArgs); + const rootComponent = storyFn(args); + map.set(canvasElement, { + vueApp: vueStoryApp, + reactiveArgs, + }); + return () => h(rootComponent, args); + }, + onMounted() { + map.set(canvasElement, { + vueApp: vueStoryApp, + reactiveArgs, + }); + }, + renderTracked(event) { + console.log('--renderTracked ', event); + }, + renderTriggered(event) { + console.log('--renderTriggered ', event); }, }); - - storybookApp.config.errorHandler = (e: unknown) => showException(e as Error); - setupFunction(storybookApp); - storybookApp.mount(canvasElement); + vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); + setupFunction(vueStoryApp); + vueStoryApp.mount(canvasElement); showMain(); return () => { - teardown(storybookApp, canvasElement); + teardown(vueStoryApp, canvasElement); }; } @@ -87,10 +96,10 @@ function getSlots(props: Args, context: StoryContext) { * @returns */ export function updateArgs(reactiveArgs: Args, nextArgs: Args) { - if (!nextArgs) return; - - Object.keys(reactiveArgs).forEach((key) => delete reactiveArgs[key]); - Object.assign(reactiveArgs, nextArgs); + const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); + Object.entries(nextArgs).forEach(([key, value]) => { + currentArgs[key] = value; + }); } function teardown( From 2a81e98b981014bd1e16240ff58ffe646fd5d80b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 17:24:01 +0400 Subject: [PATCH 0035/1428] some refactoring and cleanup --- code/renderers/vue3/src/docs/sourceDecorator.ts | 2 +- code/renderers/vue3/src/render.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index b35afeb0271b..c2fccc56882f 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.content; + const argKey = argName?.loc.source; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ff326fee847f..dbdf220cb527 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; @@ -42,14 +43,13 @@ export function renderToCanvas( // create vue app for the story const vueStoryApp = createApp({ setup() { - let { args } = storyContext; - args = reactive(reactiveArgs); - const rootComponent = storyFn(args); + storyContext.args = reactive(reactiveArgs); + const rootComponent = storyFn(); map.set(canvasElement, { vueApp: vueStoryApp, reactiveArgs, }); - return () => h(rootComponent, args); + return () => h(rootComponent, reactiveArgs); }, onMounted() { map.set(canvasElement, { @@ -58,10 +58,10 @@ export function renderToCanvas( }); }, renderTracked(event) { - console.log('--renderTracked ', event); + console.log('vueApp--renderTracked ', event); }, renderTriggered(event) { - console.log('--renderTriggered ', event); + console.log('vueApp--renderTriggered ', event); }, }); vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); From 233361a61365cd2782d9e0758b2647cb9cef0fba Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 22:36:15 +0400 Subject: [PATCH 0036/1428] fix null slots --- code/renderers/vue3/src/render.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index dbdf220cb527..ebcfd3442527 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -30,6 +30,7 @@ export function renderToCanvas( canvasElement: VueRenderer['canvasElement'] ) { const existingApp = map.get(canvasElement); + const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { @@ -41,19 +42,19 @@ export function renderToCanvas( if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement); // create vue app for the story - const vueStoryApp = createApp({ + const vueApp = createApp({ setup() { storyContext.args = reactive(reactiveArgs); - const rootComponent = storyFn(); + const rootElement = storyFn(); map.set(canvasElement, { - vueApp: vueStoryApp, + vueApp, reactiveArgs, }); - return () => h(rootComponent, reactiveArgs); + return () => h(rootElement, reactiveArgs); }, onMounted() { map.set(canvasElement, { - vueApp: vueStoryApp, + vueApp, reactiveArgs, }); }, @@ -64,13 +65,13 @@ export function renderToCanvas( console.log('vueApp--renderTriggered ', event); }, }); - vueStoryApp.config.errorHandler = (e: unknown) => showException(e as Error); - setupFunction(vueStoryApp); - vueStoryApp.mount(canvasElement); + vueApp.config.errorHandler = (e: unknown) => showException(e as Error); + setupFunction(vueApp); + vueApp.mount(canvasElement); showMain(); return () => { - teardown(vueStoryApp, canvasElement); + teardown(vueApp, canvasElement); }; } From 2b205f8a5ed2e7beeb3baf07049fdad34c457d01 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 27 Feb 2023 22:37:24 +0400 Subject: [PATCH 0037/1428] handle no slots , null issue --- .../vue3/src/docs/sourceDecorator.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index c2fccc56882f..cb2ff6131304 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source; + const argKey = argName?.loc.source ?? (argName as any).content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -180,17 +180,17 @@ export function generateSource( if (vnode.props) { const { props } = vnode; concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); + const { slots } = concreteComponent; const slotsProps = {} as Args; const attrsProps = { ...props } as Args; - Object.keys(props).forEach((prop: any) => { - const isSlot = concreteComponent.slots.find( - ({ name: slotName }: { name: string }) => slotName === prop - ); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); + if (slots) + Object.keys(props).forEach((prop: any) => { + const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); attributes = mapAttributesAndDirectives(attrsProps); children = mapSlots(slotsProps); From 80b3bb19f2127094f968af0c600159159275df41 Mon Sep 17 00:00:00 2001 From: Luca Ziliani Date: Tue, 28 Feb 2023 17:40:34 +0100 Subject: [PATCH 0038/1428] :fire: Zoom Element: remove css zoom --- code/ui/components/src/Zoom/ZoomElement.tsx | 20 +++++-------------- code/ui/components/src/Zoom/ZoomIFrame.tsx | 20 ++++++------------- .../src/Zoom/browserSupportsCssZoom.ts | 13 ------------ 3 files changed, 11 insertions(+), 42 deletions(-) delete mode 100644 code/ui/components/src/Zoom/browserSupportsCssZoom.ts diff --git a/code/ui/components/src/Zoom/ZoomElement.tsx b/code/ui/components/src/Zoom/ZoomElement.tsx index b47d70b66576..18aef8c905e3 100644 --- a/code/ui/components/src/Zoom/ZoomElement.tsx +++ b/code/ui/components/src/Zoom/ZoomElement.tsx @@ -2,23 +2,13 @@ import type { ReactElement, RefObject } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { styled } from '@storybook/theming'; -import { browserSupportsCssZoom } from './browserSupportsCssZoom'; - -const hasBrowserSupportForCssZoom = browserSupportsCssZoom(); const ZoomElementWrapper = styled.div<{ scale: number; elementHeight: number }>( - ({ scale = 1, elementHeight }) => - hasBrowserSupportForCssZoom - ? { - '> *': { - zoom: 1 / scale, - }, - } - : { - height: elementHeight || 'auto', - transformOrigin: 'top left', - transform: `scale(${1 / scale})`, - } + ({ scale = 1, elementHeight }) => ({ + height: elementHeight || 'auto', + transformOrigin: 'top left', + transform: `scale(${1 / scale})`, + }) ); const useMutationObserver = ({ diff --git a/code/ui/components/src/Zoom/ZoomIFrame.tsx b/code/ui/components/src/Zoom/ZoomIFrame.tsx index 255a39c414b5..3101364bd861 100644 --- a/code/ui/components/src/Zoom/ZoomIFrame.tsx +++ b/code/ui/components/src/Zoom/ZoomIFrame.tsx @@ -1,6 +1,5 @@ import type { RefObject, ReactElement } from 'react'; import { Component } from 'react'; -import { browserSupportsCssZoom } from './browserSupportsCssZoom'; export type IZoomIFrameProps = { scale: number; @@ -36,19 +35,12 @@ export class ZoomIFrame extends Component { setIframeInnerZoom(scale: number) { try { - if (browserSupportsCssZoom()) { - Object.assign(this.iframe.contentDocument.body.style, { - zoom: 1 / scale, - minHeight: `calc(100vh / ${1 / scale})`, - }); - } else { - Object.assign(this.iframe.contentDocument.body.style, { - width: `${scale * 100}%`, - height: `${scale * 100}%`, - transform: `scale(${1 / scale})`, - transformOrigin: 'top left', - }); - } + Object.assign(this.iframe.contentDocument.body.style, { + width: `${scale * 100}%`, + height: `${scale * 100}%`, + transform: `scale(${1 / scale})`, + transformOrigin: 'top left', + }); } catch (e) { this.setIframeZoom(scale); } diff --git a/code/ui/components/src/Zoom/browserSupportsCssZoom.ts b/code/ui/components/src/Zoom/browserSupportsCssZoom.ts deleted file mode 100644 index 43ff5b9004cf..000000000000 --- a/code/ui/components/src/Zoom/browserSupportsCssZoom.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function browserSupportsCssZoom(): boolean { - try { - // First checks if Safari is being used, because Safari supports zoom, but it's buggy: https://developer.mozilla.org/en-US/docs/Web/CSS/zoom#browser_compatibility - if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { - return false; - } - - // Next check if the browser supports zoom styling - return global.CSS?.supports('zoom: 1'); - } catch (error) { - return false; - } -} From b874cbe0fc1b53da540a5d55bbfe411a4b3000a7 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 16:29:36 +0400 Subject: [PATCH 0039/1428] fix args Inheritance and reactivity --- .../store/template/stories/args.stories.ts | 1 + code/renderers/vue3/src/decorateStory.ts | 31 ++++++++++++++++--- .../vue3/src/docs/sourceDecorator.ts | 26 +++++++--------- code/renderers/vue3/src/render.ts | 27 +++++++++------- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/code/lib/store/template/stories/args.stories.ts b/code/lib/store/template/stories/args.stories.ts index e1e6eb7104f4..e655f9e9d50f 100644 --- a/code/lib/store/template/stories/args.stories.ts +++ b/code/lib/store/template/stories/args.stories.ts @@ -73,6 +73,7 @@ export const Events = { await within(canvasElement).findByText(/initial/); await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { test: 'updated' } }); + await new Promise((resolve) => channel.once(STORY_ARGS_UPDATED, resolve)); await within(canvasElement).findByText(/updated/); await channel.emit(RESET_STORY_ARGS, { storyId: id }); diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 37192c41c8d4..ab905becc1de 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -31,10 +31,10 @@ function prepare( ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, renderTracked(event) { - console.log('innerStory renderTracked', event); + // console.log('innerStory renderTracked', event); }, renderTriggered(event) { - console.log('innerStory renderTriggered', event); + // console.log('innerStory renderTriggered', event); }, }; } @@ -44,10 +44,10 @@ function prepare( return h(story, this.$props); }, renderTracked(event) { - console.log('story renderTracked', event); + // console.log('story renderTracked', event); }, renderTriggered(event) { - console.log('story renderTriggered', event); + // console.log('story renderTriggered', event); }, }; } @@ -61,6 +61,9 @@ export function decorateStory( let story: VueRenderer['storyResult'] | undefined; const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { + // we should update the context with the update object from the decorator in reactive way + // so that the story will be re-rendered with the new context + updateReactiveContext(context, update); story = decorated({ ...context, ...sanitizeStoryContextUpdate(update), @@ -81,3 +84,23 @@ export function decorateStory( (context) => prepare(storyFn(context)) as LegacyStoryFn ); } + +function updateReactiveContext( + context: StoryContext, + update: + | import('@storybook/csf').StoryContextUpdate> + | undefined +) { + if (update) { + const { args, argTypes } = update; + if (args && !argTypes) { + const deepCopy = JSON.parse(JSON.stringify(args)); + Object.keys(context.args).forEach((key) => { + delete context.args[key]; + }); + Object.keys(args).forEach((key) => { + context.args[key] = deepCopy[key]; + }); + } + } +} diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index cb2ff6131304..8bd2866ff0a1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -263,14 +263,11 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const components = getComponentsFromRenderFn(context?.originalStoryFn); - console.log(' components ', components); - const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - console.log(' generatedTemplate ', generatedTemplate); if (generatedTemplate) { source = `${generatedScript}\n `; @@ -278,15 +275,6 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; -// export local function for testing purpose -export { - generateScriptSetup, - getComponentsFromRenderFn, - getComponentsFromTemplate, - mapAttributesAndDirectives, - attributeSource, - htmlEventAttributeToVueEventAttribute, -}; function mapSlots(slotsProps: Args): TextNode[] { return Object.keys(slotsProps).map((key) => { @@ -298,7 +286,7 @@ function mapSlots(slotsProps: Args): TextNode[] { } slotContent = ``; - const txt: TextNode = { + return { type: 2, content: slotContent, loc: { @@ -307,8 +295,16 @@ function mapSlots(slotsProps: Args): TextNode[] { end: { offset: 0, line: 1, column: 0 }, }, }; - return txt; }); - // TODO: handle other cases (array, object, html,etc) } + +// export local function for testing purpose +export { + generateScriptSetup, + getComponentsFromRenderFn, + getComponentsFromTemplate, + mapAttributesAndDirectives, + attributeSource, + htmlEventAttributeToVueEventAttribute, +}; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ebcfd3442527..68b42e3245a6 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -31,7 +31,8 @@ export function renderToCanvas( ) { const existingApp = map.get(canvasElement); - const reactiveArgs: Args = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + // updateArgs(reactiveArgs, storyContext.initialArgs); // update the reactiveArgs with the latest args // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); @@ -44,25 +45,21 @@ export function renderToCanvas( // create vue app for the story const vueApp = createApp({ setup() { - storyContext.args = reactive(reactiveArgs); + storyContext.args = reactiveArgs; const rootElement = storyFn(); - map.set(canvasElement, { - vueApp, - reactiveArgs, - }); return () => h(rootElement, reactiveArgs); }, - onMounted() { + mounted() { map.set(canvasElement, { vueApp, reactiveArgs, }); }, renderTracked(event) { - console.log('vueApp--renderTracked ', event); + // console.log('vueApp--renderTracked ', event); }, renderTriggered(event) { - console.log('vueApp--renderTriggered ', event); + // console.log('vueApp--renderTriggered ', event); }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); @@ -98,8 +95,16 @@ function getSlots(props: Args, context: StoryContext) { */ export function updateArgs(reactiveArgs: Args, nextArgs: Args) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - Object.entries(nextArgs).forEach(([key, value]) => { - currentArgs[key] = value; + + Object.keys(currentArgs).forEach((key) => { + const componentArg = currentArgs[key]; + if (typeof componentArg === 'object') { + Object.keys(componentArg).forEach((key2) => { + componentArg[key2] = nextArgs[key2]; + }); + } else { + currentArgs[key] = nextArgs[key]; + } }); } From 3d0b620e82638eadd1d9b4d9ea6237b9c2cac9d2 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 20:29:54 +0400 Subject: [PATCH 0040/1428] fix null value exception for mdx stories --- .../vue3/src/docs/sourceDecorator.ts | 45 ++++++++++--------- code/renderers/vue3/src/render.ts | 2 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 8bd2866ff0a1..dd550176a839 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -75,7 +75,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source ?? (argName as any).content; + const argKey = argName?.loc.source ?? (argName as any)?.content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -139,7 +139,10 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ export function generateSource( - componentOrNode: (TemplateChildNode | (Component & { type?: number }))[], + componentOrNode: + | (TemplateChildNode | (Component & { type?: number }))[] + | TemplateChildNode + | (Component & { type?: number }), args: Args, argTypes: ArgTypes, byRef?: boolean | undefined @@ -151,22 +154,23 @@ export function generateSource( let name; let children; let content; - if (component) { - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - if (component.type === 2) { - const child = component as TextNode; - content = child.content; - } + if (!component) return null; + + if (component.type === 1) { + const child = component as ElementNode; + attributes = child.props; + name = child.tag; + children = child.children; } + if (component.type === 5) { + const child = component as InterpolationNode; + content = child.content; + } + if (component.type === 2) { + const child = component as TextNode; + content = child.content; + } + const concreteComponent = component as Component & { render: any; props: any; @@ -214,8 +218,9 @@ export function generateSource( if (name) source += ``; return source; }; - - if (Array.isArray(componentOrNode)) { + if (componentOrNode && !Array.isArray(componentOrNode)) + return generateComponentSource(componentOrNode); + if (componentOrNode && componentOrNode.length) { return componentOrNode.map((node) => generateComponentSource(node)).join(' '); } @@ -263,7 +268,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const components = getComponentsFromRenderFn(context?.originalStoryFn); - const storyComponent = components.length ? components : [ctxtComponent as TemplateChildNode]; + const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 68b42e3245a6..25b4edbb1293 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -32,7 +32,7 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - // updateArgs(reactiveArgs, storyContext.initialArgs); // update the reactiveArgs with the latest args + // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); From da8b3b4051f8503acf4b370a34b206a38995ba2a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 20:38:50 +0400 Subject: [PATCH 0041/1428] fix slot double quote display --- code/renderers/vue3/src/render.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 25b4edbb1293..207a43ef37bc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -82,7 +82,10 @@ function getSlots(props: Args, context: StoryContext) { const { argTypes } = context; const slots = Object.entries(props) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') - .map(([key, value]) => [key, () => h('span', JSON.stringify(value))]); + .map(([key, value]) => [ + key, + () => h('template', typeof value === 'object' ? JSON.stringify(value) : value), + ]); return Object.fromEntries(slots); } From dc32a8377aa41e8aba24973be20cb5830ec9af50 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 1 Mar 2023 22:23:47 +0400 Subject: [PATCH 0042/1428] fix source for mdx using originFn and context --- code/renderers/vue3/src/docs/sourceDecorator.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index dd550176a839..4e476bec49cd 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -109,9 +109,12 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): * get component templates one or more * @param renderFn */ -function getComponentsFromRenderFn(renderFn: any): TemplateChildNode[] { +function getComponentsFromRenderFn( + renderFn: any, + context?: StoryContext +): TemplateChildNode[] { try { - const { template } = renderFn(); + const { template } = context ? renderFn(context.args, context) : renderFn(); if (!template) return []; return getComponentsFromTemplate(template); } catch (e) { @@ -266,7 +269,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - const components = getComponentsFromRenderFn(context?.originalStoryFn); + const components = getComponentsFromRenderFn(context?.originalStoryFn, context); const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); From 14eca7cf7bf52b56829a4eddfb4465e78f8b3ef9 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 2 Mar 2023 13:29:23 +0400 Subject: [PATCH 0043/1428] refactory generateSource to be elegante --- .../vue3/src/docs/sourceDecorator.ts | 148 ++++++++---------- code/renderers/vue3/src/render.ts | 1 - 2 files changed, 67 insertions(+), 82 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 4e476bec49cd..a7a21d5afbb6 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,10 +14,22 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import type { Component } from 'vue'; +import type { Component, VNodeProps } from 'vue'; import { toDisplayString, h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; +type StoryVueComponent = Component & { + render: any; + props: VNodeProps; + slots: any; + tag?: string; + name?: string; + __name?: string; + __file?: string; + __docs?: any; + __docsGen?: any; + __docsExtracted?: any; +}; /** * Check if the sourcecode should be generated. * @@ -75,7 +87,7 @@ export function generateAttributesSource( if (arg.type === 7) { const { arg: argName } = arg; - const argKey = argName?.loc.source ?? (argName as any)?.content; + const argKey = argName ? argName?.loc.source : undefined; // (argName as any)?.content; // const argExpValue = exp?.content; const propValue = args[camelCase(argKey)]; @@ -141,93 +153,67 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param argTypes ArgTypes * @param slotProp Prop used to simulate a slot */ -export function generateSource( - componentOrNode: - | (TemplateChildNode | (Component & { type?: number }))[] - | TemplateChildNode - | (Component & { type?: number }), + +function generateSource( + componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, - byRef?: boolean | undefined -): string | null { - const generateComponentSource = ( - component: TemplateChildNode | (Component & { type?: number }) - ) => { - let attributes; - let name; - let children; - let content; - if (!component) return null; - - if (component.type === 1) { - const child = component as ElementNode; - attributes = child.props; - name = child.tag; - children = child.children; - } - if (component.type === 5) { - const child = component as InterpolationNode; - content = child.content; - } - if (component.type === 2) { - const child = component as TextNode; - content = child.content; + byRef = false +) { + const isComponent = (component: any) => component && typeof component.render === 'function'; + const isElementNode = (node: any) => node && node.type === 1; + const isInterpolationNode = (node: any) => node && node.type === 5; + const isTextNode = (node: any) => node && node.type === 2; + + const generateComponentSource = (componentOrNode: StoryVueComponent | TemplateChildNode) => { + if (isElementNode(componentOrNode)) { + const { tag: name, props: attributes, children } = componentOrNode as ElementNode; + const childSources: string = children + .map((child: TemplateChildNode) => generateComponentSource(child)) + .join(''); + const props = generateAttributesSource(attributes, args, argTypes, byRef); + return `<${name} ${props}>${childSources}`; } - const concreteComponent = component as Component & { - render: any; - props: any; - slots: any; - tag?: string; - name?: string; - __name?: string; - }; - if (typeof concreteComponent.render === 'function') { - const vnode = h(component, args); - if (vnode.props) { - const { props } = vnode; - concreteComponent.slots = getDocgenSection(concreteComponent, 'slots'); - const { slots } = concreteComponent; - const slotsProps = {} as Args; - const attrsProps = { ...props } as Args; - if (slots) - Object.keys(props).forEach((prop: any) => { - const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); - - attributes = mapAttributesAndDirectives(attrsProps); - children = mapSlots(slotsProps); - } - name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { + const { content } = componentOrNode as InterpolationNode | TextNode; + // eslint-disable-next-line no-eval + if (typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval + return content; } - let source = ''; - const templateAttrs = attributes ?? []; // keep only args that are in attributes - const props = generateAttributesSource(templateAttrs, args, argTypes, byRef); - if (name) source += `<${name} ${props} >`; - - if (children) { - source += children.map((node: TemplateChildNode) => generateComponentSource(node)).join(''); + if (isComponent(componentOrNode)) { + const concreteComponent = componentOrNode as StoryVueComponent; + const vnode = h(componentOrNode, args); + const { props } = vnode; + const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; + const slotsProps = {} as Args; + const attrsProps = { ...props }; + if (slots && props) + Object.keys(props).forEach((prop: any) => { + const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); + if (isSlot?.name) { + slotsProps[prop] = props[prop]; + delete attrsProps[prop]; + } + }); + const attributes = mapAttributesAndDirectives(attrsProps); + const childSources: string = mapSlots(slotsProps) + .map((child) => generateComponentSource(child)) + .join(''); + const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); + return `<${name} ${propsSource}>${childSources}`; } - if (content) { - // eslint-disable-next-line no-eval - if (typeof content !== 'string') content = eval(content.loc.source); // it's a binding safe to eval - source += content; - } - if (name) source += ``; - return source; + + return null; }; - if (componentOrNode && !Array.isArray(componentOrNode)) - return generateComponentSource(componentOrNode); - if (componentOrNode && componentOrNode.length) { - return componentOrNode.map((node) => generateComponentSource(node)).join(' '); - } - return null; + const componentsOrNodes = Array.isArray(componentOrNodes) ? componentOrNodes : [componentOrNodes]; + const source = componentsOrNodes + .map((componentOrNode) => generateComponentSource(componentOrNode)) + .join(' '); + return source || null; } function mapAttributesAndDirectives(props: Args) { @@ -289,10 +275,10 @@ function mapSlots(slotsProps: Args): TextNode[] { const slot = slotsProps[key]; let slotContent = ''; if (typeof slot === 'function') slotContent = ``; + slotContent = ``; if (key === 'default') { slotContent = JSON.stringify(slot); } - slotContent = ``; return { type: 2, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 207a43ef37bc..d56362c0e6c4 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -32,7 +32,6 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { updateArgs(existingApp.reactiveArgs, storyContext.args); From b7584963d5d346b82da6b526950976d8a216b005 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 2 Mar 2023 21:12:20 +0400 Subject: [PATCH 0044/1428] add some tests for source decorator and render --- .../vue3/src/docs/sourceDecorator.test.ts | 18 +++---- .../vue3/src/docs/sourceDecorator.ts | 2 +- code/renderers/vue3/src/render.test.ts | 51 +++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 code/renderers/vue3/src/render.test.ts diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 3ca9aed946a1..78616e4d06a2 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -303,7 +303,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool Arg', () => { @@ -315,7 +315,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool, string Arg', () => { @@ -329,7 +329,7 @@ describe('Vue3: generateSource() snippet', () => { `` ) ).toMatchInlineSnapshot( - `` + `` ); }); @@ -342,7 +342,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase object Arg and Slot', () => { @@ -352,9 +352,9 @@ describe('Vue3: generateSource() snippet', () => { camelCaseObjectArg: { foo: 'bar' }, }, [] as ArgsType, - ` SLOT ` + ` SLOT ` ) - ).toMatchInlineSnapshot(` SLOT `); + ).toMatchInlineSnapshot(` SLOT `); }); test('template component camelCase object Arg and dynamic Slot content', () => { @@ -365,10 +365,10 @@ describe('Vue3: generateSource() snippet', () => { camelCaseStringSlotArg: 'foo', }, [] as ArgsType, - ` SLOT {{args.camelCaseStringSlotArg}}` + ` SLOT {{args.camelCaseStringSlotArg}}` ) ).toMatchInlineSnapshot( - ` SLOT foo` + ` SLOT foo` ); }); }); @@ -380,7 +380,7 @@ describe('Vue3: sourceDecorator->attributeSoure()', () => { test('html event attribute should convert to vue event directive', () => { expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); }); test('normal html attribute should not convert to vue event directive', () => { expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a7a21d5afbb6..fe91412f8818 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -154,7 +154,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ -function generateSource( +export function generateSource( componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts new file mode 100644 index 000000000000..c705fb16cdc1 --- /dev/null +++ b/code/renderers/vue3/src/render.test.ts @@ -0,0 +1,51 @@ +import { expectTypeOf } from 'expect-type'; + +import { reactive } from 'vue'; +import { updateArgs } from './render'; + +describe('Render Story', () => { + test('update reactive Args updateArgs()', () => { + const reactiveArgs = reactive({ argFoo: 'foo', argBar: 'bar' }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); + expect(reactiveArgs).toEqual(newArgs); + }); + + test('update reactive Args component new arg updateArgs()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo', argBar: 'bar' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); + expect(reactiveArgs).toEqual({ objectArg: newArgs }); + }); + + test('update reactive Args component 2 args updateArgs()', () => { + const reactiveArgs = reactive({ + objectArg: { argFoo: 'foo' }, + objectArg2: { argBar: 'bar' }, + }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + objectArg2: { argBar: string }; + }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + objectArg2: { argBar: string }; + }>(); + expect(reactiveArgs).toEqual({ + objectArg: { argFoo: newArgs.argFoo }, + objectArg2: { argBar: newArgs.argBar }, + }); + }); +}); From ada469dd401268bf53509b9573d93b97364f37c5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 13:52:19 +0400 Subject: [PATCH 0045/1428] add some tests and fix CSF2 with decorator --- code/renderers/vue3/src/decorateStory.ts | 29 ++++++++------ code/renderers/vue3/src/render.test.ts | 25 ++++++++---- code/renderers/vue3/src/render.ts | 51 +++++++++++++++++++----- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index ab905becc1de..21cb19537ef0 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,5 +1,5 @@ import type { ConcreteComponent, Component, ComponentOptions } from 'vue'; -import { h } from 'vue'; +import { reactive, h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; @@ -19,21 +19,22 @@ function prepare( rawStory: VueRenderer['storyResult'], innerStory?: ConcreteComponent ): Component | null { - const story = rawStory as ComponentOptions; + const story = normalizeFunctionalComponent(rawStory as ComponentOptions); if (story == null) { return null; } if (innerStory) { + // console.log('innerStory', innerStory); return { // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked(event) { + renderTracked() { // console.log('innerStory renderTracked', event); }, - renderTriggered(event) { + renderTriggered() { // console.log('innerStory renderTriggered', event); }, }; @@ -41,13 +42,13 @@ function prepare( return { render() { - return h(story, this.$props); + return h(story); }, renderTracked(event) { - // console.log('story renderTracked', event); + console.log('story renderTracked', event); }, renderTriggered(event) { - // console.log('story renderTriggered', event); + console.log('story renderTriggered', event); }, }; } @@ -63,11 +64,11 @@ export function decorateStory( const decoratedStory: VueRenderer['storyResult'] = decorator((update) => { // we should update the context with the update object from the decorator in reactive way // so that the story will be re-rendered with the new context - updateReactiveContext(context, update); story = decorated({ ...context, ...sanitizeStoryContextUpdate(update), }); + if (update) updateReactiveContext(context, update); return story; }, context); @@ -79,7 +80,8 @@ export function decorateStory( return story; } - return prepare(decoratedStory, story) as VueRenderer['storyResult']; + const storyFuntion = () => h(story ?? 'story', context.args); + return prepare(decoratedStory, storyFuntion) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); @@ -91,16 +93,19 @@ function updateReactiveContext( | import('@storybook/csf').StoryContextUpdate> | undefined ) { + context.args = reactive(context.args); if (update) { const { args, argTypes } = update; if (args && !argTypes) { const deepCopy = JSON.parse(JSON.stringify(args)); - Object.keys(context.args).forEach((key) => { - delete context.args[key]; - }); + console.log(' updated Args ', deepCopy); + // Object.keys(context.args).forEach((key) => { + // delete context.args[key]; + // }); Object.keys(args).forEach((key) => { context.args[key] = deepCopy[key]; }); + console.log(' updated context.args ', context.args); } } } diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index c705fb16cdc1..d2977f8d6248 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -15,7 +15,7 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual(newArgs); }); - test('update reactive Args component new arg updateArgs()', () => { + test('update reactive Args component inherit objectArg updateArgs()', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo', argBar: 'bar' } }); // get reference to reactiveArgs or create a new one; expectTypeOf(reactiveArgs).toMatchTypeOf>(); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); @@ -23,7 +23,17 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); - expect(reactiveArgs).toEqual({ objectArg: newArgs }); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2', argBar: 'bar2' } }); + }); + + test('update reactive Args component inherit objectArg only argName argName()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string } }>(); + + const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; + updateArgs(reactiveArgs, newArgs, ['argFoo']); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); }); test('update reactive Args component 2 args updateArgs()', () => { @@ -39,13 +49,12 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); - expectTypeOf(reactiveArgs).toEqualTypeOf<{ - objectArg: { argFoo: string }; - objectArg2: { argBar: string }; - }>(); + expect(reactiveArgs).toEqual({ - objectArg: { argFoo: newArgs.argFoo }, - objectArg2: { argBar: newArgs.argBar }, + objectArg: { argFoo: 'foo2' }, + objectArg2: { argBar: 'bar2' }, + argFoo: 'foo2', + argBar: 'bar2', }); }); }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index f64d668da84c..c2b0fb7455cc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive } from 'vue'; +import { createApp, h, isReactive, reactive, shallowReactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Args, StoryContext } from '@storybook/csf'; -import type { VueRenderer } from './types'; +import type { Globals, Args, StoryContext } from '@storybook/csf'; +import type { StoryFnVueReturnType, VueRenderer } from './types'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -22,9 +22,12 @@ export const setup = (fn: (app: any) => void) => { const map = new Map< VueRenderer['canvasElement'], - { vueApp: ReturnType; reactiveArgs: any } + { + vueApp: ReturnType; + reactiveArgs: Args; + } >(); - +let reactiveState: { globals: Globals }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] @@ -32,8 +35,12 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; + if (!reactiveState) reactiveState = shallowReactive({ globals: storyContext.globals }); + + // updateArgs(reactiveState.globals, storyContext.globals); // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { + reactiveState.globals = storyContext.globals; updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -45,8 +52,24 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - const rootElement = storyFn(); - return () => h(rootElement, reactiveArgs); + + let rootElement: StoryFnVueReturnType; + watch( + reactiveState, + (newVal) => { + console.log('watching reactiveState ', reactiveState.globals); + rootElement = storyFn(storyContext); + // storyContext.globals = reactiveState.globals; + console.log('reactiveState newVaue ', newVal); + }, + { immediate: true } + ); + + return () => { + console.log('rerendering reactiveState', reactiveState); + console.log('rerendering storyContext.globals', storyContext.globals); + return h(rootElement, reactiveArgs); + }; }, mounted() { map.set(canvasElement, { @@ -92,15 +115,25 @@ function getSlots(props: Args, context: StoryContext) { * @param nextArgs * @returns */ -export function updateArgs(reactiveArgs: Args, nextArgs: Args) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; + if (typeof componentArg === 'object') { Object.keys(componentArg).forEach((key2) => { - componentArg[key2] = nextArgs[key2]; + if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { + console.log(`-----${key2}:${currentArgs[key][key2]} => ${nextArgs[key2]}`); + currentArgs[key][key2] = nextArgs[key2]; + } + }); + Object.keys(nextArgs).forEach((key2) => { + if (currentArgs[key][key2] === undefined) { + currentArgs[key2] = nextArgs[key2]; + } }); + // console.log('updateArgs', key, currentArgs[key]); } else { currentArgs[key] = nextArgs[key]; } From 284313a393974d8c947238a5bf13d965b34ec521 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 18:30:15 +0400 Subject: [PATCH 0046/1428] fixing react decorator breaks the reactivity --- code/renderers/vue3/src/render.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index c2b0fb7455cc..18182109bcce 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -27,7 +27,10 @@ const map = new Map< reactiveArgs: Args; } >(); -let reactiveState: { globals: Globals }; +let reactiveState: { + [x: string]: StoryFnVueReturnType; + globals: Globals; +}; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, canvasElement: VueRenderer['canvasElement'] @@ -35,12 +38,11 @@ export function renderToCanvas( const existingApp = map.get(canvasElement); const reactiveArgs = existingApp?.reactiveArgs ?? reactive(storyContext.args); // get reference to reactiveArgs or create a new one; - if (!reactiveState) reactiveState = shallowReactive({ globals: storyContext.globals }); - // updateArgs(reactiveState.globals, storyContext.globals); // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - reactiveState.globals = storyContext.globals; + if (reactiveState) reactiveState.globals = storyContext.globals; + updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -52,23 +54,18 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - - let rootElement: StoryFnVueReturnType; + const rootElement: StoryFnVueReturnType = storyFn(); + reactiveState = reactive({ globals: storyContext.globals, rootElement }); watch( - reactiveState, + () => reactiveState.globals, (newVal) => { - console.log('watching reactiveState ', reactiveState.globals); - rootElement = storyFn(storyContext); - // storyContext.globals = reactiveState.globals; - console.log('reactiveState newVaue ', newVal); - }, - { immediate: true } + reactiveState.rootElement = storyFn(); + // run decorator functions + } ); return () => { - console.log('rerendering reactiveState', reactiveState); - console.log('rerendering storyContext.globals', storyContext.globals); - return h(rootElement, reactiveArgs); + return h(reactiveState.rootElement, reactiveArgs); }; }, mounted() { From 4a7d98b2e34cc7b26de6c1456986003c1e84b589 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 19:40:23 +0400 Subject: [PATCH 0047/1428] use remount as easier way --- code/renderers/vue3/src/decorateStory.ts | 5 ++--- code/renderers/vue3/src/render.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 21cb19537ef0..cd8da1a91c6e 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -3,6 +3,7 @@ import { reactive, h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; +import type { Args, StoryContextUpdate } from '@storybook/csf'; import type { VueRenderer } from './types'; /* @@ -89,9 +90,7 @@ export function decorateStory( function updateReactiveContext( context: StoryContext, - update: - | import('@storybook/csf').StoryContextUpdate> - | undefined + update: StoryContextUpdate> | undefined ) { context.args = reactive(context.args); if (update) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 18182109bcce..6f4decaa8407 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,12 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive, shallowReactive, watch } from 'vue'; +import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, Args, StoryContext } from '@storybook/csf'; +import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; +const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; if (!Component) { @@ -32,7 +35,7 @@ let reactiveState: { globals: Globals; }; export function renderToCanvas( - { storyFn, forceRemount, showMain, showException, storyContext }: RenderContext, + { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { const existingApp = map.get(canvasElement); @@ -59,8 +62,9 @@ export function renderToCanvas( watch( () => reactiveState.globals, (newVal) => { - reactiveState.rootElement = storyFn(); + // reactiveState.rootElement = storyFn(); // run decorator functions + channel.emit('forceRemount', { storyId: id }); } ); From f681d835212db582513c3b8841096bcc1ecfaaee Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 19:49:22 +0400 Subject: [PATCH 0048/1428] emit using channel forceRemount --- code/renderers/vue3/src/render.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 6f4decaa8407..869ab5465f8d 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -5,8 +5,6 @@ import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; - export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; if (!Component) { @@ -64,6 +62,7 @@ export function renderToCanvas( (newVal) => { // reactiveState.rootElement = storyFn(); // run decorator functions + const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); From f670ef54d8a83ed1f81763e097bc435a77cb750b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 21:43:59 +0400 Subject: [PATCH 0049/1428] cleanup code --- code/renderers/vue3/src/render.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 869ab5465f8d..24b5fe9db033 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -29,7 +29,6 @@ const map = new Map< } >(); let reactiveState: { - [x: string]: StoryFnVueReturnType; globals: Globals; }; export function renderToCanvas( @@ -55,20 +54,19 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; + reactiveState = reactive({ globals: storyContext.globals }); const rootElement: StoryFnVueReturnType = storyFn(); - reactiveState = reactive({ globals: storyContext.globals, rootElement }); + watch( () => reactiveState.globals, (newVal) => { - // reactiveState.rootElement = storyFn(); - // run decorator functions const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); return () => { - return h(reactiveState.rootElement, reactiveArgs); + return h(rootElement, reactiveArgs); }; }, mounted() { @@ -77,12 +75,6 @@ export function renderToCanvas( reactiveArgs, }); }, - renderTracked(event) { - // console.log('vueApp--renderTracked ', event); - }, - renderTriggered(event) { - // console.log('vueApp--renderTriggered ', event); - }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); @@ -120,11 +112,10 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; - + // if the arg is an object, we need to update the object if (typeof componentArg === 'object') { Object.keys(componentArg).forEach((key2) => { if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { - console.log(`-----${key2}:${currentArgs[key][key2]} => ${nextArgs[key2]}`); currentArgs[key][key2] = nextArgs[key2]; } }); @@ -133,12 +124,18 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string currentArgs[key2] = nextArgs[key2]; } }); - // console.log('updateArgs', key, currentArgs[key]); } else { currentArgs[key] = nextArgs[key]; } }); } +/** + * unmount the vue app + * @param storybookApp + * @param canvasElement + * @returns void + * @private + * */ function teardown( storybookApp: ReturnType, From a5284cd0ccda06a1be560875d8d786eba0880144 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sat, 4 Mar 2023 22:42:23 +0400 Subject: [PATCH 0050/1428] fix args after tests + cleanup --- code/renderers/vue3/src/decorateStory.ts | 8 +++----- code/renderers/vue3/src/render.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index cd8da1a91c6e..6892fb1337bf 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -97,14 +97,12 @@ function updateReactiveContext( const { args, argTypes } = update; if (args && !argTypes) { const deepCopy = JSON.parse(JSON.stringify(args)); - console.log(' updated Args ', deepCopy); - // Object.keys(context.args).forEach((key) => { - // delete context.args[key]; - // }); + Object.keys(context.args).forEach((key) => { + delete context.args[key]; + }); Object.keys(args).forEach((key) => { context.args[key] = deepCopy[key]; }); - console.log(' updated context.args ', context.args); } } } diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 24b5fe9db033..55a9636e5dab 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -60,7 +60,7 @@ export function renderToCanvas( watch( () => reactiveState.globals, (newVal) => { - const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + const channel = (globalThis as any).__STORYBOOK_ADDONS_CHANNEL__; channel.emit('forceRemount', { storyId: id }); } ); @@ -119,14 +119,14 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string currentArgs[key][key2] = nextArgs[key2]; } }); - Object.keys(nextArgs).forEach((key2) => { - if (currentArgs[key][key2] === undefined) { - currentArgs[key2] = nextArgs[key2]; - } - }); } else { currentArgs[key] = nextArgs[key]; } + Object.keys(nextArgs).forEach((key2) => { + if (currentArgs[key][key2] === undefined) { + currentArgs[key2] = nextArgs[key2]; + } + }); }); } /** From 546d62633e7af442829836861e3fe268030594c9 Mon Sep 17 00:00:00 2001 From: Roel Tijerina Date: Sun, 5 Mar 2023 01:46:11 -0600 Subject: [PATCH 0051/1428] added remount shortcut key/desc --- code/lib/manager-api/src/index.tsx | 4 ++++ code/lib/manager-api/src/modules/shortcuts.ts | 6 ++++++ code/ui/manager/src/components/layout/app.mockdata.tsx | 1 + code/ui/manager/src/settings/shortcuts.stories.tsx | 1 + code/ui/manager/src/settings/shortcuts.tsx | 1 + 5 files changed, 13 insertions(+) diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index eebb9d765ff9..3c42ca8f323c 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -55,6 +55,7 @@ import * as settings from './modules/settings'; import * as releaseNotes from './modules/release-notes'; // eslint-disable-next-line import/no-cycle import * as stories from './modules/stories'; +import * as toolbar from './modules/toolbar'; import * as refs from './modules/refs'; import * as layout from './modules/layout'; @@ -91,6 +92,7 @@ export type State = layout.SubState & url.SubState & shortcuts.SubState & releaseNotes.SubState & + toolbar.SubState & settings.SubState & globals.SubState & RouterData & @@ -107,6 +109,7 @@ export type API = addons.SubAPI & notifications.SubAPI & shortcuts.SubAPI & releaseNotes.SubAPI & + toolbar.SubAPI & settings.SubAPI & version.SubAPI & url.SubAPI & @@ -198,6 +201,7 @@ class ManagerProvider extends Component { releaseNotes, shortcuts, stories, + toolbar, refs, globals, url, diff --git a/code/lib/manager-api/src/modules/shortcuts.ts b/code/lib/manager-api/src/modules/shortcuts.ts index 1e623123bde8..05a1a4671b7c 100644 --- a/code/lib/manager-api/src/modules/shortcuts.ts +++ b/code/lib/manager-api/src/modules/shortcuts.ts @@ -57,6 +57,7 @@ export interface API_Shortcuts { escape: API_KeyCollection; collapseAll: API_KeyCollection; expandAll: API_KeyCollection; + remount: API_KeyCollection; } export type API_Action = keyof API_Shortcuts; @@ -91,6 +92,7 @@ export const defaultShortcuts: API_Shortcuts = Object.freeze({ escape: ['escape'], // This one is not customizable collapseAll: [controlOrMetaKey(), 'shift', 'ArrowUp'], expandAll: [controlOrMetaKey(), 'shift', 'ArrowDown'], + remount: ['alt', 'R'], }); const addonsShortcuts: API_AddonShortcuts = {}; @@ -320,6 +322,10 @@ export const init: ModuleFn = ({ store, fullAPI }) => { fullAPI.expandAll(); break; } + case 'remount': { + fullAPI.remount(); + break; + } default: addonsShortcuts[feature].action(); break; diff --git a/code/ui/manager/src/components/layout/app.mockdata.tsx b/code/ui/manager/src/components/layout/app.mockdata.tsx index 6f5b2657aeee..fc87fd64d17e 100644 --- a/code/ui/manager/src/components/layout/app.mockdata.tsx +++ b/code/ui/manager/src/components/layout/app.mockdata.tsx @@ -34,6 +34,7 @@ export const shortcuts: State['shortcuts'] = { escape: ['escape'], collapseAll: ['ctrl', 'shift', 'ArrowUp'], expandAll: ['ctrl', 'shift', 'ArrowDown'], + remount: ['alt', 'R'], }; export const panels: Addon_Collection = { diff --git a/code/ui/manager/src/settings/shortcuts.stories.tsx b/code/ui/manager/src/settings/shortcuts.stories.tsx index cf8a2bce3088..e3260e43bf8e 100644 --- a/code/ui/manager/src/settings/shortcuts.stories.tsx +++ b/code/ui/manager/src/settings/shortcuts.stories.tsx @@ -23,6 +23,7 @@ const defaultShortcuts = { escape: ['escape'], // This one is not customizable collapseAll: ['ctrl', 'shift', 'ArrowUp'], expandAll: ['ctrl', 'shift', 'ArrowDown'], + remount: ['alt', 'R'], }; const actions = makeActions( diff --git a/code/ui/manager/src/settings/shortcuts.tsx b/code/ui/manager/src/settings/shortcuts.tsx index 5504c8495092..57de523eb8b7 100644 --- a/code/ui/manager/src/settings/shortcuts.tsx +++ b/code/ui/manager/src/settings/shortcuts.tsx @@ -124,6 +124,7 @@ const shortcutLabels = { aboutPage: 'Go to about page', collapseAll: 'Collapse all items on sidebar', expandAll: 'Expand all items on sidebar', + remount: 'Remount component', }; export type Feature = keyof typeof shortcutLabels; From 8027e295ccee01b8fba9b5c3576b9430aa4bc653 Mon Sep 17 00:00:00 2001 From: Roel Tijerina Date: Sun, 5 Mar 2023 01:46:51 -0600 Subject: [PATCH 0052/1428] added remount test, moved remount handler to api --- code/lib/manager-api/src/modules/toolbar.ts | 30 ++++++++++ .../lib/manager-api/src/tests/toolbar.test.js | 60 +++++++++++++++++++ .../src/components/preview/tools/remount.tsx | 23 +++---- 3 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 code/lib/manager-api/src/modules/toolbar.ts create mode 100644 code/lib/manager-api/src/tests/toolbar.test.js diff --git a/code/lib/manager-api/src/modules/toolbar.ts b/code/lib/manager-api/src/modules/toolbar.ts new file mode 100644 index 000000000000..0249193d200b --- /dev/null +++ b/code/lib/manager-api/src/modules/toolbar.ts @@ -0,0 +1,30 @@ +import { FORCE_REMOUNT } from '@storybook/core-events'; +import type { ModuleFn } from '../index'; + +export interface SubState { + remount: { isAnimating: boolean }; +} + +export interface SubAPI { + remount: () => void; + remountEnd: () => void; +} + +export const init: ModuleFn = ({ store, fullAPI }) => { + const api: SubAPI = { + remount: () => { + const { storyId, remount } = store.getState(); + if (!storyId) return; + store.setState({ remount: { ...remount, isAnimating: true } }); + fullAPI.emit(FORCE_REMOUNT, { storyId }); + }, + remountEnd: () => { + const { remount } = store.getState(); + store.setState({ remount: { ...remount, isAnimating: false } }); + }, + }; + + const state: SubState = { remount: { isAnimating: false } }; + + return { api, state }; +}; diff --git a/code/lib/manager-api/src/tests/toolbar.test.js b/code/lib/manager-api/src/tests/toolbar.test.js new file mode 100644 index 000000000000..4b1e1919a6c0 --- /dev/null +++ b/code/lib/manager-api/src/tests/toolbar.test.js @@ -0,0 +1,60 @@ +import { FORCE_REMOUNT } from '@storybook/core-events'; +import { init as initToolbar } from '../modules/toolbar'; + +describe('toolbar API', () => { + function getValidMockedStore() { + const store = { + getState: () => ({ + remount: { isAnimating: true }, + storyId: 'primary-btn-test', + }), + setState: jest.fn(), + }; + return store; + } + + function getInvalidMockedStore() { + const store = { + getState: () => ({ + remount: { isAnimating: false }, + storyId: null, + }), + setState: jest.fn(), + }; + return store; + } + + it('does not remount component if storyId is null', () => { + const store = getInvalidMockedStore(); + + const { api } = initToolbar({ store }); + + api.remount(); + expect(store.setState).not.toHaveBeenCalledWith(); + }); + + it('remounts component and starts animation', () => { + const store = getValidMockedStore(); + const { storyId } = store.getState(); + const mockFullApi = { emit: jest.fn() }; + + const { api } = initToolbar({ store, fullAPI: mockFullApi }); + + api.remount(); + expect(mockFullApi.emit).toHaveBeenCalledWith(FORCE_REMOUNT, { storyId }); + expect(store.setState).toHaveBeenCalledWith({ + remount: { isAnimating: true }, + }); + }); + + it('ends remount animation', () => { + const store = getValidMockedStore(); + + const { api } = initToolbar({ store }); + + api.remountEnd(); + expect(store.setState).toHaveBeenCalledWith({ + remount: { isAnimating: false }, + }); + }); +}); diff --git a/code/ui/manager/src/components/preview/tools/remount.tsx b/code/ui/manager/src/components/preview/tools/remount.tsx index af57f73bd45f..7883ffdb8abe 100644 --- a/code/ui/manager/src/components/preview/tools/remount.tsx +++ b/code/ui/manager/src/components/preview/tools/remount.tsx @@ -1,10 +1,9 @@ import type { ComponentProps } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import { IconButton, Icons } from '@storybook/components'; import { Consumer } from '@storybook/manager-api'; import type { Addon, Combo } from '@storybook/manager-api'; import { styled } from '@storybook/theming'; -import { FORCE_REMOUNT } from '@storybook/core-events'; interface AnimatedButtonProps { animating?: boolean; @@ -20,10 +19,13 @@ const StyledAnimatedIconButton = styled(IconButton)< })); const menuMapper = ({ api, state }: Combo) => { - const { storyId } = state; + const { storyId, remount } = state; + const { isAnimating } = remount; return { storyId, - remount: () => api.emit(FORCE_REMOUNT, { storyId: state.storyId }), + remount: () => api.remount(), + remountEnd: () => api.remountEnd(), + isAnimating, }; }; @@ -33,20 +35,13 @@ export const remountTool: Addon = { match: ({ viewMode }) => viewMode === 'story', render: () => ( - {({ remount, storyId }) => { - const [isAnimating, setIsAnimating] = useState(false); - const animateAndReplay = () => { - if (!storyId) return; - setIsAnimating(true); - remount(); - }; - + {({ remount, storyId, remountEnd, isAnimating }) => { return ( setIsAnimating(false)} + onClick={remount} + onAnimationEnd={remountEnd} animating={isAnimating} disabled={!storyId} > From fb1c507dcd9f65b240c526ab0807cb169b16e292 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 5 Mar 2023 21:54:03 +0400 Subject: [PATCH 0053/1428] attribute style dynamic source --- code/renderers/vue3/src/decorateStory.ts | 21 ++- .../vue3/src/docs/sourceDecorator.ts | 167 ++++++++++-------- code/renderers/vue3/src/render.test.ts | 34 +++- code/renderers/vue3/src/render.ts | 22 +-- code/renderers/vue3/src/types.ts | 11 +- 5 files changed, 158 insertions(+), 97 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 6892fb1337bf..17c3326abb90 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -27,16 +27,15 @@ function prepare( } if (innerStory) { - // console.log('innerStory', innerStory); return { // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked() { - // console.log('innerStory renderTracked', event); + renderTracked(event) { + console.log('innerStory renderTracked', event); // this works only in dev mode }, - renderTriggered() { - // console.log('innerStory renderTriggered', event); + renderTriggered(event) { + console.log('innerStory renderTriggered', event); }, }; } @@ -46,7 +45,7 @@ function prepare( return h(story); }, renderTracked(event) { - console.log('story renderTracked', event); + console.log('story renderTracked', event); // this works only in dev mode }, renderTriggered(event) { console.log('story renderTriggered', event); @@ -87,16 +86,20 @@ export function decorateStory( (context) => prepare(storyFn(context)) as LegacyStoryFn ); } - +/** + * update the context with the update object from the decorator in reactive way + * @param context + * @param update + */ function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { - context.args = reactive(context.args); + context.args = reactive(context.args); // get reference to reactiveArgs or create a new one; in case was destructured by decorator if (update) { const { args, argTypes } = update; if (args && !argTypes) { - const deepCopy = JSON.parse(JSON.stringify(args)); + const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args Object.keys(context.args).forEach((key) => { delete context.args[key]; }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index fe91412f8818..97c7a71f975a 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,22 +14,11 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import type { Component, VNodeProps } from 'vue'; -import { toDisplayString, h } from 'vue'; +import { h, toDisplayString } from 'vue'; import { camelCase, kebabCase } from 'lodash'; -type StoryVueComponent = Component & { - render: any; - props: VNodeProps; - slots: any; - tag?: string; - name?: string; - __name?: string; - __file?: string; - __docs?: any; - __docsGen?: any; - __docsExtracted?: any; -}; +import type { VueStoryComponent } from '../types'; + /** * Check if the sourcecode should be generated. * @@ -49,15 +38,16 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; +const omitEvent = (args: Args): Args => + Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + const displayObject = (obj: Args) => { - const a = Object.keys(obj).map((key) => `${key}:"${obj[key]}"`); + const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); return `{${a.join(',')}}`; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; }; -// html event attribute to vue event attribute -// is html event attribute const directiveSource = (key: string, value: unknown) => key.includes('on') @@ -69,6 +59,7 @@ const attributeSource = (key: string, value: unknown) => ['boolean', 'number', 'object'].includes(typeof value) ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` : directiveSource(key, value); + /** * * @param _args @@ -86,21 +77,76 @@ export function generateAttributesSource( const arg = tempArgs[key]; if (arg.type === 7) { - const { arg: argName } = arg; - const argKey = argName ? argName?.loc.source : undefined; // (argName as any)?.content; - // const argExpValue = exp?.content; + // AttributeNode binding type + const { exp, arg: argName } = arg; + const argKey = argName ? argName?.loc.source : undefined; + const argExpValue = exp?.loc.source ?? (exp as any).content; const propValue = args[camelCase(argKey)]; + const argValue = argKey ? propValue ?? argExpValue : displayObject(omitEvent(args)); + + if (argKey === 'style') { + let style = argValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + style = style.replace(regex, args[akey]); + }); + return `:style="${style}"`; + } - const argValue = argKey ? propValue : toDisplayString(args); return argKey ? attributeSource(argKey, argValue) - : toDisplayString(tempArgs[key].loc.source); // tempArgs[key].loc.source.replace(`"${argExpValue}"`, `'${argValue}'`); + : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `"${argValue}"`) ?? + toDisplayString(argExpValue); } + return tempArgs[key].loc.source; }) .join(' '); } +/** + * map attributes and directives + * @param props + */ +function mapAttributesAndDirectives(props: Args) { + const tranformKey = (key: string) => (key.startsWith('on') ? key : kebabCase(key)); + return Object.keys(props).map( + (key) => + ({ + name: 'bind', + type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive + arg: { content: tranformKey(key), loc: { source: tranformKey(key) } }, // attribute name or directive name (v-bind, v-on, v-model) + loc: { source: attributeSource(tranformKey(key), props[key]) }, // attribute value or directive value + exp: { isStatic: false, loc: { source: props[key] } }, // directive expression + modifiers: [''], + } as unknown as AttributeNode) + ); +} +/** + * map slots + * @param slotsProps + */ +function mapSlots(slotsProps: Args): TextNode[] { + return Object.keys(slotsProps).map((key) => { + const slot = slotsProps[key]; + let slotContent = ''; + if (typeof slot === 'function') slotContent = ``; + slotContent = ``; + if (key === 'default') { + slotContent = JSON.stringify(slot); + } + return { + type: 2, + content: slotContent, + loc: { + source: slotContent, + start: { offset: 0, line: 1, column: 0 }, + end: { offset: 0, line: 1, column: 0 }, + }, + }; + }); + // TODO: handle other cases (array, object, html,etc) +} /** * * @param args generate script setup from args @@ -135,14 +181,10 @@ function getComponentsFromRenderFn( } function getComponentsFromTemplate(template: string): TemplateChildNode[] { - try { - const ast = baseParse(template); - const components = ast?.children; - if (!components) return []; - return components; - } catch (e) { - return []; - } + const ast = baseParse(template); + const components = ast?.children; + if (!components) return []; + return components; } /** @@ -155,7 +197,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { */ export function generateSource( - componentOrNodes: (StoryVueComponent | TemplateChildNode)[] | TemplateChildNode, + componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, byRef = false @@ -165,14 +207,17 @@ export function generateSource( const isInterpolationNode = (node: any) => node && node.type === 5; const isTextNode = (node: any) => node && node.type === 2; - const generateComponentSource = (componentOrNode: StoryVueComponent | TemplateChildNode) => { + const generateComponentSource = (componentOrNode: VueStoryComponent | TemplateChildNode) => { if (isElementNode(componentOrNode)) { const { tag: name, props: attributes, children } = componentOrNode as ElementNode; const childSources: string = children .map((child: TemplateChildNode) => generateComponentSource(child)) .join(''); const props = generateAttributesSource(attributes, args, argTypes, byRef); - return `<${name} ${props}>${childSources}`; + + return childSources === '' + ? `<${name} ${props} />` + : `<${name} ${props}>${childSources}`; } if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { @@ -183,7 +228,7 @@ export function generateSource( } if (isComponent(componentOrNode)) { - const concreteComponent = componentOrNode as StoryVueComponent; + const concreteComponent = componentOrNode as VueStoryComponent; const vnode = h(componentOrNode, args); const { props } = vnode; const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; @@ -203,7 +248,9 @@ export function generateSource( .join(''); const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); - return `<${name} ${propsSource}>${childSources}`; + return childSources.trim() === '' + ? `<${name} ${propsSource}/>` + : `<${name} ${propsSource}>${childSources}`; } return null; @@ -216,20 +263,6 @@ export function generateSource( return source || null; } -function mapAttributesAndDirectives(props: Args) { - const tranformKey = (key: string) => (key.startsWith('on') ? key : kebabCase(key)); - return Object.keys(props).map( - (key) => - ({ - name: 'bind', - type: ['v-', '@', 'v-on'].includes(key) ? 7 : 6, // 6 is attribute, 7 is directive - arg: { content: tranformKey(key), loc: { source: tranformKey(key) } }, // attribute name or directive name (v-bind, v-on, v-model) - loc: { source: attributeSource(tranformKey(key), props[key]) }, // attribute value or directive value - exp: { isStatic: false, loc: { source: props[key] } }, // directive expression - modifiers: [''], - } as unknown as AttributeNode) - ); -} /** * source decorator. * @param storyFn Fn @@ -239,9 +272,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const channel = addons.getChannel(); const skip = skipSourceRender(context); const story = storyFn(); - let source: string; - useEffect(() => { if (!skip && source) { const { id, args } = context; @@ -270,29 +301,19 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = return story; }; -function mapSlots(slotsProps: Args): TextNode[] { - return Object.keys(slotsProps).map((key) => { - const slot = slotsProps[key]; - let slotContent = ''; - if (typeof slot === 'function') slotContent = ``; - slotContent = ``; - if (key === 'default') { - slotContent = JSON.stringify(slot); - } - - return { - type: 2, - content: slotContent, - loc: { - source: slotContent, - start: { offset: 0, line: 1, column: 0 }, - end: { offset: 0, line: 1, column: 0 }, - }, - }; - }); - // TODO: handle other cases (array, object, html,etc) +export function getTemplateSource(context: StoryContext) { + const channel = addons.getChannel(); + const components = getComponentsFromRenderFn(context?.originalStoryFn, context); + const storyComponent = components.length ? components : (context.component as TemplateChildNode); + const generatedTemplate = generateSource(storyComponent, context.args, context.argTypes); + if (generatedTemplate) { + const source = ``; + const { id, args } = context; + channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); + return source; + } + return null; } - // export local function for testing purpose export { generateScriptSetup, diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index d2977f8d6248..5c39ef32a6b2 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -12,7 +12,7 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ argFoo: string; argBar: string }>(); - expect(reactiveArgs).toEqual(newArgs); + expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component inherit objectArg updateArgs()', () => { @@ -36,7 +36,7 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); }); - test('update reactive Args component 2 args updateArgs()', () => { + test('update reactive Args component 2 object args -> updateArgs()', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' }, objectArg2: { argBar: 'bar' }, @@ -53,8 +53,34 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, objectArg2: { argBar: 'bar2' }, - argFoo: 'foo2', - argBar: 'bar2', }); }); + + test('update reactive Args component object with object -> updateArgs()', () => { + const reactiveArgs = reactive({ + objectArg: { argFoo: 'foo' }, + }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + }>(); + + const newArgs = { objectArg: { argFoo: 'bar' } }; + updateArgs(reactiveArgs, newArgs); + + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'bar' } }); + }); + + test('update reactive Args component no arg with all args -> updateArgs()', () => { + const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; + expectTypeOf(reactiveArgs).toMatchTypeOf>(); + expectTypeOf(reactiveArgs).toEqualTypeOf<{ + objectArg: { argFoo: string }; + }>(); + + const newArgs = { objectArg: { argFoo: 'bar' } }; + updateArgs(reactiveArgs, newArgs); + + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'bar' } }); + }); }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 55a9636e5dab..5877135a98bb 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,10 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext } from '@storybook/csf'; +import type { Globals, Args, StoryContext, Renderer } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; +import { getTemplateSource as generateTemplateSource } from './docs/sourceDecorator'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -44,6 +45,7 @@ export function renderToCanvas( if (reactiveState) reactiveState.globals = storyContext.globals; updateArgs(existingApp.reactiveArgs, storyContext.args); + generateTemplateSource(storyContext as StoryContext); return () => { teardown(existingApp.vueApp, canvasElement); }; @@ -109,26 +111,26 @@ function getSlots(props: Args, context: StoryContext) { */ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - + const notMappedArgs = { ...nextArgs }; Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; // if the arg is an object, we need to update the object if (typeof componentArg === 'object') { - Object.keys(componentArg).forEach((key2) => { - if (nextArgs[key2] && (argNames?.includes(key2) || !argNames)) { - currentArgs[key][key2] = nextArgs[key2]; + Object.keys(componentArg).forEach((aKey) => { + if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { + currentArgs[key][aKey] = nextArgs[aKey]; + delete notMappedArgs[aKey]; } }); } else { currentArgs[key] = nextArgs[key]; } - Object.keys(nextArgs).forEach((key2) => { - if (currentArgs[key][key2] === undefined) { - currentArgs[key2] = nextArgs[key2]; - } - }); + }); + Object.keys(notMappedArgs).forEach((key) => { + currentArgs[key] = notMappedArgs[key]; }); } + /** * unmount the vue app * @param storybookApp diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index 15809f9094e2..f5b4576cedf7 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -1,5 +1,5 @@ import type { StoryContext as StoryContextBase, WebRenderer } from '@storybook/types'; -import type { ConcreteComponent } from 'vue'; +import type { ConcreteComponent, Slots, VNodeProps } from 'vue'; export type { RenderContext } from '@storybook/types'; @@ -12,6 +12,15 @@ export type StoryFnVueReturnType = ConcreteComponent; export type StoryContext = StoryContextBase; +export type VueStoryComponent = ConcreteComponent & { + render: (h: any) => any; + props: VNodeProps; + slots: Slots; + tag?: string; + name?: string; + __name?: string; +}; + /** * @deprecated Use `VueRenderer` instead. */ From c0ffbe0aea5cb63f31eb5d053c0ee86ce164d272 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Sun, 5 Mar 2023 23:54:13 +0400 Subject: [PATCH 0054/1428] string double quote isse --- .../vue3/src/docs/sourceDecorator.test.ts | 232 +++++++++--------- .../vue3/src/docs/sourceDecorator.ts | 2 +- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 78616e4d06a2..563dd5258056 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -272,128 +272,128 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); }); - test('camelCase boolean, string, and number Args', () => { - expect( - generateAttributesSource( - mapAttributesAndDirectives({ - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }), - { - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - cameCaseNumberArg: 2023, - }, - [] as ArgsType - ) - ).toMatchInlineSnapshot( - `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` - ); - }); -}); + // test('camelCase boolean, string, and number Args', () => { + // expect( + // generateAttributesSource( + // mapAttributesAndDirectives({ + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // cameCaseNumberArg: 2023, + // }), + // { + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // cameCaseNumberArg: 2023, + // }, + // [] as ArgsType + // ) + // ).toMatchInlineSnapshot( + // `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` + // ); + // }); + // }); -describe('Vue3: generateSource() snippet', () => { - test('template component camelCase string Arg', () => { - expect( - generateForArgs( - { - camelCaseStringArg: 'foo', - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // describe('Vue3: generateSource() snippet', () => { + // test('template component camelCase string Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseStringArg: 'foo', + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase bool Arg', () => { - expect( - generateForArgs( - { - camelCaseBooleanArg: true, - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // test('template component camelCase bool Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseBooleanArg: true, + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase bool, string Arg', () => { - expect( - generateForArgs( - { - camelCaseBooleanArg: true, - camelCaseStringArg: 'foo', - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot( - `` - ); - }); + // test('template component camelCase bool, string Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseBooleanArg: true, + // camelCaseStringArg: 'foo', + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot( + // `` + // ); + // }); - test('template component camelCase object Arg', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - }, - [] as ArgsType, - `` - ) - ).toMatchInlineSnapshot(``); - }); + // test('template component camelCase object Arg', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // }, + // [] as ArgsType, + // `` + // ) + // ).toMatchInlineSnapshot(``); + // }); - test('template component camelCase object Arg and Slot', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - }, - [] as ArgsType, - ` SLOT ` - ) - ).toMatchInlineSnapshot(` SLOT `); - }); + // test('template component camelCase object Arg and Slot', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // }, + // [] as ArgsType, + // ` SLOT ` + // ) + // ).toMatchInlineSnapshot(` SLOT `); + // }); - test('template component camelCase object Arg and dynamic Slot content', () => { - expect( - generateForArgs( - { - camelCaseObjectArg: { foo: 'bar' }, - camelCaseStringSlotArg: 'foo', - }, - [] as ArgsType, - ` SLOT {{args.camelCaseStringSlotArg}}` - ) - ).toMatchInlineSnapshot( - ` SLOT foo` - ); - }); -}); + // test('template component camelCase object Arg and dynamic Slot content', () => { + // expect( + // generateForArgs( + // { + // camelCaseObjectArg: { foo: 'bar' }, + // camelCaseStringSlotArg: 'foo', + // }, + // [] as ArgsType, + // ` SLOT {{args.camelCaseStringSlotArg}}` + // ) + // ).toMatchInlineSnapshot( + // ` SLOT foo` + // ); + // }); + // }); -describe('Vue3: sourceDecorator->attributeSoure()', () => { - test('camelCase boolean Arg', () => { - expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); - }); + // describe('Vue3: sourceDecorator->attributeSoure()', () => { + // test('camelCase boolean Arg', () => { + // expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); + // }); - test('html event attribute should convert to vue event directive', () => { - expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - }); - test('normal html attribute should not convert to vue event directive', () => { - expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); - }); - test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { - const htmlEventAttributeToVueEventAttribute = (attribute: string) => { - return htmlEventToVueEvent(attribute); - }; - expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); - expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); - expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); - expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); - expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); - expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); - }); + // test('html event attribute should convert to vue event directive', () => { + // expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + // expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + // }); + // test('normal html attribute should not convert to vue event directive', () => { + // expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + // }); + // test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { + // const htmlEventAttributeToVueEventAttribute = (attribute: string) => { + // return htmlEventToVueEvent(attribute); + // }; + // expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); + // expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); + // expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); + // expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); + // expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); + // expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); + // }); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 97c7a71f975a..a963a46389e9 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -95,7 +95,7 @@ export function generateAttributesSource( return argKey ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`"${argExpValue}"`, `"${argValue}"`) ?? + : tempArgs[key].loc.source.replace(`${argExpValue}`, `${argValue}`) ?? toDisplayString(argExpValue); } From 7183154bd59d34e892d9b05131bae8e3a6d2d195 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 13:38:18 +0400 Subject: [PATCH 0055/1428] v-bind , style display as string --- .../vue3/src/docs/sourceDecorator.test.ts | 252 +++++++++--------- .../vue3/src/docs/sourceDecorator.ts | 44 +-- 2 files changed, 149 insertions(+), 147 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 563dd5258056..75e3b1e96745 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -49,7 +49,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :camel-case-boolean-arg='true', + source: :camel-case-boolean-arg="true", }, modifiers: Array [ , @@ -77,7 +77,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: camel-case-string-arg='foo', + source: camel-case-string-arg="foo", }, modifiers: Array [ , @@ -105,7 +105,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :booleanarg='true', + source: :booleanarg="true", }, modifiers: Array [ , @@ -133,7 +133,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: stringarg='bar', + source: stringarg="bar", }, modifiers: Array [ , @@ -161,7 +161,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :numberarg='2023', + source: :numberarg="2023", }, modifiers: Array [ , @@ -195,7 +195,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :camel-case-boolean-arg='true', + source: :camel-case-boolean-arg="true", }, modifiers: Array [ , @@ -217,7 +217,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: camel-case-string-arg='foo', + source: camel-case-string-arg="foo", }, modifiers: Array [ , @@ -239,7 +239,7 @@ describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { }, }, loc: Object { - source: :came-case-number-arg='2023', + source: :came-case-number-arg="2023", }, modifiers: Array [ , @@ -260,7 +260,7 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { { camelCaseBooleanArg: true }, [{ camelCaseBooleanArg: { type: 'boolean' } }] as ArgsType ) - ).toMatchInlineSnapshot(`:camel-case-boolean-arg='true'`); + ).toMatchInlineSnapshot(`:camel-case-boolean-arg="true"`); }); test('camelCase string Arg', () => { expect( @@ -269,131 +269,131 @@ describe('Vue3: sourceDecorator->generateAttributesSource()', () => { { camelCaseStringArg: 'foo' }, [{ camelCaseStringArg: { type: 'string' } }] as ArgsType ) - ).toMatchInlineSnapshot(`camel-case-string-arg='foo'`); + ).toMatchInlineSnapshot(`camel-case-string-arg="foo"`); }); - // test('camelCase boolean, string, and number Args', () => { - // expect( - // generateAttributesSource( - // mapAttributesAndDirectives({ - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // cameCaseNumberArg: 2023, - // }), - // { - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // cameCaseNumberArg: 2023, - // }, - // [] as ArgsType - // ) - // ).toMatchInlineSnapshot( - // `:camel-case-boolean-arg='true' camel-case-string-arg='foo' :came-case-number-arg='2023'` - // ); - // }); - // }); + test('camelCase boolean, string, and number Args', () => { + expect( + generateAttributesSource( + mapAttributesAndDirectives({ + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }), + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + cameCaseNumberArg: 2023, + }, + [] as ArgsType + ) + ).toMatchInlineSnapshot( + `:camel-case-boolean-arg="true" camel-case-string-arg="foo" :came-case-number-arg="2023"` + ); + }); +}); - // describe('Vue3: generateSource() snippet', () => { - // test('template component camelCase string Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseStringArg: 'foo', - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); +describe('Vue3: generateSource() snippet', () => { + test('template component camelCase string Arg', () => { + expect( + generateForArgs( + { + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase bool Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseBooleanArg: true, - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); + test('template component camelCase bool Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase bool, string Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseBooleanArg: true, - // camelCaseStringArg: 'foo', - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot( - // `` - // ); - // }); + test('template component camelCase bool, string Arg', () => { + expect( + generateForArgs( + { + camelCaseBooleanArg: true, + camelCaseStringArg: 'foo', + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot( + `` + ); + }); - // test('template component camelCase object Arg', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // }, - // [] as ArgsType, - // `` - // ) - // ).toMatchInlineSnapshot(``); - // }); + test('template component camelCase object Arg', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + `` + ) + ).toMatchInlineSnapshot(``); + }); - // test('template component camelCase object Arg and Slot', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // }, - // [] as ArgsType, - // ` SLOT ` - // ) - // ).toMatchInlineSnapshot(` SLOT `); - // }); + test('template component camelCase object Arg and Slot', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + }, + [] as ArgsType, + ` SLOT ` + ) + ).toMatchInlineSnapshot(` SLOT `); + }); - // test('template component camelCase object Arg and dynamic Slot content', () => { - // expect( - // generateForArgs( - // { - // camelCaseObjectArg: { foo: 'bar' }, - // camelCaseStringSlotArg: 'foo', - // }, - // [] as ArgsType, - // ` SLOT {{args.camelCaseStringSlotArg}}` - // ) - // ).toMatchInlineSnapshot( - // ` SLOT foo` - // ); - // }); - // }); + test('template component camelCase object Arg and dynamic Slot content', () => { + expect( + generateForArgs( + { + camelCaseObjectArg: { foo: 'bar' }, + camelCaseStringSlotArg: 'foo', + }, + [] as ArgsType, + ` SLOT {{args.camelCaseStringSlotArg}}` + ) + ).toMatchInlineSnapshot( + ` SLOT foo` + ); + }); +}); - // describe('Vue3: sourceDecorator->attributeSoure()', () => { - // test('camelCase boolean Arg', () => { - // expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg='foo'`); - // }); +describe('Vue3: sourceDecorator->attributeSoure()', () => { + test('camelCase boolean Arg', () => { + expect(attributeSource('stringArg', 'foo')).toMatchInlineSnapshot(`stringArg="foo"`); + }); - // test('html event attribute should convert to vue event directive', () => { - // expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - // expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); - // }); - // test('normal html attribute should not convert to vue event directive', () => { - // expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); - // }); - // test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { - // const htmlEventAttributeToVueEventAttribute = (attribute: string) => { - // return htmlEventToVueEvent(attribute); - // }; - // expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); - // expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); - // expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); - // expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); - // expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); - // expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); - // }); + test('html event attribute should convert to vue event directive', () => { + expect(attributeSource('onClick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + expect(attributeSource('onclick', () => {})).toMatchInlineSnapshot(`v-on:click='()=>({})'`); + }); + test('normal html attribute should not convert to vue event directive', () => { + expect(attributeSource('on-click', () => {})).toMatchInlineSnapshot(`on-click='()=>({})'`); + }); + test('htmlEventAttributeToVueEventAttribute onEv => v-on:', () => { + const htmlEventAttributeToVueEventAttribute = (attribute: string) => { + return htmlEventToVueEvent(attribute); + }; + expect(/^on[A-Za-z]/.test('onClick')).toBeTruthy(); + expect(htmlEventAttributeToVueEventAttribute('onclick')).toMatchInlineSnapshot(`v-on:click`); + expect(htmlEventAttributeToVueEventAttribute('onClick')).toMatchInlineSnapshot(`v-on:click`); + expect(htmlEventAttributeToVueEventAttribute('onChange')).toMatchInlineSnapshot(`v-on:change`); + expect(htmlEventAttributeToVueEventAttribute('onFocus')).toMatchInlineSnapshot(`v-on:focus`); + expect(htmlEventAttributeToVueEventAttribute('on-focus')).toMatchInlineSnapshot(`on-focus`); + }); }); diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a963a46389e9..86b5783bc945 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -41,9 +41,12 @@ const skipSourceRender = (context: StoryContext) => { const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); -const displayObject = (obj: Args) => { - const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); - return `{${a.join(',')}}`; +const displayObject = (obj: any) => { + if (typeof obj === 'object' && obj !== null && (obj as { [key: string]: unknown })) { + const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); + return `{${a.join(',')}}`; + } + return obj; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; @@ -52,12 +55,12 @@ const htmlEventAttributeToVueEventAttribute = (key: string) => { const directiveSource = (key: string, value: unknown) => key.includes('on') ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` - : `${key}='${value}'`; + : `${key}="${value}"`; const attributeSource = (key: string, value: unknown) => // convert html event key to vue event key ['boolean', 'number', 'object'].includes(typeof value) - ? `:${key}='${value && typeof value === 'object' ? displayObject(value) : value}'` + ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); /** @@ -82,21 +85,11 @@ export function generateAttributesSource( const argKey = argName ? argName?.loc.source : undefined; const argExpValue = exp?.loc.source ?? (exp as any).content; const propValue = args[camelCase(argKey)]; - const argValue = argKey ? propValue ?? argExpValue : displayObject(omitEvent(args)); - - if (argKey === 'style') { - let style = argValue; - Object.keys(args).forEach((akey) => { - const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - style = style.replace(regex, args[akey]); - }); - return `:style="${style}"`; - } - - return argKey - ? attributeSource(argKey, argValue) - : tempArgs[key].loc.source.replace(`${argExpValue}`, `${argValue}`) ?? - toDisplayString(argExpValue); + const argValue = argKey + ? propValue ?? evalExp(argExpValue, args) + : displayObject(omitEvent(args)); + + return argKey ? attributeSource(argKey, argValue) : `v-bind="${argValue}"`; } return tempArgs[key].loc.source; @@ -293,7 +286,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - + console.log('generatedTemplate -------\r\n\n\n', generatedTemplate, '\n\n'); if (generatedTemplate) { source = `${generatedScript}\n `; } @@ -323,3 +316,12 @@ export { attributeSource, htmlEventAttributeToVueEventAttribute, }; + +function evalExp(argExpValue: any, args: Args): any { + let evalVal = argExpValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + evalVal = evalVal.replace(regex, args[akey]); + }); + return evalVal; +} From db908d3a86287c877c5240306c4a907800a131a4 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 13:52:48 +0400 Subject: [PATCH 0056/1428] style should be dynamic in source --- code/renderers/vue3/src/docs/sourceDecorator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 86b5783bc945..a77e59d9f466 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -57,9 +57,9 @@ const directiveSource = (key: string, value: unknown) => ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` : `${key}="${value}"`; -const attributeSource = (key: string, value: unknown) => +const attributeSource = (key: string, value: unknown, dynamic?: boolean) => // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) + ['boolean', 'number', 'object'].includes(typeof value) || dynamic ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); @@ -89,7 +89,7 @@ export function generateAttributesSource( ? propValue ?? evalExp(argExpValue, args) : displayObject(omitEvent(args)); - return argKey ? attributeSource(argKey, argValue) : `v-bind="${argValue}"`; + return argKey ? attributeSource(argKey, argValue, true) : `v-bind="${argValue}"`; } return tempArgs[key].loc.source; @@ -286,7 +286,7 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - console.log('generatedTemplate -------\r\n\n\n', generatedTemplate, '\n\n'); + if (generatedTemplate) { source = `${generatedScript}\n `; } From 6d4d3aa46a4c21a18e924b0235abdec5fa41478a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 14:53:41 +0400 Subject: [PATCH 0057/1428] cleanup after testing. --- code/renderers/vue3/src/docs/sourceDecorator.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a77e59d9f466..e85ec53b1d98 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -14,9 +14,8 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h, toDisplayString } from 'vue'; +import { h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; - import type { VueStoryComponent } from '../types'; /** @@ -41,11 +40,13 @@ const skipSourceRender = (context: StoryContext) => { const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); -const displayObject = (obj: any) => { - if (typeof obj === 'object' && obj !== null && (obj as { [key: string]: unknown })) { - const a = Object.keys(obj).map((key) => `${key}:'${obj[key]}'`); - return `{${a.join(',')}}`; +const displayObject = (obj: any): string => { + if (typeof obj === 'object') { + return `{${Object.keys(obj) + .map((key) => `${key}:${displayObject(obj[key])}`) + .join(',')}}`; } + if (typeof obj === 'string') return `'${obj}'`; return obj; }; const htmlEventAttributeToVueEventAttribute = (key: string) => { @@ -59,7 +60,8 @@ const directiveSource = (key: string, value: unknown) => const attributeSource = (key: string, value: unknown, dynamic?: boolean) => // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) || dynamic + ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value + (dynamic && ['style', 'class'].includes(key)) // dynamic style or class ? `:${key}="${displayObject(value)}"` : directiveSource(key, value); From 13699fe6a77e724c1bed71e91631afe12e3d8b89 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 15:16:36 +0400 Subject: [PATCH 0058/1428] refactor add utils to docs --- .../vue3/src/docs/sourceDecorator.ts | 44 +++---------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e85ec53b1d98..317e098345d5 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -16,6 +16,13 @@ import type { import { baseParse } from '@vue/compiler-core'; import { h } from 'vue'; import { camelCase, kebabCase } from 'lodash'; +import { + attributeSource, + htmlEventAttributeToVueEventAttribute, + omitEvent, + displayObject, + evalExp, +} from './utils'; import type { VueStoryComponent } from '../types'; /** @@ -37,34 +44,6 @@ const skipSourceRender = (context: StoryContext) => { return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; -const omitEvent = (args: Args): Args => - Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); - -const displayObject = (obj: any): string => { - if (typeof obj === 'object') { - return `{${Object.keys(obj) - .map((key) => `${key}:${displayObject(obj[key])}`) - .join(',')}}`; - } - if (typeof obj === 'string') return `'${obj}'`; - return obj; -}; -const htmlEventAttributeToVueEventAttribute = (key: string) => { - return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; -}; - -const directiveSource = (key: string, value: unknown) => - key.includes('on') - ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` - : `${key}="${value}"`; - -const attributeSource = (key: string, value: unknown, dynamic?: boolean) => - // convert html event key to vue event key - ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value - (dynamic && ['style', 'class'].includes(key)) // dynamic style or class - ? `:${key}="${displayObject(value)}"` - : directiveSource(key, value); - /** * * @param _args @@ -318,12 +297,3 @@ export { attributeSource, htmlEventAttributeToVueEventAttribute, }; - -function evalExp(argExpValue: any, args: Args): any { - let evalVal = argExpValue; - Object.keys(args).forEach((akey) => { - const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - evalVal = evalVal.replace(regex, args[akey]); - }); - return evalVal; -} From 311a69e3e2f3b15cf80bf1dd0d5a3ba91e34f97c Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 6 Mar 2023 15:18:13 +0400 Subject: [PATCH 0059/1428] add utils --- code/renderers/vue3/src/docs/utils.ts | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 code/renderers/vue3/src/docs/utils.ts diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts new file mode 100644 index 000000000000..05d72791e015 --- /dev/null +++ b/code/renderers/vue3/src/docs/utils.ts @@ -0,0 +1,47 @@ +import type { Args } from '@storybook/types'; + +const omitEvent = (args: Args): Args => + Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + +const displayObject = (obj: any): string | boolean | number => { + if (typeof obj === 'object') { + return `{${Object.keys(obj) + .map((key) => `${key}:${displayObject(obj[key])}`) + .join(',')}}`; + } + if (typeof obj === 'string') return `'${obj}'`; + return obj; +}; +const htmlEventAttributeToVueEventAttribute = (key: string) => { + return /^on[A-Za-z]/.test(key) ? key.replace(/^on/, 'v-on:').toLowerCase() : key; +}; + +const directiveSource = (key: string, value: unknown) => + key.includes('on') + ? `${htmlEventAttributeToVueEventAttribute(key)}='()=>({})'` + : `${key}="${value}"`; + +const attributeSource = (key: string, value: unknown, dynamic?: boolean) => + // convert html event key to vue event key + ['boolean', 'number', 'object'].includes(typeof value) || // dynamic value + (dynamic && ['style', 'class'].includes(key)) // dynamic style or class + ? `:${key}="${displayObject(value)}"` + : directiveSource(key, value); + +const evalExp = (argExpValue: any, args: Args): any => { + let evalVal = argExpValue; + Object.keys(args).forEach((akey) => { + const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); + evalVal = evalVal.replace(regex, args[akey]); + }); + return evalVal; +}; + +export { + omitEvent, + displayObject, + htmlEventAttributeToVueEventAttribute, + directiveSource, + attributeSource, + evalExp, +}; From d38c8ac7b9e30c8fffb408553038481462c5d0f7 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 7 Mar 2023 05:09:41 +0400 Subject: [PATCH 0060/1428] use vue watch() instead of useEffect() --- .../vue3/src/docs/sourceDecorator.test.ts | 4 +- .../vue3/src/docs/sourceDecorator.ts | 68 +++++++------------ code/renderers/vue3/src/render.ts | 5 +- 3 files changed, 29 insertions(+), 48 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 75e3b1e96745..5a239de38120 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -3,7 +3,7 @@ import type { Args } from '@storybook/types'; import type { ArgsType } from 'jest-mock'; import { - generateSource, + generateTemplateSource, getComponentsFromTemplate, mapAttributesAndDirectives, generateAttributesSource, @@ -28,7 +28,7 @@ function generateForArgs( template = '' ) { const components = getComponentsFromTemplate(template); - return generateSource(components, args, generateArgTypes(args, slotProps), true); + return generateTemplateSource(components, args, generateArgTypes(args, slotProps), true); } describe('Vue3: sourceDecorator->mapAttributesAndDirective()', () => { diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 317e098345d5..e0689ad886aa 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-underscore-dangle */ -import { addons, useEffect } from '@storybook/preview-api'; +import { addons } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; @@ -14,7 +14,7 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h } from 'vue'; +import { h, watch } from 'vue'; import { camelCase, kebabCase } from 'lodash'; import { attributeSource, @@ -138,23 +138,23 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): return ``; } /** - * get component templates one or more + * get template components one or more * @param renderFn */ -function getComponentsFromRenderFn( +function getTemplateComponents( renderFn: any, context?: StoryContext ): TemplateChildNode[] { try { const { template } = context ? renderFn(context.args, context) : renderFn(); if (!template) return []; - return getComponentsFromTemplate(template); + return getComponents(template); } catch (e) { return []; } } -function getComponentsFromTemplate(template: string): TemplateChildNode[] { +function getComponents(template: string): TemplateChildNode[] { const ast = baseParse(template); const components = ast?.children; if (!components) return []; @@ -170,7 +170,7 @@ function getComponentsFromTemplate(template: string): TemplateChildNode[] { * @param slotProp Prop used to simulate a slot */ -export function generateSource( +export function generateTemplateSource( componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, args: Args, argTypes: ArgTypes, @@ -226,7 +226,6 @@ export function generateSource( ? `<${name} ${propsSource}/>` : `<${name} ${propsSource}>${childSources}`; } - return null; }; @@ -243,46 +242,31 @@ export function generateSource( * @param context StoryContext */ export const sourceDecorator = (storyFn: any, context: StoryContext) => { - const channel = addons.getChannel(); const skip = skipSourceRender(context); const story = storyFn(); - let source: string; - useEffect(() => { - if (!skip && source) { - const { id, args } = context; - channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); - } - }); - - if (skip) { - return story; - } - - const { args = {}, component: ctxtComponent, argTypes = {} } = context || {}; - - const components = getComponentsFromRenderFn(context?.originalStoryFn, context); + watch( + () => context.args, + () => { + if (!skip) { + generateSource(context); + } + }, + { immediate: true, deep: true } + ); + return story; +}; +export function generateSource(context: StoryContext) { + const channel = addons.getChannel(); + const { args = {}, component: ctxtComponent, argTypes = {}, id } = context || {}; + const components = getTemplateComponents(context?.originalStoryFn, context); const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; - const generatedTemplate = generateSource(storyComponent, args, argTypes, withScript); - - if (generatedTemplate) { - source = `${generatedScript}\n `; - } - - return story; -}; - -export function getTemplateSource(context: StoryContext) { - const channel = addons.getChannel(); - const components = getComponentsFromRenderFn(context?.originalStoryFn, context); - const storyComponent = components.length ? components : (context.component as TemplateChildNode); - const generatedTemplate = generateSource(storyComponent, context.args, context.argTypes); + const generatedTemplate = generateTemplateSource(storyComponent, context.args, context.argTypes); if (generatedTemplate) { - const source = ``; - const { id, args } = context; + const source = `${generatedScript}\n `; channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); return source; } @@ -291,8 +275,8 @@ export function getTemplateSource(context: StoryContext) { // export local function for testing purpose export { generateScriptSetup, - getComponentsFromRenderFn, - getComponentsFromTemplate, + getTemplateComponents as getComponentsFromRenderFn, + getComponents as getComponentsFromTemplate, mapAttributesAndDirectives, attributeSource, htmlEventAttributeToVueEventAttribute, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 5877135a98bb..e61c2967ab99 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,10 +1,9 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext, Renderer } from '@storybook/csf'; +import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -import { getTemplateSource as generateTemplateSource } from './docs/sourceDecorator'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -43,9 +42,7 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { if (reactiveState) reactiveState.globals = storyContext.globals; - updateArgs(existingApp.reactiveArgs, storyContext.args); - generateTemplateSource(storyContext as StoryContext); return () => { teardown(existingApp.vueApp, canvasElement); }; From 51cf25286b8a8c8a00651720af6f86bd99d4f0ce Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 7 Mar 2023 05:57:03 +0400 Subject: [PATCH 0061/1428] just to rerun the ci, it did pass on server y?? --- code/renderers/vue3/src/docs/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 05d72791e015..7c5b3f19675b 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -1,5 +1,9 @@ import type { Args } from '@storybook/types'; +/** + * omit event args + * @param args + */ const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); From 7d81483e4810e0084a0e14458632e62c8ebd049d Mon Sep 17 00:00:00 2001 From: thtliife <9210782+thtliife@users.noreply.github.com> Date: Wed, 8 Mar 2023 14:19:23 +1000 Subject: [PATCH 0062/1428] fix: Storybook does not respect BROWSER env var Storybook does not respect BROWSER env var if default browser is not chromium based. This commit adds a check for the environment variable, and passes it to the open package as an argument if the specified browser is not chromium based. Closes #21343 --- .../core-server/src/utils/open-in-browser.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/code/lib/core-server/src/utils/open-in-browser.ts b/code/lib/core-server/src/utils/open-in-browser.ts index 9cb05bbda5ac..f7e7be3d5a70 100644 --- a/code/lib/core-server/src/utils/open-in-browser.ts +++ b/code/lib/core-server/src/utils/open-in-browser.ts @@ -5,14 +5,29 @@ import getDefaultBrowser from '@aw-web-design/x-default-browser'; import { dedent } from 'ts-dedent'; export function openInBrowser(address: string) { + const browserEnvVar = process.env.BROWSER; + const userBrowserIsChrome = + browserEnvVar === 'chrome' || + browserEnvVar === 'chromium' || + browserEnvVar === 'brave' || + browserEnvVar === 'com.brave.browser'; + + const openOptions = browserEnvVar ? { app: { name: browserEnvVar } } : {}; + getDefaultBrowser(async (err: any, res: any) => { try { - if (res && (res.isChrome || res.isChromium || res.identity === 'com.brave.browser')) { + if ( + res && + (res.isChrome || + res.isChromium || + res.identity === 'com.brave.browser' || + userBrowserIsChrome) + ) { // We use betterOpn for Chrome because it is better at handling which chrome tab // or window the preview loads in. betterOpn(address); } else { - await open(address); + await open(address, openOptions); } } catch (error) { logger.error(dedent` From 6c806b6409e6c50377f56e4028e5ef812e70ac3a Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 03:00:29 +0400 Subject: [PATCH 0063/1428] fix slots reactivity without render function temp --- code/renderers/vue3/src/decorateStory.ts | 1 + code/renderers/vue3/src/render.ts | 33 +++++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 17c3326abb90..164feec6be20 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -42,6 +42,7 @@ function prepare( return { render() { + console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index e61c2967ab99..a6f45768e62b 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -13,7 +13,8 @@ export const render: ArgsStoryFn = (props, context) => { ); } - return h(Component, props, getSlots(props, context)); + const slots = getSlots(context); + return h(Component, props, slots); }; let setupFunction = (_app: any) => {}; @@ -74,6 +75,12 @@ export function renderToCanvas( reactiveArgs, }); }, + renderTracked(event) { + console.log('vueApp renderTracked', event); // this works only in dev mode + }, + renderTriggered(event) { + console.log('vueApp renderTriggered', event); // this works only in dev mode + }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); @@ -85,19 +92,21 @@ export function renderToCanvas( }; } -/** - * get the slots as functions to be rendered - * @param props - * @param context - */ - -function getSlots(props: Args, context: StoryContext) { +function getSlots(context: StoryContext) { const { argTypes } = context; - const slots = Object.entries(props) + const slots = Object.entries(argTypes) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') - .map(([key, value]) => [key, typeof value === 'function' ? value : () => value]); - - return Object.fromEntries(slots); + .map(([key, value]) => [ + key, + () => { + if (typeof context.args[key] === 'function') return h(context.args[key]); + if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); + if (typeof context.args[key] === 'string') return context.args[key]; + return context.args[key]; + }, + ]); + + return reactive(Object.fromEntries(slots)); } /** From a75e222bbaf902bb866ef00d8eb2b770baa46032 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 03:26:24 +0400 Subject: [PATCH 0064/1428] fix undefined attr + cleanup --- code/renderers/vue3/src/decorateStory.ts | 10 +++++----- code/renderers/vue3/src/docs/utils.ts | 2 +- code/renderers/vue3/src/render.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 164feec6be20..13ed507a822a 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -32,24 +32,24 @@ function prepare( ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, renderTracked(event) { - console.log('innerStory renderTracked', event); // this works only in dev mode + // console.log('innerStory renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('innerStory renderTriggered', event); + // console.log('innerStory renderTriggered', event); }, }; } return { render() { - console.log('story render', story, this.$slots); + // console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { - console.log('story renderTracked', event); // this works only in dev mode + // console.log('story renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('story renderTriggered', event); + // console.log('story renderTriggered', event); }, }; } diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 7c5b3f19675b..538a235fbeb0 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -8,7 +8,7 @@ const omitEvent = (args: Args): Args => Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); const displayObject = (obj: any): string | boolean | number => { - if (typeof obj === 'object') { + if (obj && typeof obj === 'object') { return `{${Object.keys(obj) .map((key) => `${key}:${displayObject(obj[key])}`) .join(',')}}`; diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index a6f45768e62b..c0dd91e36ddc 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, reactive, watch } from 'vue'; +import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, Args, StoryContext } from '@storybook/csf'; import { global as globalThis } from '@storybook/global'; @@ -76,10 +76,10 @@ export function renderToCanvas( }); }, renderTracked(event) { - console.log('vueApp renderTracked', event); // this works only in dev mode + // console.log('vueApp renderTracked', event); // this works only in dev mode }, renderTriggered(event) { - console.log('vueApp renderTriggered', event); // this works only in dev mode + // console.log('vueApp renderTriggered', event); // this works only in dev mode }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); @@ -99,7 +99,9 @@ function getSlots(context: StoryContext) { .map(([key, value]) => [ key, () => { - if (typeof context.args[key] === 'function') return h(context.args[key]); + if (typeof context.args[key] === 'function' || isVNode(context.args[key])) + return h(context.args[key]); + if (Array.isArray(context.args[key])) return context.args[key].map((item: any) => h(item)); if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); if (typeof context.args[key] === 'string') return context.args[key]; return context.args[key]; From fc833f4871cbf978d1974bac8cfd2d285815bf5b Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 19:21:13 +0400 Subject: [PATCH 0065/1428] run decorators in renderToCanvas save reactivity --- code/lib/store/template/stories/globals.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/lib/store/template/stories/globals.stories.ts b/code/lib/store/template/stories/globals.stories.ts index 216551d9fb5b..625c14829e9e 100644 --- a/code/lib/store/template/stories/globals.stories.ts +++ b/code/lib/store/template/stories/globals.stories.ts @@ -29,6 +29,7 @@ export const Events = { ], play: async ({ canvasElement }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + await channel.emit('updateGlobals', { globals: { foo: 'fooValue' } }); await within(canvasElement).findByText('fooValue'); await channel.emit('updateGlobals', { globals: { foo: 'updated' } }); From a46d316e35dcf489fa0f3d2227e9dd75e58ff365 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 19:22:37 +0400 Subject: [PATCH 0066/1428] great reasult finally --- code/renderers/vue3/src/decorateStory.ts | 3 +- code/renderers/vue3/src/render.ts | 71 ++++++++++++++++++++---- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 13ed507a822a..47a0d325efc7 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -12,6 +12,7 @@ import type { VueRenderer } from './types'; The concept is taken from Vue 3's `defineComponent` but changed from creating a `setup` method on the ComponentOptions so end-users don't need to specify a "thunk" as a decorator. */ + function normalizeFunctionalComponent(options: ConcreteComponent): ComponentOptions { return typeof options === 'function' ? { render: options, name: options.name } : options; } @@ -92,7 +93,7 @@ export function decorateStory( * @param context * @param update */ -function updateReactiveContext( +export function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index c0dd91e36ddc..09608202227a 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,9 +1,16 @@ /* eslint-disable no-param-reassign */ import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; -import type { Globals, Args, StoryContext } from '@storybook/csf'; -import { global as globalThis } from '@storybook/global'; +import type { + Globals, + Args, + StoryContext, + DecoratorFunction, + PartialStoryFn, +} from '@storybook/csf'; + import type { StoryFnVueReturnType, VueRenderer } from './types'; +import { updateReactiveContext } from './decorateStory'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -13,7 +20,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - const slots = getSlots(context); + const slots = generateSlots(context); return h(Component, props, slots); }; @@ -31,6 +38,7 @@ const map = new Map< >(); let reactiveState: { globals: Globals; + changed: boolean; }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, @@ -42,7 +50,8 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - if (reactiveState) reactiveState.globals = storyContext.globals; + updateGlobals(storyContext); + updateContextDecorator(storyFn, storyContext); updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { teardown(existingApp.vueApp, canvasElement); @@ -54,18 +63,19 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - reactiveState = reactive({ globals: storyContext.globals }); - const rootElement: StoryFnVueReturnType = storyFn(); + reactiveState = reactive({ globals: storyContext.globals, changed: false }); + let rootElement: StoryFnVueReturnType = storyFn(); watch( () => reactiveState.globals, - (newVal) => { - const channel = (globalThis as any).__STORYBOOK_ADDONS_CHANNEL__; - channel.emit('forceRemount', { storyId: id }); + () => { + reactiveState.changed = true; } ); return () => { + storyContext.globals = reactiveState.globals; + rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; }, @@ -92,7 +102,12 @@ export function renderToCanvas( }; } -function getSlots(context: StoryContext) { +/** + * generate slots for default story without render function template + * @param context + */ + +function generateSlots(context: StoryContext) { const { argTypes } = context; const slots = Object.entries(argTypes) .filter(([key, value]) => argTypes[key]?.table?.category === 'slots') @@ -110,6 +125,39 @@ function getSlots(context: StoryContext) { return reactive(Object.fromEntries(slots)); } +/** + * update vue reactive state for globals to be able to dectect changes and re-render the story + * @param storyContext + */ +function updateGlobals(storyContext: StoryContext) { + if (reactiveState) { + reactiveState.changed = false; + reactiveState.globals = storyContext.globals; + } +} + +/** + * update the context args in case of decorators that change args + * @param storyFn + * @param storyContext + */ + +function updateContextDecorator( + storyFn: PartialStoryFn, + storyContext: StoryContext +) { + const storyDecorators = storyContext.moduleExport?.decorators; + if (storyDecorators && storyDecorators.length > 0) { + storyDecorators.forEach((decorator: DecoratorFunction) => { + if (typeof decorator === 'function') { + decorator((update) => { + if (update) updateReactiveContext(storyContext, update); + return storyFn(); + }, storyContext); + } + }); + } +} /** * update the reactive args @@ -118,8 +166,10 @@ function getSlots(context: StoryContext) { * @returns */ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { + if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); const notMappedArgs = { ...nextArgs }; + Object.keys(currentArgs).forEach((key) => { const componentArg = currentArgs[key]; // if the arg is an object, we need to update the object @@ -128,6 +178,7 @@ export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { currentArgs[key][aKey] = nextArgs[aKey]; delete notMappedArgs[aKey]; + delete notMappedArgs[key]; } }); } else { From 069d6155f6c10a3dcd986e76f1f474a4a66f875e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Thu, 9 Mar 2023 21:39:28 +0400 Subject: [PATCH 0067/1428] add try catch in case decorator are react Fn use --- code/renderers/vue3/src/render.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 09608202227a..0b0c8535b9e3 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -149,11 +149,18 @@ function updateContextDecorator( const storyDecorators = storyContext.moduleExport?.decorators; if (storyDecorators && storyDecorators.length > 0) { storyDecorators.forEach((decorator: DecoratorFunction) => { - if (typeof decorator === 'function') { - decorator((update) => { - if (update) updateReactiveContext(storyContext, update); - return storyFn(); - }, storyContext); + try { + if (typeof decorator === 'function') { + decorator((update) => { + if (update) updateReactiveContext(storyContext, update); + return storyFn(); + }, storyContext); + } + } catch (e) { + console.error(e); + // in case the decorator throws an error, we need to re-render the story + // mostly because of react hooks that are not allowed to be called conditionally + reactiveState.changed = true; } }); } From 8a3ea0a0b5bc3c623e9cfc48d9bf75b993744d68 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:15:50 +0400 Subject: [PATCH 0068/1428] slots properly rendered , and source generated --- code/renderers/vue3/src/render.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 0b0c8535b9e3..653dd38f8d7e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -75,6 +75,7 @@ export function renderToCanvas( return () => { storyContext.globals = reactiveState.globals; + console.log('---******---- render', reactiveState.changed, rootElement); rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; From 4408459fee84e9b4788ac0cd381de7c4e77d530e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:38:30 +0400 Subject: [PATCH 0069/1428] cleanup --- code/renderers/vue3/src/render.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 653dd38f8d7e..36d4ae7b974c 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -38,7 +38,6 @@ const map = new Map< >(); let reactiveState: { globals: Globals; - changed: boolean; }; export function renderToCanvas( { storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext, @@ -63,20 +62,18 @@ export function renderToCanvas( const vueApp = createApp({ setup() { storyContext.args = reactiveArgs; - reactiveState = reactive({ globals: storyContext.globals, changed: false }); + reactiveState = reactive({ globals: storyContext.globals }); let rootElement: StoryFnVueReturnType = storyFn(); watch( () => reactiveState.globals, () => { - reactiveState.changed = true; + storyContext.globals = reactiveState.globals; + rootElement = storyFn(); } ); return () => { - storyContext.globals = reactiveState.globals; - console.log('---******---- render', reactiveState.changed, rootElement); - rootElement = reactiveState.changed ? storyFn() : rootElement; return h(rootElement, reactiveArgs); }; }, @@ -132,7 +129,6 @@ function generateSlots(context: StoryContext) { */ function updateGlobals(storyContext: StoryContext) { if (reactiveState) { - reactiveState.changed = false; reactiveState.globals = storyContext.globals; } } @@ -161,7 +157,7 @@ function updateContextDecorator( console.error(e); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.changed = true; + reactiveState.globals = { ...storyContext.globals, change: Math.random() }; } }); } From 4b3ee6704be5540733beb170553d11bb3bd4711e Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Fri, 10 Mar 2023 07:45:56 +0400 Subject: [PATCH 0070/1428] source generating for slots with recusivity with --- .../vue3/src/docs/sourceDecorator.ts | 112 +++++++++++------- .../vue3/template/components/BaseLayout.vue | 17 +++ 2 files changed, 84 insertions(+), 45 deletions(-) create mode 100644 code/renderers/vue3/template/components/BaseLayout.vue diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index e0689ad886aa..a6e0f67e3be1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -3,7 +3,7 @@ import { addons } from '@storybook/preview-api'; import type { ArgTypes, Args, StoryContext, Renderer } from '@storybook/types'; -import { getDocgenSection, SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; +import { SourceType, SNIPPET_RENDERED } from '@storybook/docs-tools'; import type { ElementNode, @@ -14,7 +14,8 @@ import type { TemplateChildNode, } from '@vue/compiler-core'; import { baseParse } from '@vue/compiler-core'; -import { h, watch } from 'vue'; +import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue'; +import { h, isVNode, watch } from 'vue'; import { camelCase, kebabCase } from 'lodash'; import { attributeSource, @@ -23,7 +24,6 @@ import { displayObject, evalExp, } from './utils'; -import type { VueStoryComponent } from '../types'; /** * Check if the sourcecode should be generated. @@ -99,16 +99,28 @@ function mapAttributesAndDirectives(props: Args) { * map slots * @param slotsProps */ -function mapSlots(slotsProps: Args): TextNode[] { +function mapSlots(slotsProps: Args, generateComponentSource: (v: VNode) => string): TextNode[] { return Object.keys(slotsProps).map((key) => { const slot = slotsProps[key]; let slotContent = ''; - if (typeof slot === 'function') slotContent = ``; - slotContent = ``; - if (key === 'default') { + + if (typeof slot === 'function') { + const slotVNode = slot(); + if (isVNode(slotVNode)) { + slotContent = generateComponentSource(h(slotVNode)); + } + } + + if (typeof slot === 'object' && !isVNode(slot)) { slotContent = JSON.stringify(slot); } + if (typeof slot === 'string') { + slotContent = slot; + } + + slotContent = slot && slotContent.trim() ? `` : ``; + return { type: 2, content: slotContent, @@ -144,17 +156,20 @@ function generateScriptSetup(args: Args, argTypes: ArgTypes, components: any[]): function getTemplateComponents( renderFn: any, context?: StoryContext -): TemplateChildNode[] { +): (TemplateChildNode | VNode)[] { try { - const { template } = context ? renderFn(context.args, context) : renderFn(); - if (!template) return []; + const originalStoryFn = renderFn; + const story = originalStoryFn ? originalStoryFn(context?.args, context) : context?.component; + const { template } = story; + + if (!template) return [h(story, context?.args)]; return getComponents(template); } catch (e) { return []; } } -function getComponents(template: string): TemplateChildNode[] { +function getComponents(template: string): (TemplateChildNode | VNode)[] { const ast = baseParse(template); const components = ast?.children; if (!components) return []; @@ -171,22 +186,24 @@ function getComponents(template: string): TemplateChildNode[] { */ export function generateTemplateSource( - componentOrNodes: (VueStoryComponent | TemplateChildNode)[] | TemplateChildNode, + componentOrNodes: (ConcreteComponent | TemplateChildNode)[] | TemplateChildNode | VNode, args: Args, argTypes: ArgTypes, byRef = false ) { - const isComponent = (component: any) => component && typeof component.render === 'function'; const isElementNode = (node: any) => node && node.type === 1; const isInterpolationNode = (node: any) => node && node.type === 5; const isTextNode = (node: any) => node && node.type === 2; - const generateComponentSource = (componentOrNode: VueStoryComponent | TemplateChildNode) => { + const generateComponentSource = ( + componentOrNode: ConcreteComponent | TemplateChildNode | VNode + ) => { if (isElementNode(componentOrNode)) { const { tag: name, props: attributes, children } = componentOrNode as ElementNode; - const childSources: string = children - .map((child: TemplateChildNode) => generateComponentSource(child)) - .join(''); + const childSources: string = + typeof children === 'string' + ? children + : children.map((child: TemplateChildNode) => generateComponentSource(child)).join(''); const props = generateAttributesSource(attributes, args, argTypes, byRef); return childSources === '' @@ -197,35 +214,39 @@ export function generateTemplateSource( if (isInterpolationNode(componentOrNode) || isTextNode(componentOrNode)) { const { content } = componentOrNode as InterpolationNode | TextNode; // eslint-disable-next-line no-eval - if (typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval + if (content && typeof content !== 'string') return eval(content.loc.source); // it's a binding safe to eval return content; } - - if (isComponent(componentOrNode)) { - const concreteComponent = componentOrNode as VueStoryComponent; - const vnode = h(componentOrNode, args); - const { props } = vnode; - const { slots } = getDocgenSection(concreteComponent, 'slots') || {}; - const slotsProps = {} as Args; - const attrsProps = { ...props }; - if (slots && props) - Object.keys(props).forEach((prop: any) => { - const isSlot = slots.find(({ name: slotName }: { name: string }) => slotName === prop); - if (isSlot?.name) { - slotsProps[prop] = props[prop]; - delete attrsProps[prop]; - } - }); - const attributes = mapAttributesAndDirectives(attrsProps); - const childSources: string = mapSlots(slotsProps) - .map((child) => generateComponentSource(child)) - .join(''); - const name = concreteComponent.tag || concreteComponent.name || concreteComponent.__name; + if (isVNode(componentOrNode)) { + const vnode = componentOrNode as VNode; + const { props, type, children } = vnode; + const slotsProps = typeof children === 'string' ? undefined : (children as Args); + const attrsProps = slotsProps + ? Object.fromEntries( + Object.entries(props ?? {}) + .filter(([key, value]) => !slotsProps[key] && !['class', 'style'].includes(key)) + .map(([key, value]) => [key, value]) + ) + : props; + const attributes = mapAttributesAndDirectives(attrsProps ?? {}); + // eslint-disable-next-line no-nested-ternary + const childSources: string = children + ? typeof children === 'string' + ? children + : mapSlots(children as Args, generateComponentSource) + .map((child) => child.content) + .join('') + : ''; + const name = + typeof type === 'string' + ? type + : (type as FunctionalComponent).name || (type as ConcreteComponent).__name; const propsSource = generateAttributesSource(attributes, args, argTypes, byRef); return childSources.trim() === '' ? `<${name} ${propsSource}/>` : `<${name} ${propsSource}>${childSources}`; } + return null; }; @@ -244,6 +265,7 @@ export function generateTemplateSource( export const sourceDecorator = (storyFn: any, context: StoryContext) => { const skip = skipSourceRender(context); const story = storyFn(); + generateSource(context); watch( () => context.args, () => { @@ -251,20 +273,20 @@ export const sourceDecorator = (storyFn: any, context: StoryContext) = generateSource(context); } }, - { immediate: true, deep: true } + { deep: true } ); return story; }; export function generateSource(context: StoryContext) { const channel = addons.getChannel(); - const { args = {}, component: ctxtComponent, argTypes = {}, id } = context || {}; - const components = getTemplateComponents(context?.originalStoryFn, context); - const storyComponent = components.length ? components : (ctxtComponent as TemplateChildNode); + const { args = {}, argTypes = {}, id } = context || {}; + const storyComponents = getTemplateComponents(context?.originalStoryFn, context); const withScript = context?.parameters?.docs?.source?.withScriptSetup || false; - const generatedScript = withScript ? generateScriptSetup(args, argTypes, components) : ''; - const generatedTemplate = generateTemplateSource(storyComponent, context.args, context.argTypes); + const generatedScript = withScript ? generateScriptSetup(args, argTypes, storyComponents) : ''; + const generatedTemplate = generateTemplateSource(storyComponents, context.args, context.argTypes); + if (generatedTemplate) { const source = `${generatedScript}\n `; channel.emit(SNIPPET_RENDERED, { id, args, source, format: 'vue' }); diff --git a/code/renderers/vue3/template/components/BaseLayout.vue b/code/renderers/vue3/template/components/BaseLayout.vue new file mode 100644 index 000000000000..3e546165f187 --- /dev/null +++ b/code/renderers/vue3/template/components/BaseLayout.vue @@ -0,0 +1,17 @@ + + + From d8c87af2a03a5e6023b3a88baf664295392bb788 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 07:00:40 +0400 Subject: [PATCH 0071/1428] refactory and cleanup --- code/renderers/vue3/src/decorateStory.ts | 7 +- .../vue3/src/docs/sourceDecorator.ts | 20 +----- code/renderers/vue3/src/docs/utils.ts | 10 ++- code/renderers/vue3/src/render.ts | 67 ++++++------------- 4 files changed, 33 insertions(+), 71 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 47a0d325efc7..9c45525d11fd 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -1,5 +1,5 @@ import type { ConcreteComponent, Component, ComponentOptions } from 'vue'; -import { reactive, h } from 'vue'; +import { h } from 'vue'; import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; @@ -82,8 +82,8 @@ export function decorateStory( return story; } - const storyFuntion = () => h(story ?? 'story', context.args); - return prepare(decoratedStory, storyFuntion) as VueRenderer['storyResult']; + const innerStory = () => (story ? h(story, context.args) : null); + return prepare(decoratedStory, innerStory) as VueRenderer['storyResult']; }, (context) => prepare(storyFn(context)) as LegacyStoryFn ); @@ -97,7 +97,6 @@ export function updateReactiveContext( context: StoryContext, update: StoryContextUpdate> | undefined ) { - context.args = reactive(context.args); // get reference to reactiveArgs or create a new one; in case was destructured by decorator if (update) { const { args, argTypes } = update; if (args && !argTypes) { diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index a6e0f67e3be1..7092a0182a18 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -16,12 +16,11 @@ import type { import { baseParse } from '@vue/compiler-core'; import type { ConcreteComponent, FunctionalComponent, VNode } from 'vue'; import { h, isVNode, watch } from 'vue'; -import { camelCase, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; import { attributeSource, htmlEventAttributeToVueEventAttribute, omitEvent, - displayObject, evalExp, } from './utils'; @@ -58,22 +57,7 @@ export function generateAttributesSource( ): string { return Object.keys(tempArgs) .map((key: any) => { - const arg = tempArgs[key]; - - if (arg.type === 7) { - // AttributeNode binding type - const { exp, arg: argName } = arg; - const argKey = argName ? argName?.loc.source : undefined; - const argExpValue = exp?.loc.source ?? (exp as any).content; - const propValue = args[camelCase(argKey)]; - const argValue = argKey - ? propValue ?? evalExp(argExpValue, args) - : displayObject(omitEvent(args)); - - return argKey ? attributeSource(argKey, argValue, true) : `v-bind="${argValue}"`; - } - - return tempArgs[key].loc.source; + return evalExp(tempArgs[key].loc.source.replace(/\$props/g, 'args'), omitEvent(args)); }) .join(' '); } diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index 538a235fbeb0..f4d9a2000883 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -5,7 +5,9 @@ import type { Args } from '@storybook/types'; * @param args */ const omitEvent = (args: Args): Args => - Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))); + args + ? Object.fromEntries(Object.entries(args).filter(([key, value]) => !key.startsWith('on'))) + : {}; const displayObject = (obj: any): string | boolean | number => { if (obj && typeof obj === 'object') { @@ -34,13 +36,17 @@ const attributeSource = (key: string, value: unknown, dynamic?: boolean) => const evalExp = (argExpValue: any, args: Args): any => { let evalVal = argExpValue; + if (/v-bind="(\w+)"/.test(evalVal)) + return evalVal.replace(/"(\w+)"/g, `"${displayObject(args)}"`); Object.keys(args).forEach((akey) => { const regex = new RegExp(`(\\w+)\\.${akey}`, 'g'); - evalVal = evalVal.replace(regex, args[akey]); + evalVal = evalVal.replace(regex, displayObject(args[akey])); }); return evalVal; }; +// regExp match a word without dots with double quotes expression (e.g. "args") + export { omitEvent, displayObject, diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 36d4ae7b974c..7ccc47f6f45b 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -9,8 +9,8 @@ import type { PartialStoryFn, } from '@storybook/csf'; +import type { HooksContext } from 'lib/preview-api/src'; import type { StoryFnVueReturnType, VueRenderer } from './types'; -import { updateReactiveContext } from './decorateStory'; export const render: ArgsStoryFn = (props, context) => { const { id, component: Component } = context; @@ -20,8 +20,7 @@ export const render: ArgsStoryFn = (props, context) => { ); } - const slots = generateSlots(context); - return h(Component, props, slots); + return h(Component, props, generateSlots(context)); }; let setupFunction = (_app: any) => {}; @@ -49,7 +48,6 @@ export function renderToCanvas( // if the story is already rendered and we are not forcing a remount, we just update the reactive args if (existingApp && !forceRemount) { - updateGlobals(storyContext); updateContextDecorator(storyFn, storyContext); updateArgs(existingApp.reactiveArgs, storyContext.args); return () => { @@ -112,26 +110,17 @@ function generateSlots(context: StoryContext) { .map(([key, value]) => [ key, () => { - if (typeof context.args[key] === 'function' || isVNode(context.args[key])) - return h(context.args[key]); - if (Array.isArray(context.args[key])) return context.args[key].map((item: any) => h(item)); - if (typeof context.args[key] === 'object') return JSON.stringify(context.args[key]); - if (typeof context.args[key] === 'string') return context.args[key]; - return context.args[key]; + const slotValue = context.args[key]; + if (typeof slotValue === 'function' || isVNode(slotValue)) return h(slotValue); + if (Array.isArray(slotValue)) return slotValue.map((item: any) => h(item)); + if (typeof slotValue === 'object') return JSON.stringify(slotValue); + if (typeof slotValue === 'string') return slotValue; + return slotValue; }, ]); return reactive(Object.fromEntries(slots)); } -/** - * update vue reactive state for globals to be able to dectect changes and re-render the story - * @param storyContext - */ -function updateGlobals(storyContext: StoryContext) { - if (reactiveState) { - reactiveState.globals = storyContext.globals; - } -} /** * update the context args in case of decorators that change args @@ -143,21 +132,24 @@ function updateContextDecorator( storyFn: PartialStoryFn, storyContext: StoryContext ) { - const storyDecorators = storyContext.moduleExport?.decorators; - if (storyDecorators && storyDecorators.length > 0) { + const storyDecorators: Set> = ( + storyContext.hooks as HooksContext + ).mountedDecorators; + + if (storyDecorators && storyDecorators.size > 0) { storyDecorators.forEach((decorator: DecoratorFunction) => { try { if (typeof decorator === 'function') { - decorator((update) => { - if (update) updateReactiveContext(storyContext, update); - return storyFn(); + decorator((u) => { + if (u && u.args && !u.globals) return storyFn(); + return () => {}; }, storyContext); } } catch (e) { - console.error(e); + console.log(' issue with decorator ', decorator.name); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.globals = { ...storyContext.globals, change: Math.random() }; + reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; } }); } @@ -169,29 +161,10 @@ function updateContextDecorator( * @param nextArgs * @returns */ -export function updateArgs(reactiveArgs: Args, nextArgs: Args, argNames?: string[]) { +export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); - const notMappedArgs = { ...nextArgs }; - - Object.keys(currentArgs).forEach((key) => { - const componentArg = currentArgs[key]; - // if the arg is an object, we need to update the object - if (typeof componentArg === 'object') { - Object.keys(componentArg).forEach((aKey) => { - if (nextArgs[aKey] && (argNames?.includes(aKey) || !argNames)) { - currentArgs[key][aKey] = nextArgs[aKey]; - delete notMappedArgs[aKey]; - delete notMappedArgs[key]; - } - }); - } else { - currentArgs[key] = nextArgs[key]; - } - }); - Object.keys(notMappedArgs).forEach((key) => { - currentArgs[key] = notMappedArgs[key]; - }); + Object.assign(currentArgs, nextArgs); } /** From 5643df23093432c0a2350bcc3c4f4014a735a789 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 11:35:24 +0400 Subject: [PATCH 0072/1428] fix the units tests --- .../vue3/src/docs/sourceDecorator.test.ts | 4 ++-- code/renderers/vue3/src/render.test.ts | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/code/renderers/vue3/src/docs/sourceDecorator.test.ts b/code/renderers/vue3/src/docs/sourceDecorator.test.ts index 5a239de38120..43bd1c7476c3 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.test.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.test.ts @@ -303,7 +303,7 @@ describe('Vue3: generateSource() snippet', () => { [] as ArgsType, `` ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); test('template component camelCase bool Arg', () => { @@ -329,7 +329,7 @@ describe('Vue3: generateSource() snippet', () => { `` ) ).toMatchInlineSnapshot( - `` + `` ); }); diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index 5c39ef32a6b2..af4681d58354 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -23,17 +23,21 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2', argBar: 'bar2' } }); + expect(reactiveArgs).toEqual({ + objectArg: { argFoo: 'foo', argBar: 'bar' }, + argFoo: 'foo2', + argBar: 'bar2', + }); }); - test('update reactive Args component inherit objectArg only argName argName()', () => { + test('update reactive Args component inherit objectArg', () => { const reactiveArgs = reactive({ objectArg: { argFoo: 'foo' } }); // get reference to reactiveArgs or create a new one; expectTypeOf(reactiveArgs).toMatchTypeOf>(); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string } }>(); const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; - updateArgs(reactiveArgs, newArgs, ['argFoo']); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo2' }, argBar: 'bar2' }); + updateArgs(reactiveArgs, newArgs); + expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo' }, argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component 2 object args -> updateArgs()', () => { @@ -51,8 +55,10 @@ describe('Render Story', () => { updateArgs(reactiveArgs, newArgs); expect(reactiveArgs).toEqual({ - objectArg: { argFoo: 'foo2' }, - objectArg2: { argBar: 'bar2' }, + argFoo: 'foo2', + argBar: 'bar2', + objectArg: { argFoo: 'foo' }, + objectArg2: { argBar: 'bar' }, }); }); From 4634f30ac5301035be993626a2e7703c31b2ee1f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 12:54:42 +0400 Subject: [PATCH 0073/1428] remove console logs --- code/renderers/vue3/src/render.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 7ccc47f6f45b..2a6a5e861c65 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -146,7 +146,6 @@ function updateContextDecorator( }, storyContext); } } catch (e) { - console.log(' issue with decorator ', decorator.name); // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; From 12dd5fb2a9348d6ac61dc7a095de1ef0d479e679 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 13:07:05 +0400 Subject: [PATCH 0074/1428] cleanup oode and try to solve yn pkg issue --- code/renderers/vue3/src/decorateStory.ts | 1 - code/renderers/vue3/src/docs/utils.ts | 2 -- code/renderers/vue3/src/types.ts | 9 --------- 3 files changed, 12 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index 9c45525d11fd..e88495936183 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -43,7 +43,6 @@ function prepare( return { render() { - // console.log('story render', story, this.$slots); return h(story); }, renderTracked(event) { diff --git a/code/renderers/vue3/src/docs/utils.ts b/code/renderers/vue3/src/docs/utils.ts index f4d9a2000883..91fc1b42af06 100644 --- a/code/renderers/vue3/src/docs/utils.ts +++ b/code/renderers/vue3/src/docs/utils.ts @@ -45,8 +45,6 @@ const evalExp = (argExpValue: any, args: Args): any => { return evalVal; }; -// regExp match a word without dots with double quotes expression (e.g. "args") - export { omitEvent, displayObject, diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index f5b4576cedf7..fa17d9b0eeae 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -12,15 +12,6 @@ export type StoryFnVueReturnType = ConcreteComponent; export type StoryContext = StoryContextBase; -export type VueStoryComponent = ConcreteComponent & { - render: (h: any) => any; - props: VNodeProps; - slots: Slots; - tag?: string; - name?: string; - __name?: string; -}; - /** * @deprecated Use `VueRenderer` instead. */ From f2a8cc7885238fdef98298d6d0b4f3dc3f0d571f Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 17:03:47 +0400 Subject: [PATCH 0075/1428] cleanup and refactory --- code/renderers/vue3/src/decorateStory.ts | 14 +------------- code/renderers/vue3/src/render.ts | 6 ------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/code/renderers/vue3/src/decorateStory.ts b/code/renderers/vue3/src/decorateStory.ts index e88495936183..c6f18361f6da 100644 --- a/code/renderers/vue3/src/decorateStory.ts +++ b/code/renderers/vue3/src/decorateStory.ts @@ -32,12 +32,6 @@ function prepare( // Normalize so we can always spread an object ...normalizeFunctionalComponent(story), components: { ...(story.components || {}), story: innerStory }, - renderTracked(event) { - // console.log('innerStory renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('innerStory renderTriggered', event); - }, }; } @@ -45,12 +39,6 @@ function prepare( render() { return h(story); }, - renderTracked(event) { - // console.log('story renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('story renderTriggered', event); - }, }; } @@ -99,7 +87,7 @@ export function updateReactiveContext( if (update) { const { args, argTypes } = update; if (args && !argTypes) { - const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args + const deepCopy = JSON.parse(JSON.stringify(args)); // avoid reference to args we assume it's serializable Object.keys(context.args).forEach((key) => { delete context.args[key]; }); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 2a6a5e861c65..1c4ff919f4ef 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -81,12 +81,6 @@ export function renderToCanvas( reactiveArgs, }); }, - renderTracked(event) { - // console.log('vueApp renderTracked', event); // this works only in dev mode - }, - renderTriggered(event) { - // console.log('vueApp renderTriggered', event); // this works only in dev mode - }, }); vueApp.config.errorHandler = (e: unknown) => showException(e as Error); setupFunction(vueApp); From 9c8ae2c1ae3e61fd96f28924a87e1e110c7f6fd5 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 22:25:16 +0400 Subject: [PATCH 0076/1428] fix prep, pass tests --- code/renderers/vue3/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/types.ts b/code/renderers/vue3/src/types.ts index fa17d9b0eeae..15809f9094e2 100644 --- a/code/renderers/vue3/src/types.ts +++ b/code/renderers/vue3/src/types.ts @@ -1,5 +1,5 @@ import type { StoryContext as StoryContextBase, WebRenderer } from '@storybook/types'; -import type { ConcreteComponent, Slots, VNodeProps } from 'vue'; +import type { ConcreteComponent } from 'vue'; export type { RenderContext } from '@storybook/types'; From 3add1df064ae82f28166c5303d93ef94361ba9cf Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Mon, 13 Mar 2023 22:59:17 +0400 Subject: [PATCH 0077/1428] refactory get slots --- code/renderers/vue3/src/render.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 1c4ff919f4ef..ad12b9e6d0a4 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { createApp, h, isReactive, isVNode, reactive, watch } from 'vue'; +import { createApp, h, isReactive, reactive, watch } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Globals, @@ -105,11 +105,7 @@ function generateSlots(context: StoryContext) { key, () => { const slotValue = context.args[key]; - if (typeof slotValue === 'function' || isVNode(slotValue)) return h(slotValue); - if (Array.isArray(slotValue)) return slotValue.map((item: any) => h(item)); - if (typeof slotValue === 'object') return JSON.stringify(slotValue); - if (typeof slotValue === 'string') return slotValue; - return slotValue; + return typeof slotValue === 'function' ? h(slotValue) : slotValue; }, ]); From f274f05fae3e92e6def48100d49fb607bc0be5a8 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 00:06:54 +0400 Subject: [PATCH 0078/1428] fix args updates and correct unit tests --- code/renderers/vue3/src/render.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/renderers/vue3/src/render.test.ts b/code/renderers/vue3/src/render.test.ts index af4681d58354..d1d04cf40638 100644 --- a/code/renderers/vue3/src/render.test.ts +++ b/code/renderers/vue3/src/render.test.ts @@ -24,7 +24,6 @@ describe('Render Story', () => { updateArgs(reactiveArgs, newArgs); expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>(); expect(reactiveArgs).toEqual({ - objectArg: { argFoo: 'foo', argBar: 'bar' }, argFoo: 'foo2', argBar: 'bar2', }); @@ -37,7 +36,7 @@ describe('Render Story', () => { const newArgs = { argFoo: 'foo2', argBar: 'bar2' }; updateArgs(reactiveArgs, newArgs); - expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'foo' }, argFoo: 'foo2', argBar: 'bar2' }); + expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2' }); }); test('update reactive Args component 2 object args -> updateArgs()', () => { @@ -57,8 +56,6 @@ describe('Render Story', () => { expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2', - objectArg: { argFoo: 'foo' }, - objectArg2: { argBar: 'bar' }, }); }); From 24aa49298d3cb6d499415a7bd8c932283f3cf778 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 00:08:12 +0400 Subject: [PATCH 0079/1428] fix args update --- code/renderers/vue3/src/render.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index ad12b9e6d0a4..64fe3cacfc1e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -153,6 +153,13 @@ function updateContextDecorator( export function updateArgs(reactiveArgs: Args, nextArgs: Args) { if (Object.keys(nextArgs).length === 0) return; const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs); + // delete all args in currentArgs that are not in nextArgs + Object.keys(currentArgs).forEach((key) => { + if (!(key in nextArgs)) { + delete currentArgs[key]; + } + }); + // update currentArgs with nextArgs Object.assign(currentArgs, nextArgs); } From 118ef872363d856d91c49bf2608f7cb09c46111d Mon Sep 17 00:00:00 2001 From: thtliife <9210782+thtliife@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:00:38 +1000 Subject: [PATCH 0080/1428] chore: update token permissions to hopefully fix dangerjs action --- code/lib/core-server/src/utils/open-in-browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/core-server/src/utils/open-in-browser.ts b/code/lib/core-server/src/utils/open-in-browser.ts index f7e7be3d5a70..e7251f7732dd 100644 --- a/code/lib/core-server/src/utils/open-in-browser.ts +++ b/code/lib/core-server/src/utils/open-in-browser.ts @@ -37,4 +37,4 @@ export function openInBrowser(address: string) { `); } }); -} +} \ No newline at end of file From fbb1014c4de73ea59ee35999626e78c88ecd757f Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 14 Mar 2023 13:15:08 +0100 Subject: [PATCH 0081/1428] fix linting --- code/lib/core-server/src/utils/open-in-browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/core-server/src/utils/open-in-browser.ts b/code/lib/core-server/src/utils/open-in-browser.ts index e7251f7732dd..f7e7be3d5a70 100644 --- a/code/lib/core-server/src/utils/open-in-browser.ts +++ b/code/lib/core-server/src/utils/open-in-browser.ts @@ -37,4 +37,4 @@ export function openInBrowser(address: string) { `); } }); -} \ No newline at end of file +} From a1deda0da71c43354845578e1fd90bcf8e1ba999 Mon Sep 17 00:00:00 2001 From: gitstart Date: Tue, 14 Mar 2023 15:30:26 +0000 Subject: [PATCH 0082/1428] chore: activate measure tool only in story mode Co-authored-by: phunguyenmurcul <51897872+phunguyenmurcul@users.noreply.github.com> --- code/addons/measure/src/withMeasure.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/addons/measure/src/withMeasure.ts b/code/addons/measure/src/withMeasure.ts index 8a6b2a41489f..4cf105844f55 100644 --- a/code/addons/measure/src/withMeasure.ts +++ b/code/addons/measure/src/withMeasure.ts @@ -46,7 +46,7 @@ export const withMeasure = (StoryFn: StoryFunction, context: StoryCont }); }; - if (measureEnabled) { + if (context.viewMode === 'story' && measureEnabled) { document.addEventListener('pointerover', onPointerOver); init(); window.addEventListener('resize', onResize); @@ -58,7 +58,7 @@ export const withMeasure = (StoryFn: StoryFunction, context: StoryCont window.removeEventListener('resize', onResize); destroy(); }; - }, [measureEnabled]); + }, [measureEnabled, context.viewMode]); return StoryFn(); }; From d62fa0fd9e831e8917cca30bebb7b110357886a6 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Tue, 14 Mar 2023 20:01:09 +0400 Subject: [PATCH 0083/1428] fix hooks story tests, by triggering render all tree --- code/renderers/vue3/src/render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 64fe3cacfc1e..c39fe14d735e 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -138,7 +138,7 @@ function updateContextDecorator( } catch (e) { // in case the decorator throws an error, we need to re-render the story // mostly because of react hooks that are not allowed to be called conditionally - reactiveState.globals = storyContext.globals; // { ...storyContext.globals }; + reactiveState.globals = { ...storyContext.globals, update: !decorator.name }; // { ...storyContext.globals }; } }); } From 69ac8138dcfdfd2874f070500665620bbeabfdb1 Mon Sep 17 00:00:00 2001 From: chakir qatab Date: Wed, 15 Mar 2023 00:33:12 +0400 Subject: [PATCH 0084/1428] add slots test story to vue3 renderer templates --- .../{components => stories}/BaseLayout.vue | 6 +-- .../template/stories/ReactiveSlots.stories.js | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) rename code/renderers/vue3/template/{components => stories}/BaseLayout.vue (69%) create mode 100644 code/renderers/vue3/template/stories/ReactiveSlots.stories.js diff --git a/code/renderers/vue3/template/components/BaseLayout.vue b/code/renderers/vue3/template/stories/BaseLayout.vue similarity index 69% rename from code/renderers/vue3/template/components/BaseLayout.vue rename to code/renderers/vue3/template/stories/BaseLayout.vue index 3e546165f187..c8773b85f326 100644 --- a/code/renderers/vue3/template/components/BaseLayout.vue +++ b/code/renderers/vue3/template/stories/BaseLayout.vue @@ -4,13 +4,13 @@ defineProps<{ otherProp: boolean; }>();