Skip to content

Commit 0cfa211

Browse files
committed
fix(custom-elements): fix number prop casting
fix #4370, close #4393
1 parent 5bd0ac6 commit 0cfa211

File tree

2 files changed

+77
-17
lines changed

2 files changed

+77
-17
lines changed

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

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,26 +136,40 @@ describe('defineCustomElement', () => {
136136
const E = defineCustomElement({
137137
props: {
138138
foo: Number,
139-
bar: Boolean
139+
bar: Boolean,
140+
baz: String
140141
},
141142
render() {
142-
return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
143-
' '
144-
)
143+
return [
144+
this.foo,
145+
typeof this.foo,
146+
this.bar,
147+
typeof this.bar,
148+
this.baz,
149+
typeof this.baz
150+
].join(' ')
145151
}
146152
})
147153
customElements.define('my-el-props-cast', E)
148-
container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
154+
container.innerHTML = `<my-el-props-cast foo="1" baz="12345"></my-el-props-cast>`
149155
const e = container.childNodes[0] as VueElement
150-
expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
156+
expect(e.shadowRoot!.innerHTML).toBe(
157+
`1 number false boolean 12345 string`
158+
)
151159

152160
e.setAttribute('bar', '')
153161
await nextTick()
154-
expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
162+
expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
155163

156164
e.setAttribute('foo', '2e1')
157165
await nextTick()
158-
expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
166+
expect(e.shadowRoot!.innerHTML).toBe(
167+
`20 number true boolean 12345 string`
168+
)
169+
170+
e.setAttribute('baz', '2e1')
171+
await nextTick()
172+
expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
159173
})
160174

161175
test('handling properties set before upgrading', () => {
@@ -392,5 +406,25 @@ describe('defineCustomElement', () => {
392406
e2.msg = 'hello'
393407
expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
394408
})
409+
410+
test('Number prop casting before resolve', async () => {
411+
const E = defineCustomElement(
412+
defineAsyncComponent(() => {
413+
return Promise.resolve({
414+
props: { n: Number },
415+
render(this: any) {
416+
return h('div', this.n + ',' + typeof this.n)
417+
}
418+
})
419+
})
420+
)
421+
customElements.define('my-el-async-3', E)
422+
container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`
423+
424+
await new Promise(r => setTimeout(r))
425+
426+
const e = container.childNodes[0] as VueElement
427+
expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
428+
})
395429
})
396430
})

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export class VueElement extends BaseClass {
154154

155155
private _connected = false
156156
private _resolved = false
157+
private _numberProps: Record<string, true> | null = null
157158
private _styles?: HTMLStyleElement[]
158159

159160
constructor(
@@ -179,19 +180,18 @@ export class VueElement extends BaseClass {
179180
this._setAttr(this.attributes[i].name)
180181
}
181182
// watch future attr changes
182-
const observer = new MutationObserver(mutations => {
183+
new MutationObserver(mutations => {
183184
for (const m of mutations) {
184185
this._setAttr(m.attributeName!)
185186
}
186-
})
187-
observer.observe(this, { attributes: true })
187+
}).observe(this, { attributes: true })
188188
}
189189

190190
connectedCallback() {
191191
this._connected = true
192192
if (!this._instance) {
193193
this._resolveDef()
194-
render(this._createVNode(), this.shadowRoot!)
194+
this._update()
195195
}
196196
}
197197

@@ -215,15 +215,33 @@ export class VueElement extends BaseClass {
215215

216216
const resolve = (def: InnerComponentDef) => {
217217
this._resolved = true
218+
const { props, styles } = def
219+
const hasOptions = !isArray(props)
220+
const rawKeys = props ? (hasOptions ? Object.keys(props) : props) : []
221+
222+
// cast Number-type props set before resolve
223+
let numberProps
224+
if (hasOptions) {
225+
for (const key in this._props) {
226+
const opt = props[key]
227+
if (opt === Number || (opt && opt.type === Number)) {
228+
this._props[key] = toNumber(this._props[key])
229+
;(numberProps || (numberProps = Object.create(null)))[key] = true
230+
}
231+
}
232+
}
233+
if (numberProps) {
234+
this._numberProps = numberProps
235+
this._update()
236+
}
237+
218238
// check if there are props set pre-upgrade or connect
219239
for (const key of Object.keys(this)) {
220240
if (key[0] !== '_') {
221241
this._setProp(key, this[key as keyof this])
222242
}
223243
}
224-
const { props, styles } = def
225244
// defining getter/setters on prototype
226-
const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
227245
for (const key of rawKeys.map(camelize)) {
228246
Object.defineProperty(this, key, {
229247
get() {
@@ -246,7 +264,11 @@ export class VueElement extends BaseClass {
246264
}
247265

248266
protected _setAttr(key: string) {
249-
this._setProp(camelize(key), toNumber(this.getAttribute(key)), false)
267+
let value = this.getAttribute(key)
268+
if (this._numberProps && this._numberProps[key]) {
269+
value = toNumber(value)
270+
}
271+
this._setProp(camelize(key), value, false)
250272
}
251273

252274
/**
@@ -263,7 +285,7 @@ export class VueElement extends BaseClass {
263285
if (val !== this._props[key]) {
264286
this._props[key] = val
265287
if (this._instance) {
266-
render(this._createVNode(), this.shadowRoot!)
288+
this._update()
267289
}
268290
// reflect
269291
if (shouldReflect) {
@@ -278,6 +300,10 @@ export class VueElement extends BaseClass {
278300
}
279301
}
280302

303+
private _update() {
304+
render(this._createVNode(), this.shadowRoot!)
305+
}
306+
281307
private _createVNode(): VNode<any, any> {
282308
const vnode = createVNode(this._def, extend({}, this._props))
283309
if (!this._instance) {
@@ -298,7 +324,7 @@ export class VueElement extends BaseClass {
298324
if (!(this._def as ComponentOptions).__asyncLoader) {
299325
// reload
300326
this._instance = null
301-
render(this._createVNode(), this.shadowRoot!)
327+
this._update()
302328
}
303329
}
304330
}

0 commit comments

Comments
 (0)