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
16 changes: 16 additions & 0 deletions packages/compiler-ssr/__tests__/ssrComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ describe('ssr: components', () => {
`)
})

test('static style', () => {
expect(compile(`<div><foo style="color: red" /></div>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent } = require("vue")
const { ssrRenderComponent: _ssrRenderComponent, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent("foo")

_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
_push(_ssrRenderComponent(_component_foo, { style: {"color":"red"} }, null, _parent))
_push(\`</div>\`)
}"
`)
})

// event listeners should still be passed
test('event listeners', () => {
expect(compile(`<foo @click="bar" />`).code).toMatchInlineSnapshot(`
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler-ssr/__tests__/ssrElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,35 @@ describe('ssr: element', () => {
`)
})

test('static style', () => {
expect(
getCompiledString(
`<div style="color: red; --Custom-Prop: 1px; z-index: 1; font-size: 14px !important"></div>`,
),
).toMatchInlineSnapshot(
`"\`<div style="color:red;--Custom-Prop:1px;z-index:1;font-size:14px !important;"></div>\`"`,
)
})

test('static style with escaping', () => {
expect(
getCompiledString(
`<div style="font-family: &quot;A&amp;B&quot;; background: url('a.png?x=1&y=2')"></div>`,
),
).toMatchInlineSnapshot(
`"\`<div style="font-family:&quot;A&amp;B&quot;;background:url(&#39;a.png?x=1&amp;y=2&#39;);"></div>\`"`,
)
})

test('v-bind:style + static style', () => {
expect(getCompiledString(`<div :style="bar" style="color:red;"></div>`))
.toMatchInlineSnapshot(`
"\`<div style="\${
_ssrRenderStyle([_ctx.bar, {"color":"red"}])
}"></div>\`"
`)
})

test('static style + v-bind:style', () => {
expect(getCompiledString(`<div style="color:red;" :style="bar"></div>`))
.toMatchInlineSnapshot(`
Expand Down Expand Up @@ -274,6 +303,13 @@ describe('ssr: element', () => {
}></div>\`"
`)

expect(getCompiledString(`<div style="color:red;" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ style: {"color":"red"} }, _ctx.obj))
}></div>\`"
`)

// should merge style and :style
expect(
getCompiledString(
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ describe('ssr: attrs fallthrough', () => {
`)
})

test('with static style', () => {
expect(compile(`<div style="color: red"/>`).code).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")

return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps({ style: {"color":"red"} }, _attrs))}></div>\`)
}"
`)
})

test('with comments', () => {
expect(compile(`<!--!--><div/>`).code).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
Expand Down
8 changes: 5 additions & 3 deletions packages/compiler-ssr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
transformBind,
transformExpression,
transformOn,
transformStyle,
transformVBindShorthand,
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
import {
ssrTransformElement,
ssrTransformStyle,
} from './transforms/ssrTransformElement'
import {
rawOptionsMap,
ssrTransformComponent,
Expand Down Expand Up @@ -67,7 +69,7 @@ export function compile(
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,
transformStyle,
ssrTransformStyle,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: {
Expand Down
49 changes: 40 additions & 9 deletions packages/compiler-ssr/src/transforms/ssrTransformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ import {
hasDynamicKeyVBind,
isStaticArgOf,
isStaticExp,
transformStyle,
} from '@vue/compiler-dom'
import {
NO,
escapeHtml,
isBooleanAttr,
isBuiltInDirective,
isSSRSafeAttrName,
parseStringStyle,
propsToAttrMap,
stringifyStyle,
} from '@vue/shared'
import { SSRErrorCodes, createSSRCompilerError } from '../errors'
import {
Expand All @@ -65,6 +68,16 @@ const rawChildrenMap = new WeakMap<
TemplateLiteral['elements'][0]
>()

// static styles on plain elements are rendered by ssrTransformElement
export const ssrTransformStyle: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType !== ElementTypes.ELEMENT
) {
transformStyle(node, context)
}
}

export const ssrTransformElement: NodeTransform = (node, context) => {
if (
node.type !== NodeTypes.ELEMENT ||
Expand Down Expand Up @@ -101,6 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {

const needMergeProps = hasDynamicVBind || hasCustomDir
if (needMergeProps) {
transformStyle(node, context)
const { props, directives } = buildProps(
node,
context,
Expand Down Expand Up @@ -213,9 +227,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// book keeping static/dynamic class merging.
let dynamicClassBinding: CallExpression | undefined = undefined
let staticClassBinding: string | undefined = undefined
// all style bindings are converted to dynamic by transformStyle.
// but we need to make sure to merge them.
let dynamicStyleBinding: CallExpression | undefined = undefined
let staticStyleBinding: string | undefined = undefined
let staticStyleIndex = -1

for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
Expand Down Expand Up @@ -274,14 +288,20 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// already has style binding, merge into it.
mergeCall(dynamicStyleBinding, value)
} else {
openTag.push(
` style="`,
(dynamicStyleBinding = createCallExpression(
context.helper(SSR_RENDER_STYLE),
[value],
)),
`"`,
dynamicStyleBinding = createCallExpression(
context.helper(SSR_RENDER_STYLE),
[
staticStyleBinding
? createArrayExpression([staticStyleBinding, value])
: value,
],
)
const parts = [` style="`, dynamicStyleBinding, `"`]
if (staticStyleBinding) {
openTag.splice(staticStyleIndex, 1, ...parts)
} else {
openTag.push(...parts)
}
}
} else {
attrName =
Expand Down Expand Up @@ -354,6 +374,17 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
if (name === 'class' && prop.value) {
staticClassBinding = JSON.stringify(prop.value.content)
}
if (name === 'style' && prop.value) {
const style = parseStringStyle(prop.value.content)
if (dynamicStyleBinding) {
mergeCall(dynamicStyleBinding, JSON.stringify(style))
} else {
staticStyleBinding = JSON.stringify(style)
staticStyleIndex = openTag.length
openTag.push(` style="${escapeHtml(stringifyStyle(style))}"`)
}
continue
}
openTag.push(
` ${prop.name}` +
(prop.value ? `="${escapeHtml(prop.value.content)}"` : ``),
Expand Down
38 changes: 38 additions & 0 deletions packages/server-renderer/__tests__/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,44 @@ function testRender(type: string, render: typeof renderToString) {
).toBe(`<div>hello</div>`)
})

test('static style', async () => {
expect(
await render(
createApp({
components: {
Child: {
inheritAttrs: false,
template: `<i>{{ JSON.stringify($attrs.style) }}</i>`,
},
},
data: () => ({ dynamic: { color: 'blue' }, obj: { id: 'a' } }),
template:
`<div>` +
`<p style="color: red; --Custom-Prop: 1px; z-index: 1; font-size: 14px !important"></p>` +
`<p style="font-family: &quot;A&amp;B&quot;; background: url('a.png?x=1&y=2')"></p>` +
`<p style="color: red" v-show="false"></p>` +
`<p style="color: red; margin: 0" :style="dynamic"></p>` +
`<p :style="dynamic" style="color: red; margin: 0"></p>` +
`<p style="color: red" v-bind="obj"></p>` +
`<svg><path style="stroke-width: 2"/></svg>` +
`<Child style="color: red; z-index: 1" />` +
`</div>`,
}),
),
).toBe(
`<div>` +
`<p style="color:red;--Custom-Prop:1px;z-index:1;font-size:14px !important;"></p>` +
`<p style="font-family:&quot;A&amp;B&quot;;background:url(&#39;a.png?x=1&amp;y=2&#39;);"></p>` +
`<p style="color:red;display:none;"></p>` +
`<p style="color:blue;margin:0;"></p>` +
`<p style="color:red;margin:0;"></p>` +
`<p style="color:red;" id="a"></p>` +
`<svg><path style="stroke-width:2;"></path></svg>` +
`<i>{&quot;color&quot;:&quot;red&quot;,&quot;z-index&quot;:&quot;1&quot;}</i>` +
`</div>`,
)
})

test('handle compiler errors', async () => {
await render(
// render different content since compilation is cached
Expand Down
21 changes: 21 additions & 0 deletions packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ describe('ssr: attr fallthrough', () => {
)
})

test('with static style', async () => {
const Child = {
props: ['ok'],
template: `<div v-if="ok" style="color: red; margin: 0" /><span v-else style="color: red" />`,
}
const TransitionChild = {
template: `<transition><div style="color: red; margin: 0" /></transition>`,
}
const Parent = {
props: ['ok'],
components: { Child, TransitionChild },
template: `<div><child :ok="ok" style="color: blue" /><transition-child style="color: blue" /></div>`,
}
expect(await renderToString(createApp(Parent, { ok: true }))).toBe(
`<div><div style="color:blue;margin:0;"></div><div style="color:blue;margin:0;"></div></div>`,
)
expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
`<div><span style="color:blue;"></span><div style="color:blue;margin:0;"></div></div>`,
)
})

test('with v-model', async () => {
const Child = {
props: ['text'],
Expand Down
Loading