mirror of https://github.com/vuejs/core.git
fix(ssr): render `hidden` correctly
This commit is contained in:
parent
f6e84af30a
commit
1d1ae00d87
|
@ -2086,6 +2086,24 @@ describe('SSR hydration', () => {
|
||||||
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('combined boolean/string attribute', () => {
|
||||||
|
mountWithHydration(`<div></div>`, () => h('div', { hidden: false }))
|
||||||
|
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||||
|
|
||||||
|
mountWithHydration(`<div hidden></div>`, () => h('div', { hidden: true }))
|
||||||
|
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||||
|
|
||||||
|
mountWithHydration(`<div hidden="until-found"></div>`, () =>
|
||||||
|
h('div', { hidden: 'until-found' }),
|
||||||
|
)
|
||||||
|
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||||
|
|
||||||
|
mountWithHydration(`<div hidden=""></div>`, () =>
|
||||||
|
h('div', { hidden: true }),
|
||||||
|
)
|
||||||
|
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
|
||||||
test('client value is null or undefined', () => {
|
test('client value is null or undefined', () => {
|
||||||
mountWithHydration(`<div></div>`, () =>
|
mountWithHydration(`<div></div>`, () =>
|
||||||
h('div', { draggable: undefined }),
|
h('div', { draggable: undefined }),
|
||||||
|
|
|
@ -21,9 +21,11 @@ import {
|
||||||
getEscapedCssVarName,
|
getEscapedCssVarName,
|
||||||
includeBooleanAttr,
|
includeBooleanAttr,
|
||||||
isBooleanAttr,
|
isBooleanAttr,
|
||||||
|
isBooleanAttrValue,
|
||||||
isKnownHtmlAttr,
|
isKnownHtmlAttr,
|
||||||
isKnownSvgAttr,
|
isKnownSvgAttr,
|
||||||
isOn,
|
isOn,
|
||||||
|
isOverloadedBooleanAttr,
|
||||||
isRenderableAttrValue,
|
isRenderableAttrValue,
|
||||||
isReservedProp,
|
isReservedProp,
|
||||||
isString,
|
isString,
|
||||||
|
@ -835,7 +837,10 @@ function propHasMismatch(
|
||||||
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
|
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
|
||||||
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
|
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
|
||||||
) {
|
) {
|
||||||
if (isBooleanAttr(key)) {
|
if (
|
||||||
|
isBooleanAttr(key) ||
|
||||||
|
(isOverloadedBooleanAttr(key) && isBooleanAttrValue(clientValue))
|
||||||
|
) {
|
||||||
actual = el.hasAttribute(key)
|
actual = el.hasAttribute(key)
|
||||||
expected = includeBooleanAttr(clientValue)
|
expected = includeBooleanAttr(clientValue)
|
||||||
} else if (clientValue == null) {
|
} else if (clientValue == null) {
|
||||||
|
|
|
@ -264,7 +264,7 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
|
||||||
contextmenu?: string
|
contextmenu?: string
|
||||||
dir?: string
|
dir?: string
|
||||||
draggable?: Booleanish
|
draggable?: Booleanish
|
||||||
hidden?: Booleanish | '' | 'hidden' | 'until-found'
|
hidden?: boolean | '' | 'hidden' | 'until-found'
|
||||||
id?: string
|
id?: string
|
||||||
inert?: Booleanish
|
inert?: Booleanish
|
||||||
lang?: string
|
lang?: string
|
||||||
|
|
|
@ -55,6 +55,15 @@ describe('ssr: renderAttrs', () => {
|
||||||
).toBe(` checked disabled`) // boolean attr w/ false should be ignored
|
).toBe(` checked disabled`) // boolean attr w/ false should be ignored
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('combined boolean/string attribute', () => {
|
||||||
|
expect(ssrRenderAttrs({ hidden: true })).toBe(` hidden`)
|
||||||
|
expect(ssrRenderAttrs({ disabled: true, hidden: false })).toBe(` disabled`)
|
||||||
|
expect(ssrRenderAttrs({ hidden: 'until-found' })).toBe(
|
||||||
|
` hidden="until-found"`,
|
||||||
|
)
|
||||||
|
expect(ssrRenderAttrs({ hidden: '' })).toBe(` hidden`)
|
||||||
|
})
|
||||||
|
|
||||||
test('ignore falsy values', () => {
|
test('ignore falsy values', () => {
|
||||||
expect(
|
expect(
|
||||||
ssrRenderAttrs({
|
ssrRenderAttrs({
|
||||||
|
@ -122,6 +131,13 @@ describe('ssr: renderAttr', () => {
|
||||||
` foo="${escapeHtml(`<script>`)}"`,
|
` foo="${escapeHtml(`<script>`)}"`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('combined boolean/string attribute', () => {
|
||||||
|
expect(ssrRenderAttr('hidden', true)).toBe(` hidden`)
|
||||||
|
expect(ssrRenderAttr('hidden', false)).toBe('')
|
||||||
|
expect(ssrRenderAttr('hidden', 'until-found')).toBe(` hidden="until-found"`)
|
||||||
|
expect(ssrRenderAttr('hidden', '')).toBe(` hidden`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('ssr: renderClass', () => {
|
describe('ssr: renderClass', () => {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import {
|
import {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
isBooleanAttrValue,
|
||||||
|
isOverloadedBooleanAttr,
|
||||||
isRenderableAttrValue,
|
isRenderableAttrValue,
|
||||||
isSVGTag,
|
isSVGTag,
|
||||||
stringifyStyle,
|
stringifyStyle,
|
||||||
|
@ -61,7 +63,10 @@ export function ssrRenderDynamicAttr(
|
||||||
tag && (tag.indexOf('-') > 0 || isSVGTag(tag))
|
tag && (tag.indexOf('-') > 0 || isSVGTag(tag))
|
||||||
? key // preserve raw name on custom elements and svg
|
? key // preserve raw name on custom elements and svg
|
||||||
: propsToAttrMap[key] || key.toLowerCase()
|
: propsToAttrMap[key] || key.toLowerCase()
|
||||||
if (isBooleanAttr(attrKey)) {
|
if (
|
||||||
|
isBooleanAttr(attrKey) ||
|
||||||
|
(isOverloadedBooleanAttr(attrKey) && isBooleanAttrValue(value))
|
||||||
|
) {
|
||||||
return includeBooleanAttr(value) ? ` ${attrKey}` : ``
|
return includeBooleanAttr(value) ? ` ${attrKey}` : ``
|
||||||
} else if (isSSRSafeAttrName(attrKey)) {
|
} else if (isSSRSafeAttrName(attrKey)) {
|
||||||
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
|
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
|
||||||
|
@ -79,6 +84,9 @@ export function ssrRenderAttr(key: string, value: unknown): string {
|
||||||
if (!isRenderableAttrValue(value)) {
|
if (!isRenderableAttrValue(value)) {
|
||||||
return ``
|
return ``
|
||||||
}
|
}
|
||||||
|
if (isOverloadedBooleanAttr(key) && isBooleanAttrValue(value)) {
|
||||||
|
return includeBooleanAttr(value) ? ` ${key}` : ``
|
||||||
|
}
|
||||||
return ` ${key}="${escapeHtml(value)}"`
|
return ` ${key}="${escapeHtml(value)}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const isSpecialBooleanAttr: (key: string) => boolean =
|
||||||
*/
|
*/
|
||||||
export const isBooleanAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
|
export const isBooleanAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
|
||||||
specialBooleanAttrs +
|
specialBooleanAttrs +
|
||||||
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
|
`,async,autofocus,autoplay,controls,default,defer,disabled,` +
|
||||||
`inert,loop,open,required,reversed,scoped,seamless,` +
|
`inert,loop,open,required,reversed,scoped,seamless,` +
|
||||||
`checked,muted,multiple,selected`,
|
`checked,muted,multiple,selected`,
|
||||||
)
|
)
|
||||||
|
@ -152,3 +152,16 @@ export function isRenderableAttrValue(value: unknown): boolean {
|
||||||
const type = typeof value
|
const type = typeof value
|
||||||
return type === 'string' || type === 'number' || type === 'boolean'
|
return type === 'string' || type === 'number' || type === 'boolean'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An attribute that can be used as a flag as well as with a value.
|
||||||
|
* When `true`, it should be present (set either to an empty string or its name).
|
||||||
|
* When `false`, it should be omitted.
|
||||||
|
* For any other value, should be present with that value.
|
||||||
|
*/
|
||||||
|
export const isOverloadedBooleanAttr: (key: string) => boolean =
|
||||||
|
/*@__PURE__*/ makeMap('hidden')
|
||||||
|
|
||||||
|
export function isBooleanAttrValue(value: unknown): boolean {
|
||||||
|
return typeof value === 'boolean' || value === ''
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue