Skip to content

Commit 487a8df

Browse files
committed
fix(runtime-dom): preserve nullish values for custom element properties
Custom element properties may accept multiple types, so when a nullish value (null/undefined) or empty string is passed, Vue should not coerce it based on the existing property type. Previously, null would be coerced to 0 for number props, '' for string props, and '' would be coerced to true for boolean props on custom elements. close #14209
1 parent 09dec96 commit 487a8df

File tree

2 files changed

+50
-13
lines changed

2 files changed

+50
-13
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ describe('runtime-dom: props patching', () => {
6565
expect(el.value).toBe('foo')
6666
expect(el.setterCalled).toBe(1)
6767
patchProp(el, 'value', null, null)
68-
expect(el.value).toBe('')
68+
// #14209: custom elements should preserve nullish values as-is
69+
expect(el.value).toBe(null)
6970
expect(el.setterCalled).toBe(2)
7071
expect(el.getAttribute('value')).toBe(null)
7172
const obj = {}
@@ -74,6 +75,35 @@ describe('runtime-dom: props patching', () => {
7475
expect(el.setterCalled).toBe(3)
7576
})
7677

78+
// #14209
79+
test('nullish value handling for custom element props', () => {
80+
class TestElement extends HTMLElement {
81+
myNumber = 1
82+
myBool = true
83+
myString = 'hello'
84+
}
85+
window.customElements.define('ce-nullish-test', TestElement)
86+
const el = document.createElement('ce-nullish-test') as TestElement
87+
88+
// setting null on a number prop should preserve null, not coerce to 0
89+
patchProp(el, '.myNumber', null, null)
90+
expect(el.myNumber).toBe(null)
91+
expect(el.getAttribute('myNumber')).toBe(null)
92+
93+
// setting null on a boolean prop should preserve null, not coerce to false
94+
patchProp(el, '.myBool', null, null)
95+
expect(el.myBool).toBe(null)
96+
97+
// setting null on a string prop should preserve null, not coerce to ''
98+
patchProp(el, '.myString', null, null)
99+
expect(el.myString).toBe(null)
100+
101+
// setting '' on a boolean prop should preserve '', not coerce to true
102+
el.myBool = true
103+
patchProp(el, '.myBool', null, '')
104+
expect(el.myBool).toBe('')
105+
})
106+
77107
// For <input type="text">, setting el.value won't create a `value` attribute
78108
// so we need to add tests for other elements
79109
test('value for non-text input', () => {

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,25 @@ export function patchDOMProp(
5656

5757
let needRemove = false
5858
if (value === '' || value == null) {
59-
const type = typeof el[key]
60-
if (type === 'boolean') {
61-
// e.g. <select multiple> compiles to { multiple: '' }
62-
value = includeBooleanAttr(value)
63-
} else if (value == null && type === 'string') {
64-
// e.g. <div :id="null">
65-
value = ''
66-
needRemove = true
67-
} else if (type === 'number') {
68-
// e.g. <img :width="null">
69-
value = 0
70-
needRemove = true
59+
if (tag.includes('-')) {
60+
// #14209 custom element properties may accept multiple types
61+
// so we avoid coercing the value based on the existing property type
62+
// and preserve the value as-is
63+
needRemove = value == null
64+
} else {
65+
const type = typeof el[key]
66+
if (type === 'boolean') {
67+
// e.g. <select multiple> compiles to { multiple: '' }
68+
value = includeBooleanAttr(value)
69+
} else if (value == null && type === 'string') {
70+
// e.g. <div :id="null">
71+
value = ''
72+
needRemove = true
73+
} else if (type === 'number') {
74+
// e.g. <img :width="null">
75+
value = 0
76+
needRemove = true
77+
}
7178
}
7279
} else {
7380
if (

0 commit comments

Comments
 (0)