Skip to content

Commit 620a69b

Browse files
fix(runtime-dom): consistently remove boolean attributes for falsy values (#4348)
1 parent f855ccb commit 620a69b

File tree

12 files changed

+70
-22
lines changed

12 files changed

+70
-22
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe('ssr: element', () => {
177177
expect(getCompiledString(`<input type="checkbox" :checked="checked">`))
178178
.toMatchInlineSnapshot(`
179179
"\`<input type=\\"checkbox\\"\${
180-
(_ctx.checked) ? \\" checked\\" : \\"\\"
180+
(_ssrIncludeBooleanAttr(_ctx.checked)) ? \\" checked\\" : \\"\\"
181181
}>\`"
182182
`)
183183
})

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

+12-12
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ describe('ssr: v-model', () => {
3737
expect(
3838
compileWithWrapper(`<input type="radio" value="foo" v-model="bar">`).code
3939
).toMatchInlineSnapshot(`
40-
"const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
40+
"const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
4141
4242
return function ssrRender(_ctx, _push, _parent, _attrs) {
4343
_push(\`<div\${
4444
_ssrRenderAttrs(_attrs)
4545
}><input type=\\"radio\\" value=\\"foo\\"\${
46-
(_ssrLooseEqual(_ctx.bar, \\"foo\\")) ? \\" checked\\" : \\"\\"
46+
(_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.bar, \\"foo\\"))) ? \\" checked\\" : \\"\\"
4747
}></div>\`)
4848
}"
4949
`)
@@ -52,15 +52,15 @@ describe('ssr: v-model', () => {
5252
test('<input type="checkbox">', () => {
5353
expect(compileWithWrapper(`<input type="checkbox" v-model="bar">`).code)
5454
.toMatchInlineSnapshot(`
55-
"const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
55+
"const { ssrLooseContain: _ssrLooseContain, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
5656
5757
return function ssrRender(_ctx, _push, _parent, _attrs) {
5858
_push(\`<div\${
5959
_ssrRenderAttrs(_attrs)
6060
}><input type=\\"checkbox\\"\${
61-
((Array.isArray(_ctx.bar))
61+
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.bar))
6262
? _ssrLooseContain(_ctx.bar, null)
63-
: _ctx.bar) ? \\" checked\\" : \\"\\"
63+
: _ctx.bar)) ? \\" checked\\" : \\"\\"
6464
}></div>\`)
6565
}"
6666
`)
@@ -69,15 +69,15 @@ describe('ssr: v-model', () => {
6969
compileWithWrapper(`<input type="checkbox" value="foo" v-model="bar">`)
7070
.code
7171
).toMatchInlineSnapshot(`
72-
"const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
72+
"const { ssrLooseContain: _ssrLooseContain, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
7373
7474
return function ssrRender(_ctx, _push, _parent, _attrs) {
7575
_push(\`<div\${
7676
_ssrRenderAttrs(_attrs)
7777
}><input type=\\"checkbox\\" value=\\"foo\\"\${
78-
((Array.isArray(_ctx.bar))
78+
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.bar))
7979
? _ssrLooseContain(_ctx.bar, \\"foo\\")
80-
: _ctx.bar) ? \\" checked\\" : \\"\\"
80+
: _ctx.bar)) ? \\" checked\\" : \\"\\"
8181
}></div>\`)
8282
}"
8383
`)
@@ -87,13 +87,13 @@ describe('ssr: v-model', () => {
8787
`<input type="checkbox" :true-value="foo" :false-value="bar" v-model="baz">`
8888
).code
8989
).toMatchInlineSnapshot(`
90-
"const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
90+
"const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
9191
9292
return function ssrRender(_ctx, _push, _parent, _attrs) {
9393
_push(\`<div\${
9494
_ssrRenderAttrs(_attrs)
9595
}><input type=\\"checkbox\\"\${
96-
(_ssrLooseEqual(_ctx.baz, _ctx.foo)) ? \\" checked\\" : \\"\\"
96+
(_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.baz, _ctx.foo))) ? \\" checked\\" : \\"\\"
9797
}></div>\`)
9898
}"
9999
`)
@@ -103,13 +103,13 @@ describe('ssr: v-model', () => {
103103
`<input type="checkbox" true-value="foo" false-value="bar" v-model="baz">`
104104
).code
105105
).toMatchInlineSnapshot(`
106-
"const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
106+
"const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
107107
108108
return function ssrRender(_ctx, _push, _parent, _attrs) {
109109
_push(\`<div\${
110110
_ssrRenderAttrs(_attrs)
111111
}><input type=\\"checkbox\\"\${
112-
(_ssrLooseEqual(_ctx.baz, \\"foo\\")) ? \\" checked\\" : \\"\\"
112+
(_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.baz, \\"foo\\"))) ? \\" checked\\" : \\"\\"
113113
}></div>\`)
114114
}"
115115
`)

packages/compiler-ssr/src/runtimeHelpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
1010
export const SSR_RENDER_ATTR = Symbol(`ssrRenderAttr`)
1111
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`ssrRenderDynamicAttr`)
1212
export const SSR_RENDER_LIST = Symbol(`ssrRenderList`)
13+
export const SSR_INCLUDE_BOOLEAN_ATTR = Symbol(`ssrIncludeBooleanAttr`)
1314
export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`)
1415
export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
1516
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
@@ -28,6 +29,7 @@ export const ssrHelpers = {
2829
[SSR_RENDER_ATTR]: `ssrRenderAttr`,
2930
[SSR_RENDER_DYNAMIC_ATTR]: `ssrRenderDynamicAttr`,
3031
[SSR_RENDER_LIST]: `ssrRenderList`,
32+
[SSR_INCLUDE_BOOLEAN_ATTR]: `ssrIncludeBooleanAttr`,
3133
[SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
3234
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
3335
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,

packages/compiler-ssr/src/transforms/ssrTransformElement.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ import {
4343
SSR_RENDER_DYNAMIC_ATTR,
4444
SSR_RENDER_ATTRS,
4545
SSR_INTERPOLATE,
46-
SSR_GET_DYNAMIC_MODEL_PROPS
46+
SSR_GET_DYNAMIC_MODEL_PROPS,
47+
SSR_INCLUDE_BOOLEAN_ATTR
4748
} from '../runtimeHelpers'
4849
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
4950

@@ -237,7 +238,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
237238
if (isBooleanAttr(attrName)) {
238239
openTag.push(
239240
createConditionalExpression(
240-
value,
241+
createCallExpression(
242+
context.helper(SSR_INCLUDE_BOOLEAN_ATTR),
243+
[value]
244+
),
241245
createSimpleExpression(' ' + attrName, true),
242246
createSimpleExpression('', true),
243247
false /* no newline */

packages/runtime-dom/__tests__/patchAttrs.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ describe('runtime-dom: attrs patching', () => {
2323
expect(el.getAttribute('readonly')).toBe('')
2424
patchProp(el, 'readonly', true, false)
2525
expect(el.getAttribute('readonly')).toBe(null)
26+
patchProp(el, 'readonly', false, '')
27+
expect(el.getAttribute('readonly')).toBe('')
28+
patchProp(el, 'readonly', '', 0)
29+
expect(el.getAttribute('readonly')).toBe(null)
30+
patchProp(el, 'readonly', 0, '0')
31+
expect(el.getAttribute('readonly')).toBe('')
32+
patchProp(el, 'readonly', '0', false)
33+
expect(el.getAttribute('readonly')).toBe(null)
34+
patchProp(el, 'readonly', false, 1)
35+
expect(el.getAttribute('readonly')).toBe('')
36+
patchProp(el, 'readonly', 1, undefined)
37+
expect(el.getAttribute('readonly')).toBe(null)
2638
})
2739

2840
test('attributes', () => {

packages/runtime-dom/__tests__/patchProps.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ describe('runtime-dom: props patching', () => {
4343
expect(el.multiple).toBe(true)
4444
patchProp(el, 'multiple', null, null)
4545
expect(el.multiple).toBe(false)
46+
patchProp(el, 'multiple', null, true)
47+
expect(el.multiple).toBe(true)
48+
patchProp(el, 'multiple', null, 0)
49+
expect(el.multiple).toBe(false)
50+
patchProp(el, 'multiple', null, '0')
51+
expect(el.multiple).toBe(true)
52+
patchProp(el, 'multiple', null, false)
53+
expect(el.multiple).toBe(false)
54+
patchProp(el, 'multiple', null, 1)
55+
expect(el.multiple).toBe(true)
56+
patchProp(el, 'multiple', null, undefined)
57+
expect(el.multiple).toBe(false)
4658
})
4759

4860
test('innerHTML unmount prev children', () => {

packages/runtime-dom/src/modules/attrs.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { isSpecialBooleanAttr, makeMap, NOOP } from '@vue/shared'
1+
import {
2+
includeBooleanAttr,
3+
isSpecialBooleanAttr,
4+
makeMap,
5+
NOOP
6+
} from '@vue/shared'
27
import {
38
compatUtils,
49
ComponentInternalInstance,
@@ -28,7 +33,7 @@ export function patchAttr(
2833
// note we are only checking boolean attributes that don't have a
2934
// corresponding dom prop of the same name here.
3035
const isBoolean = isSpecialBooleanAttr(key)
31-
if (value == null || (isBoolean && value === false)) {
36+
if (value == null || (isBoolean && !includeBooleanAttr(value))) {
3237
el.removeAttribute(key)
3338
} else {
3439
el.setAttribute(key, isBoolean ? '' : value)

packages/runtime-dom/src/modules/props.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This can come from explicit usage of v-html or innerHTML as a prop in render
44

55
import { warn, DeprecationTypes, compatUtils } from '@vue/runtime-core'
6+
import { includeBooleanAttr } from '@vue/shared'
67

78
// functions. The user is responsible for using them with only trusted content.
89
export function patchDOMProp(
@@ -41,9 +42,9 @@ export function patchDOMProp(
4142

4243
if (value === '' || value == null) {
4344
const type = typeof el[key]
44-
if (value === '' && type === 'boolean') {
45+
if (type === 'boolean') {
4546
// e.g. <select multiple> compiles to { multiple: '' }
46-
el[key] = true
47+
el[key] = includeBooleanAttr(value)
4748
return
4849
} else if (value == null && type === 'string') {
4950
// e.g. <div :id="null">

packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ describe('ssr: renderAttrs', () => {
5050
expect(
5151
ssrRenderAttrs({
5252
checked: true,
53-
multiple: false
53+
multiple: false,
54+
readonly: 0,
55+
disabled: ''
5456
})
55-
).toBe(` checked`) // boolean attr w/ false should be ignored
57+
).toBe(` checked disabled`) // boolean attr w/ false should be ignored
5658
})
5759

5860
test('ignore falsy values', () => {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isOn,
88
isSSRSafeAttrName,
99
isBooleanAttr,
10+
includeBooleanAttr,
1011
makeMap
1112
} from '@vue/shared'
1213

@@ -52,7 +53,7 @@ export function ssrRenderDynamicAttr(
5253
? key // preserve raw name on custom elements
5354
: propsToAttrMap[key] || key.toLowerCase()
5455
if (isBooleanAttr(attrKey)) {
55-
return value === false ? `` : ` ${attrKey}`
56+
return includeBooleanAttr(value) ? ` ${attrKey}` : ``
5657
} else if (isSSRSafeAttrName(attrKey)) {
5758
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
5859
} else {

packages/server-renderer/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
export { ssrInterpolate } from './helpers/ssrInterpolate'
2828
export { ssrRenderList } from './helpers/ssrRenderList'
2929
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
30+
export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared'
3031

3132
// v-model helpers
3233
export {

packages/shared/src/domAttrConfig.ts

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export const isBooleanAttr = /*#__PURE__*/ makeMap(
2424
`checked,muted,multiple,selected`
2525
)
2626

27+
/**
28+
* Boolean attributes should be included if the value is truthy or ''.
29+
* e.g. <select multiple> compiles to { multiple: '' }
30+
*/
31+
export function includeBooleanAttr(value: unknown): boolean {
32+
return !!value || value === ''
33+
}
34+
2735
const unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/
2836
const attrValidationCache: Record<string, boolean> = {}
2937

0 commit comments

Comments
 (0)