Skip to content

Commit 1b9b235

Browse files
committed
fix(compiler-dom): properly stringify class/style bindings when hoisting static strings
1 parent 189a0a3 commit 1b9b235

File tree

11 files changed

+57
-58
lines changed

11 files changed

+57
-58
lines changed

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ describe('compiler: element transform', () => {
623623

624624
test(`props merging: style`, () => {
625625
const { node } = parseWithElementTransform(
626-
`<div style="color: red" :style="{ color: 'red' }" />`,
626+
`<div style="color: green" :style="{ color: 'red' }" />`,
627627
{
628628
nodeTransforms: [transformStyle, transformElement],
629629
directiveTransforms: {
@@ -646,7 +646,7 @@ describe('compiler: element transform', () => {
646646
elements: [
647647
{
648648
type: NodeTypes.SIMPLE_EXPRESSION,
649-
content: `_hoisted_1`,
649+
content: `{"color":"green"}`,
650650
isStatic: false
651651
},
652652
{

packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
exports[`compile should contain standard transforms 1`] = `
44
"const _Vue = Vue
5-
const { createVNode: _createVNode } = _Vue
6-
7-
const _hoisted_1 = {}
85
96
return function render(_ctx, _cache) {
107
with (_ctx) {
@@ -14,7 +11,7 @@ return function render(_ctx, _cache) {
1411
_createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]),
1512
_createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]),
1613
_createVNode(\\"div\\", null, \\"test\\"),
17-
_createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"),
14+
_createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"),
1815
_createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */)
1916
], 64 /* STABLE_FRAGMENT */))
2017
}

packages/compiler-dom/__tests__/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('compile', () => {
55
const { code } = compile(`<div v-text="text"></div>
66
<div v-html="html"></div>
77
<div v-cloak>test</div>
8-
<div style="color=red">red</div>
8+
<div style="color:red">red</div>
99
<div :style="{color: 'green'}"></div>`)
1010

1111
expect(code).toMatchSnapshot()

packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ describe('stringify static html', () => {
7777

7878
test('serliazing constant bindings', () => {
7979
const { ast } = compileWithStringify(
80-
`<div><div>${repeat(
81-
`<span :class="'foo' + 'bar'">{{ 1 }} + {{ false }}</span>`,
80+
`<div><div :style="{ color: 'red' }">${repeat(
81+
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
8282
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
8383
)}</div></div>`
8484
)
@@ -89,8 +89,8 @@ describe('stringify static html', () => {
8989
callee: CREATE_STATIC,
9090
arguments: [
9191
JSON.stringify(
92-
`<div>${repeat(
93-
`<span class="foobar">1 + false</span>`,
92+
`<div style="color:red;">${repeat(
93+
`<span class="foo bar">1 + false</span>`,
9494
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
9595
)}</div>`
9696
)

packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,8 @@ function transformWithStyleTransform(
2626
}
2727

2828
describe('compiler: style transform', () => {
29-
test('should transform into directive node and hoist value', () => {
30-
const { root, node } = transformWithStyleTransform(
31-
`<div style="color: red"/>`
32-
)
33-
expect(root.hoists).toMatchObject([
34-
{
35-
type: NodeTypes.SIMPLE_EXPRESSION,
36-
content: `{"color":"red"}`,
37-
isStatic: false
38-
}
39-
])
29+
test('should transform into directive node', () => {
30+
const { node } = transformWithStyleTransform(`<div style="color: red"/>`)
4031
expect(node.props[0]).toMatchObject({
4132
type: NodeTypes.DIRECTIVE,
4233
name: `bind`,
@@ -47,7 +38,7 @@ describe('compiler: style transform', () => {
4738
},
4839
exp: {
4940
type: NodeTypes.SIMPLE_EXPRESSION,
50-
content: `_hoisted_1`,
41+
content: `{"color":"red"}`,
5142
isStatic: false
5243
}
5344
})
@@ -71,7 +62,7 @@ describe('compiler: style transform', () => {
7162
},
7263
value: {
7364
type: NodeTypes.SIMPLE_EXPRESSION,
74-
content: `_hoisted_1`,
65+
content: `{"color":"red"}`,
7566
isStatic: false
7667
}
7768
}

packages/compiler-dom/src/transforms/stringifyStatic.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
isString,
1515
isSymbol,
1616
escapeHtml,
17-
toDisplayString
17+
toDisplayString,
18+
normalizeClass,
19+
normalizeStyle,
20+
stringifyStyle
1821
} from '@vue/shared'
1922

2023
// Turn eligible hoisted static trees into stringied static nodes, e.g.
@@ -84,8 +87,15 @@ function stringifyElement(
8487
}
8588
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
8689
// constant v-bind, e.g. :foo="1"
90+
let evaluated = evaluateConstant(p.exp as SimpleExpressionNode)
91+
const arg = p.arg && (p.arg as SimpleExpressionNode).content
92+
if (arg === 'class') {
93+
evaluated = normalizeClass(evaluated)
94+
} else if (arg === 'style') {
95+
evaluated = stringifyStyle(normalizeStyle(evaluated))
96+
}
8797
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
88-
evaluateConstant(p.exp as ExpressionNode)
98+
evaluated
8999
)}"`
90100
}
91101
}
@@ -151,7 +161,7 @@ function evaluateConstant(exp: ExpressionNode): string {
151161
if (c.type === NodeTypes.TEXT) {
152162
res += c.content
153163
} else if (c.type === NodeTypes.INTERPOLATION) {
154-
res += evaluateConstant(c.content)
164+
res += toDisplayString(evaluateConstant(c.content))
155165
} else {
156166
res += evaluateConstant(c)
157167
}

packages/compiler-dom/src/transforms/transformStyle.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => {
1717
node.props.forEach((p, i) => {
1818
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
1919
// replace p with an expression node
20-
const exp = context.hoist(parseInlineCSS(p.value.content, p.loc))
2120
node.props[i] = {
2221
type: NodeTypes.DIRECTIVE,
2322
name: `bind`,
2423
arg: createSimpleExpression(`style`, true, p.loc),
25-
exp,
24+
exp: parseInlineCSS(p.value.content, p.loc),
2625
modifiers: [],
2726
loc: p.loc
2827
}
@@ -45,5 +44,5 @@ function parseInlineCSS(
4544
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
4645
}
4746
})
48-
return createSimpleExpression(JSON.stringify(res), false, loc)
47+
return createSimpleExpression(JSON.stringify(res), false, loc, true)
4948
}

packages/compiler-ssr/__tests__/ssrElement.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('ssr: element', () => {
101101
expect(
102102
getCompiledString(`<div style="color:red;" :style="bar"></div>`)
103103
).toMatchInlineSnapshot(
104-
`"\`<div style=\\"\${_ssrRenderStyle([_hoisted_1, _ctx.bar])}\\"></div>\`"`
104+
`"\`<div style=\\"\${_ssrRenderStyle([{\\"color\\":\\"red\\"}, _ctx.bar])}\\"></div>\`"`
105105
)
106106
})
107107

@@ -184,7 +184,7 @@ describe('ssr: element', () => {
184184
)
185185
).toMatchInlineSnapshot(`
186186
"\`<div\${_ssrRenderAttrs(_mergeProps({
187-
style: [_hoisted_1, _ctx.b]
187+
style: [{\\"color\\":\\"red\\"}, _ctx.b]
188188
}, _ctx.obj))}></div>\`"
189189
`)
190190
})

packages/compiler-ssr/__tests__/ssrVShow.spec.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@ describe('ssr: v-show', () => {
1616
.toMatchInlineSnapshot(`
1717
"const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
1818
19-
const _hoisted_1 = {\\"color\\":\\"red\\"}
20-
2119
return function ssrRender(_ctx, _push, _parent) {
2220
_push(\`<div style=\\"\${_ssrRenderStyle([
23-
_hoisted_1,
21+
{\\"color\\":\\"red\\"},
2422
(_ctx.foo) ? null : { display: \\"none\\" }
2523
])}\\"></div>\`)
2624
}"
@@ -48,11 +46,9 @@ describe('ssr: v-show', () => {
4846
).toMatchInlineSnapshot(`
4947
"const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\")
5048
51-
const _hoisted_1 = {\\"color\\":\\"red\\"}
52-
5349
return function ssrRender(_ctx, _push, _parent) {
5450
_push(\`<div style=\\"\${_ssrRenderStyle([
55-
_hoisted_1,
51+
{\\"color\\":\\"red\\"},
5652
{ fontSize: 14 },
5753
(_ctx.foo) ? null : { display: \\"none\\" }
5854
])}\\"></div>\`)
@@ -69,12 +65,10 @@ describe('ssr: v-show', () => {
6965
"const { mergeProps: _mergeProps } = require(\\"vue\\")
7066
const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
7167
72-
const _hoisted_1 = {\\"color\\":\\"red\\"}
73-
7468
return function ssrRender(_ctx, _push, _parent) {
7569
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_ctx.baz, {
7670
style: [
77-
_hoisted_1,
71+
{\\"color\\":\\"red\\"},
7872
{ fontSize: 14 },
7973
(_ctx.foo) ? null : { display: \\"none\\" }
8074
]

packages/server-renderer/src/helpers/ssrRenderAttrs.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { escapeHtml } from '@vue/shared'
1+
import { escapeHtml, stringifyStyle } from '@vue/shared'
22
import {
33
normalizeClass,
44
normalizeStyle,
55
propsToAttrMap,
6-
hyphenate,
76
isString,
8-
isNoUnitNumericStyleProp,
97
isOn,
108
isSSRSafeAttrName,
119
isBooleanAttr,
@@ -93,17 +91,5 @@ export function ssrRenderStyle(raw: unknown): string {
9391
return escapeHtml(raw)
9492
}
9593
const styles = normalizeStyle(raw)
96-
let ret = ''
97-
for (const key in styles) {
98-
const value = styles[key]
99-
const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
100-
if (
101-
isString(value) ||
102-
(typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
103-
) {
104-
// only render valid values
105-
ret += `${normalizedKey}:${value};`
106-
}
107-
}
108-
return escapeHtml(ret)
94+
return escapeHtml(stringifyStyle(styles))
10995
}

packages/shared/src/normalizeProp.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { isArray, isString, isObject } from './'
1+
import { isArray, isString, isObject, hyphenate } from './'
2+
import { isNoUnitNumericStyleProp } from './domAttrConfig'
23

34
export function normalizeStyle(
45
value: unknown
@@ -19,6 +20,27 @@ export function normalizeStyle(
1920
}
2021
}
2122

23+
export function stringifyStyle(
24+
styles: Record<string, string | number> | undefined
25+
): string {
26+
let ret = ''
27+
if (!styles) {
28+
return ret
29+
}
30+
for (const key in styles) {
31+
const value = styles[key]
32+
const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
33+
if (
34+
isString(value) ||
35+
(typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
36+
) {
37+
// only render valid values
38+
ret += `${normalizedKey}:${value};`
39+
}
40+
}
41+
return ret
42+
}
43+
2244
export function normalizeClass(value: unknown): string {
2345
let res = ''
2446
if (isString(value)) {

0 commit comments

Comments
 (0)