]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-dom): consistently remove boolean attributes for falsy values (#4348)
authorskirtle <65301168+skirtles-code@users.noreply.github.com>
Mon, 16 Aug 2021 22:18:36 +0000 (23:18 +0100)
committerGitHub <noreply@github.com>
Mon, 16 Aug 2021 22:18:36 +0000 (18:18 -0400)
12 files changed:
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/src/runtimeHelpers.ts
packages/compiler-ssr/src/transforms/ssrTransformElement.ts
packages/runtime-dom/__tests__/patchAttrs.spec.ts
packages/runtime-dom/__tests__/patchProps.spec.ts
packages/runtime-dom/src/modules/attrs.ts
packages/runtime-dom/src/modules/props.ts
packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts
packages/server-renderer/src/helpers/ssrRenderAttrs.ts
packages/server-renderer/src/index.ts
packages/shared/src/domAttrConfig.ts

index d77b607a296d98c32b8c35a427e38bf5fece720d..b7de9f69c3146df6ed68e05b2e6f2ac22440c11c 100644 (file)
@@ -177,7 +177,7 @@ describe('ssr: element', () => {
       expect(getCompiledString(`<input type="checkbox" :checked="checked">`))
         .toMatchInlineSnapshot(`
         "\`<input type=\\"checkbox\\"\${
-            (_ctx.checked) ? \\" checked\\" : \\"\\"
+            (_ssrIncludeBooleanAttr(_ctx.checked)) ? \\" checked\\" : \\"\\"
           }>\`"
       `)
     })
index 21ce80a00bd52bdc006058f73ce31346cecd4a6c..c6f57e0708cd846ea7de68140e6d71bf55bcc130 100644 (file)
@@ -37,13 +37,13 @@ describe('ssr: v-model', () => {
     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>\`)
       }"
     `)
@@ -52,15 +52,15 @@ describe('ssr: v-model', () => {
   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>\`)
       }"
     `)
@@ -69,15 +69,15 @@ describe('ssr: v-model', () => {
       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>\`)
       }"
     `)
@@ -87,13 +87,13 @@ describe('ssr: v-model', () => {
         `<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>\`)
       }"
     `)
@@ -103,13 +103,13 @@ describe('ssr: v-model', () => {
         `<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>\`)
       }"
     `)
index 2aa93c0bf1589ba7d5c0b582a69b23573be7e12f..9be6c610a93c4c36c557a1a6c97552868bd3ae78 100644 (file)
@@ -10,6 +10,7 @@ export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
 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`)
@@ -28,6 +29,7 @@ export const ssrHelpers = {
   [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`,
index 861446d562db141e8888d9764ec6ac9f04eb952f..c9515d27baa4e05b2e0f6a1437246d1ad636c627 100644 (file)
@@ -43,7 +43,8 @@ import {
   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'
 
@@ -237,7 +238,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
                   if (isBooleanAttr(attrName)) {
                     openTag.push(
                       createConditionalExpression(
-                        value,
+                        createCallExpression(
+                          context.helper(SSR_INCLUDE_BOOLEAN_ATTR),
+                          [value]
+                        ),
                         createSimpleExpression(' ' + attrName, true),
                         createSimpleExpression('', true),
                         false /* no newline */
index ca48c10fea2d28bd941f1404499527b96618b812..b78dd44c634ea917c3d46ac5763552e9c402977f 100644 (file)
@@ -23,6 +23,18 @@ describe('runtime-dom: attrs patching', () => {
     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', () => {
index 3d00e51ccfed1a714a1939a45a3ffcd6f3fd34cb..e1ee7928c845df22bd345c277325882f4ba066ec 100644 (file)
@@ -43,6 +43,18 @@ describe('runtime-dom: props patching', () => {
     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', () => {
index fa217800525096a47ad9f76e8178cdb0dfe55ba5..a80936345ed130ad76b3d6ccf90c9a2b55caf475 100644 (file)
@@ -1,4 +1,9 @@
-import { isSpecialBooleanAttr, makeMap, NOOP } from '@vue/shared'
+import {
+  includeBooleanAttr,
+  isSpecialBooleanAttr,
+  makeMap,
+  NOOP
+} from '@vue/shared'
 import {
   compatUtils,
   ComponentInternalInstance,
@@ -28,7 +33,7 @@ export function patchAttr(
     // 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)
index 36bb0c11ea3a79984cbdeeea743b47f1732d1ba2..aff6909f8864b23b2bd88808725a06725e0a6eb9 100644 (file)
@@ -3,6 +3,7 @@
 // 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(
@@ -41,9 +42,9 @@ 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">
index 270c910d5aae113440802d8a71430aa8173b3552..f3bcf8d3b93d84a7b723cc0edb35ff67da5b8d75 100644 (file)
@@ -50,9 +50,11 @@ describe('ssr: renderAttrs', () => {
     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', () => {
index c06def0a992e5b3a2965e8d552823f45f7a7de7c..8814f1fd735b612c838423878a3f7b60a73b9845 100644 (file)
@@ -7,6 +7,7 @@ import {
   isOn,
   isSSRSafeAttrName,
   isBooleanAttr,
+  includeBooleanAttr,
   makeMap
 } from '@vue/shared'
 
@@ -52,7 +53,7 @@ export function ssrRenderDynamicAttr(
       ? 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 {
index e5a9f650e478bf271fe1f31859284cd41e7f9711..352c263fffd534c8fcb18e588b21fd235b0b0642 100644 (file)
@@ -27,6 +27,7 @@ export {
 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 {
index 93f1b36d327968e72f262dbc5c6e1f0149bdd253..46607c55b0792981094cf4f866b516c181a826ab 100644 (file)
@@ -24,6 +24,14 @@ export const isBooleanAttr = /*#__PURE__*/ makeMap(
     `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> = {}