Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/parser/analyse/story/content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,5 +287,38 @@ describe(getStoryContentRawCode.name, () => {

expect(rawSource).toBe(`<SomeComponent wins="inner-template" {...args} />`);
});

it('works when a static children content provided without component in defineMeta', async ({
expect,
}) => {
const code = `
<script module>
import { defineMeta } from "@storybook/addon-svelte-csf";

const { Story } = defineMeta({
title: 'Templating',
});
</script>

<Story name="Static template">
<h2 data-testid="heading">Static template</h2>
<p>This story is static</p>
</Story>
`;
const ast = getSvelteAST({ code });
const svelteASTNodes = await extractSvelteASTNodes({ ast });
const { storyComponents } = svelteASTNodes;
const component = storyComponents[0].component;
const rawSource = getStoryContentRawCode({
nodes: {
component,
svelte: svelteASTNodes,
},
originalCode: code,
});

expect(rawSource).toBe(dedent`<h2 data-testid="heading">Static template</h2>
<p>This story is static</p>`);
});
});
});
11 changes: 8 additions & 3 deletions src/parser/analyse/story/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,15 @@ export function getStoryContentRawCode(params: Params): string {
filename,
});

// NOTE: It should never be `undefined` in this particular case, otherwise Storybook wouldn't know what to render.
return dedent(`<${defineMetaComponentValue?.name} {...args}>
// If there's no component defined in defineMeta, return the raw code directly
// This handles static templates without a component wrapper
if (!defineMetaComponentValue) {
return rawCode;
}

return dedent(`<${defineMetaComponentValue.name} {...args}>
${rawCode}
</${defineMetaComponentValue?.name}>`);
</${defineMetaComponentValue.name}>`);
}

/**
Expand Down
64 changes: 64 additions & 0 deletions src/runtime/emit-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,68 @@ describe('Emit Code', () => {
`
);
});

it('should exclude children from props when used in template and unwrap string literals', () => {
expect(
generateCodeToEmit({
code: '<Button {...args}>{args.children}</Button>',
args: {
onclick: 'onclick',
primary: true,
children: 'Click me',
},
})
).toMatchInlineSnapshot(`"<Button onclick="onclick" primary>Click me</Button>"`);
});

it('should exclude children from props when children is used in slot', () => {
expect(
generateCodeToEmit({
code: '<Button {...args}>{args.children}</Button>',
args: {
onclick: 'onclick',
primary: true,
size: 'large',
children: 'Button text',
},
})
).toMatchInlineSnapshot(
`"<Button onclick="onclick" primary size="large">Button text</Button>"`
);
});

it('should handle children when not used in template', () => {
expect(
generateCodeToEmit({
code: '<Button {...args} />',
args: {
onclick: 'onclick',
children: 'Click me',
},
})
).toMatchInlineSnapshot(`"<Button onclick="onclick" children="Click me" />"`);
});

it('should unwrap string literals in slot content', () => {
expect(
generateCodeToEmit({
code: '<Component>{args.text}</Component>',
args: {
text: 'Hello World',
},
})
).toMatchInlineSnapshot(`"<Component>Hello World</Component>"`);
});

it('should unwrap multiple string literals in slot content', () => {
expect(
generateCodeToEmit({
code: '<div><p>{args.first}</p><p>{args.second}</p></div>',
args: {
first: 'First text',
second: 'Second text',
},
})
).toMatchInlineSnapshot(`"<div><p>First text</p><p>Second text</p></div>"`);
});
});
19 changes: 18 additions & 1 deletion src/runtime/emit-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,18 @@ const skipSourceRender = (context: Params['storyContext']) => {
};

export const generateCodeToEmit = ({ code, args }: { code: string; args: StoryObj['args'] }) => {
// check if children is used in the template (e.g., {args.children})
// if so, we should exclude it from the props to avoid it appearing twice
const childrenUsedInTemplate = /\bargs\.children\b/.test(code);

const allPropsArray = Object.entries(args ?? {})
.map(([argKey, argValue]) => argsToProps(argKey, argValue))
.map(([argKey, argValue]) => {
// skip children if it's used in the template content (slot)
if (childrenUsedInTemplate && argKey === 'children') {
return null;
}
return argsToProps(argKey, argValue);
})
.filter((p) => p);

let allPropsString = allPropsArray.join(' ');
Expand All @@ -85,6 +95,13 @@ export const generateCodeToEmit = ({ code, args }: { code: string; args: StoryOb
const path = argPath.replaceAll('?', ''); // remove optional chaining character
const value = get({ args }, path);
return valueToString(value);
})
// clean up string literals in slot content: {"string"} => string
// This handles cases like <Button>{"Click me"}</Button> => <Button>Click me</Button>
.replace(/>\s*\{(".*?")\}\s*</g, (match, stringLiteral) => {
// Remove the quotes from the string literal
const unwrapped = stringLiteral.slice(1, -1);
return `>${unwrapped}<`;
});

return codeToEmit;
Expand Down