Skip to content

Commit 1b3df37

Browse files
committed
feat: allow passing props to templates via render()
1 parent 518be34 commit 1b3df37

4 files changed

Lines changed: 82 additions & 4 deletions

File tree

src/render/createRenderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface RenderedTemplate {
4444
}
4545

4646
export interface Renderer {
47-
render(input: string | Component, config: MaizzleConfig, opts?: { source?: string }): Promise<RenderedTemplate>
47+
render(input: string | Component, config: MaizzleConfig, opts?: { source?: string; props?: Record<string, any> }): Promise<RenderedTemplate>
4848
invalidate(filePath: string): Promise<void>
4949
invalidateAll(): Promise<void>
5050
close(): Promise<void>
@@ -412,7 +412,7 @@ export async function createRenderer(
412412
const server = await createServer(finalConfig)
413413

414414
return {
415-
async render(input: string | Component, config: MaizzleConfig, opts?: { source?: string }): Promise<RenderedTemplate> {
415+
async render(input: string | Component, config: MaizzleConfig, opts?: { source?: string; props?: Record<string, any> }): Promise<RenderedTemplate> {
416416
let component: Component
417417
let configKey: InjectionKey<MaizzleConfig>
418418
let contextKey: InjectionKey<RenderContext>
@@ -469,7 +469,7 @@ export async function createRenderer(
469469
}
470470

471471
const head = createHead({ disableDefaults: true })
472-
const app = createSSRApp(component)
472+
const app = createSSRApp(component, opts?.props)
473473
app.use(head)
474474

475475
// Register user Vue plugins, directives, and global properties

src/render/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export async function render(
3939
}
4040

4141
const resolvedConfig = resolveConfigObject(config)
42+
const { props, ...templateConfig } = resolvedConfig
4243

4344
/**
4445
* Reuse a renderer started by the Vite plugin when one is active.
@@ -59,7 +60,7 @@ export async function render(
5960
&& ['.vue', '.md'].includes(extname(template))
6061
&& !template.includes('\n')
6162

62-
const rendered = await renderer.render(isFile ? resolve(template) : template, resolvedConfig)
63+
const rendered = await renderer.render(isFile ? resolve(template) : template, templateConfig, { props })
6364
let html = rendered.html
6465

6566
const doctype = rendered.doctype ?? rendered.templateConfig.doctype ?? '<!DOCTYPE html>'

src/tests/render/props.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2+
import { rmSync } from 'node:fs'
3+
import { render } from '../../render/index.ts'
4+
import { createTempProject } from './_helpers.ts'
5+
6+
describe('render props', () => {
7+
let tempDir: string
8+
const originalCwd = process.cwd()
9+
10+
beforeEach(() => {
11+
tempDir = createTempProject()
12+
process.chdir(tempDir)
13+
})
14+
15+
afterEach(() => {
16+
process.chdir(originalCwd)
17+
rmSync(tempDir, { recursive: true, force: true })
18+
})
19+
20+
it('passes props to the template via defineProps', async () => {
21+
const result = await render(`
22+
<script setup>
23+
defineProps(['name'])
24+
</script>
25+
<template>
26+
<div>Hello {{ name }}</div>
27+
</template>
28+
`, {
29+
props: { name: 'Ava' },
30+
})
31+
32+
expect(result.html).toContain('Hello Ava')
33+
})
34+
35+
it('does not leak props into useConfig() or the returned config', async () => {
36+
const result = await render(`
37+
<script setup>
38+
const config = useConfig()
39+
</script>
40+
<template>
41+
<div>{{ config.props === undefined ? 'clean' : 'leaked' }}</div>
42+
</template>
43+
`, {
44+
props: { name: 'Ava' },
45+
})
46+
47+
expect(result.html).toContain('clean')
48+
expect(result.config.props).toBeUndefined()
49+
})
50+
51+
it('does not render a declared prop as a fallthrough attribute', async () => {
52+
const result = await render(`
53+
<script setup>
54+
defineProps(['name'])
55+
</script>
56+
<template>
57+
<div>{{ name }}</div>
58+
</template>
59+
`, {
60+
props: { name: 'Ava' },
61+
})
62+
63+
expect(result.html).not.toContain('name="Ava"')
64+
})
65+
})

src/types/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,18 @@ export interface MaizzleConfig {
744744
* }
745745
*/
746746
vue?: VueConfig
747+
/**
748+
* Props passed to the template's root component when rendering
749+
* programmatically. Map 1:1 to the template's `defineProps`. Not
750+
* merged into `useConfig()`.
751+
*
752+
* Props not declared via `defineProps` fall through as HTML
753+
* attributes on the root element, so declare every prop you pass.
754+
*
755+
* @example
756+
* render('./welcome.vue', { props: { name: 'Ava', plan: 'Pro' } })
757+
*/
758+
props?: Record<string, any>
747759

748760
// Events
749761

0 commit comments

Comments
 (0)