expect(getCompiledString(`<input type="checkbox" :checked="checked">`))
.toMatchInlineSnapshot(`
"\`<input type=\\"checkbox\\"\${
- (_ctx.checked) ? \\" checked\\" : \\"\\"
+ (_ssrIncludeBooleanAttr(_ctx.checked)) ? \\" checked\\" : \\"\\"
}>\`"
`)
})
expect(
compileWithWrapper(`<input type="radio" value="foo" v-model="bar">`).code
).toMatchInlineSnapshot(`
- "const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+ "const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><input type=\\"radio\\" value=\\"foo\\"\${
- (_ssrLooseEqual(_ctx.bar, \\"foo\\")) ? \\" checked\\" : \\"\\"
+ (_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.bar, \\"foo\\"))) ? \\" checked\\" : \\"\\"
}></div>\`)
}"
`)
test('<input type="checkbox">', () => {
expect(compileWithWrapper(`<input type="checkbox" v-model="bar">`).code)
.toMatchInlineSnapshot(`
- "const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+ "const { ssrLooseContain: _ssrLooseContain, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><input type=\\"checkbox\\"\${
- ((Array.isArray(_ctx.bar))
+ (_ssrIncludeBooleanAttr((Array.isArray(_ctx.bar))
? _ssrLooseContain(_ctx.bar, null)
- : _ctx.bar) ? \\" checked\\" : \\"\\"
+ : _ctx.bar)) ? \\" checked\\" : \\"\\"
}></div>\`)
}"
`)
compileWithWrapper(`<input type="checkbox" value="foo" v-model="bar">`)
.code
).toMatchInlineSnapshot(`
- "const { ssrLooseContain: _ssrLooseContain, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+ "const { ssrLooseContain: _ssrLooseContain, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><input type=\\"checkbox\\" value=\\"foo\\"\${
- ((Array.isArray(_ctx.bar))
+ (_ssrIncludeBooleanAttr((Array.isArray(_ctx.bar))
? _ssrLooseContain(_ctx.bar, \\"foo\\")
- : _ctx.bar) ? \\" checked\\" : \\"\\"
+ : _ctx.bar)) ? \\" checked\\" : \\"\\"
}></div>\`)
}"
`)
`<input type="checkbox" :true-value="foo" :false-value="bar" v-model="baz">`
).code
).toMatchInlineSnapshot(`
- "const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+ "const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><input type=\\"checkbox\\"\${
- (_ssrLooseEqual(_ctx.baz, _ctx.foo)) ? \\" checked\\" : \\"\\"
+ (_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.baz, _ctx.foo))) ? \\" checked\\" : \\"\\"
}></div>\`)
}"
`)
`<input type="checkbox" true-value="foo" false-value="bar" v-model="baz">`
).code
).toMatchInlineSnapshot(`
- "const { ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
+ "const { ssrLooseEqual: _ssrLooseEqual, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}><input type=\\"checkbox\\"\${
- (_ssrLooseEqual(_ctx.baz, \\"foo\\")) ? \\" checked\\" : \\"\\"
+ (_ssrIncludeBooleanAttr(_ssrLooseEqual(_ctx.baz, \\"foo\\"))) ? \\" checked\\" : \\"\\"
}></div>\`)
}"
`)
export const SSR_RENDER_ATTR = Symbol(`ssrRenderAttr`)
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`ssrRenderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`ssrRenderList`)
+export const SSR_INCLUDE_BOOLEAN_ATTR = Symbol(`ssrIncludeBooleanAttr`)
export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`)
export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
[SSR_RENDER_ATTR]: `ssrRenderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `ssrRenderDynamicAttr`,
[SSR_RENDER_LIST]: `ssrRenderList`,
+ [SSR_INCLUDE_BOOLEAN_ATTR]: `ssrIncludeBooleanAttr`,
[SSR_LOOSE_EQUAL]: `ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
SSR_RENDER_DYNAMIC_ATTR,
SSR_RENDER_ATTRS,
SSR_INTERPOLATE,
- SSR_GET_DYNAMIC_MODEL_PROPS
+ SSR_GET_DYNAMIC_MODEL_PROPS,
+ SSR_INCLUDE_BOOLEAN_ATTR
} from '../runtimeHelpers'
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
if (isBooleanAttr(attrName)) {
openTag.push(
createConditionalExpression(
- value,
+ createCallExpression(
+ context.helper(SSR_INCLUDE_BOOLEAN_ATTR),
+ [value]
+ ),
createSimpleExpression(' ' + attrName, true),
createSimpleExpression('', true),
false /* no newline */
expect(el.getAttribute('readonly')).toBe('')
patchProp(el, 'readonly', true, false)
expect(el.getAttribute('readonly')).toBe(null)
+ patchProp(el, 'readonly', false, '')
+ expect(el.getAttribute('readonly')).toBe('')
+ patchProp(el, 'readonly', '', 0)
+ expect(el.getAttribute('readonly')).toBe(null)
+ patchProp(el, 'readonly', 0, '0')
+ expect(el.getAttribute('readonly')).toBe('')
+ patchProp(el, 'readonly', '0', false)
+ expect(el.getAttribute('readonly')).toBe(null)
+ patchProp(el, 'readonly', false, 1)
+ expect(el.getAttribute('readonly')).toBe('')
+ patchProp(el, 'readonly', 1, undefined)
+ expect(el.getAttribute('readonly')).toBe(null)
})
test('attributes', () => {
expect(el.multiple).toBe(true)
patchProp(el, 'multiple', null, null)
expect(el.multiple).toBe(false)
+ patchProp(el, 'multiple', null, true)
+ expect(el.multiple).toBe(true)
+ patchProp(el, 'multiple', null, 0)
+ expect(el.multiple).toBe(false)
+ patchProp(el, 'multiple', null, '0')
+ expect(el.multiple).toBe(true)
+ patchProp(el, 'multiple', null, false)
+ expect(el.multiple).toBe(false)
+ patchProp(el, 'multiple', null, 1)
+ expect(el.multiple).toBe(true)
+ patchProp(el, 'multiple', null, undefined)
+ expect(el.multiple).toBe(false)
})
test('innerHTML unmount prev children', () => {
-import { isSpecialBooleanAttr, makeMap, NOOP } from '@vue/shared'
+import {
+ includeBooleanAttr,
+ isSpecialBooleanAttr,
+ makeMap,
+ NOOP
+} from '@vue/shared'
import {
compatUtils,
ComponentInternalInstance,
// note we are only checking boolean attributes that don't have a
// corresponding dom prop of the same name here.
const isBoolean = isSpecialBooleanAttr(key)
- if (value == null || (isBoolean && value === false)) {
+ if (value == null || (isBoolean && !includeBooleanAttr(value))) {
el.removeAttribute(key)
} else {
el.setAttribute(key, isBoolean ? '' : value)
// This can come from explicit usage of v-html or innerHTML as a prop in render
import { warn, DeprecationTypes, compatUtils } from '@vue/runtime-core'
+import { includeBooleanAttr } from '@vue/shared'
// functions. The user is responsible for using them with only trusted content.
export function patchDOMProp(
if (value === '' || value == null) {
const type = typeof el[key]
- if (value === '' && type === 'boolean') {
+ if (type === 'boolean') {
// e.g. <select multiple> compiles to { multiple: '' }
- el[key] = true
+ el[key] = includeBooleanAttr(value)
return
} else if (value == null && type === 'string') {
// e.g. <div :id="null">
expect(
ssrRenderAttrs({
checked: true,
- multiple: false
+ multiple: false,
+ readonly: 0,
+ disabled: ''
})
- ).toBe(` checked`) // boolean attr w/ false should be ignored
+ ).toBe(` checked disabled`) // boolean attr w/ false should be ignored
})
test('ignore falsy values', () => {
isOn,
isSSRSafeAttrName,
isBooleanAttr,
+ includeBooleanAttr,
makeMap
} from '@vue/shared'
? key // preserve raw name on custom elements
: propsToAttrMap[key] || key.toLowerCase()
if (isBooleanAttr(attrKey)) {
- return value === false ? `` : ` ${attrKey}`
+ return includeBooleanAttr(value) ? ` ${attrKey}` : ``
} else if (isSSRSafeAttrName(attrKey)) {
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
} else {
export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList } from './helpers/ssrRenderList'
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
+export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared'
// v-model helpers
export {
`checked,muted,multiple,selected`
)
+/**
+ * Boolean attributes should be included if the value is truthy or ''.
+ * e.g. <select multiple> compiles to { multiple: '' }
+ */
+export function includeBooleanAttr(value: unknown): boolean {
+ return !!value || value === ''
+}
+
const unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/
const attrValidationCache: Record<string, boolean> = {}