Skip to content

Commit

Permalink
fix(custom-elements): fix number prop casting
Browse files Browse the repository at this point in the history
fix #4370, close #4393
  • Loading branch information
yyx990803 committed Sep 16, 2021
1 parent 5bd0ac6 commit 0cfa211
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 17 deletions.
50 changes: 42 additions & 8 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,26 +136,40 @@ describe('defineCustomElement', () => {
const E = defineCustomElement({
props: {
foo: Number,
bar: Boolean
bar: Boolean,
baz: String
},
render() {
return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
' '
)
return [
this.foo,
typeof this.foo,
this.bar,
typeof this.bar,
this.baz,
typeof this.baz
].join(' ')
}
})
customElements.define('my-el-props-cast', E)
container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
container.innerHTML = `<my-el-props-cast foo="1" baz="12345"></my-el-props-cast>`
const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
expect(e.shadowRoot!.innerHTML).toBe(
`1 number false boolean 12345 string`
)

e.setAttribute('bar', '')
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)

e.setAttribute('foo', '2e1')
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
expect(e.shadowRoot!.innerHTML).toBe(
`20 number true boolean 12345 string`
)

e.setAttribute('baz', '2e1')
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
})

test('handling properties set before upgrading', () => {
Expand Down Expand Up @@ -392,5 +406,25 @@ describe('defineCustomElement', () => {
e2.msg = 'hello'
expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
})

test('Number prop casting before resolve', async () => {
const E = defineCustomElement(
defineAsyncComponent(() => {
return Promise.resolve({
props: { n: Number },
render(this: any) {
return h('div', this.n + ',' + typeof this.n)
}
})
})
)
customElements.define('my-el-async-3', E)
container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`

await new Promise(r => setTimeout(r))

const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
})
})
})
44 changes: 35 additions & 9 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export class VueElement extends BaseClass {

private _connected = false
private _resolved = false
private _numberProps: Record<string, true> | null = null
private _styles?: HTMLStyleElement[]

constructor(
Expand All @@ -179,19 +180,18 @@ export class VueElement extends BaseClass {
this._setAttr(this.attributes[i].name)
}
// watch future attr changes
const observer = new MutationObserver(mutations => {
new MutationObserver(mutations => {
for (const m of mutations) {
this._setAttr(m.attributeName!)
}
})
observer.observe(this, { attributes: true })
}).observe(this, { attributes: true })
}

connectedCallback() {
this._connected = true
if (!this._instance) {
this._resolveDef()
render(this._createVNode(), this.shadowRoot!)
this._update()
}
}

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

const resolve = (def: InnerComponentDef) => {
this._resolved = true
const { props, styles } = def
const hasOptions = !isArray(props)
const rawKeys = props ? (hasOptions ? Object.keys(props) : props) : []

// cast Number-type props set before resolve
let numberProps
if (hasOptions) {
for (const key in this._props) {
const opt = props[key]
if (opt === Number || (opt && opt.type === Number)) {
this._props[key] = toNumber(this._props[key])
;(numberProps || (numberProps = Object.create(null)))[key] = true
}
}
}
if (numberProps) {
this._numberProps = numberProps
this._update()
}

// check if there are props set pre-upgrade or connect
for (const key of Object.keys(this)) {
if (key[0] !== '_') {
this._setProp(key, this[key as keyof this])
}
}
const { props, styles } = def
// defining getter/setters on prototype
const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
for (const key of rawKeys.map(camelize)) {
Object.defineProperty(this, key, {
get() {
Expand All @@ -246,7 +264,11 @@ export class VueElement extends BaseClass {
}

protected _setAttr(key: string) {
this._setProp(camelize(key), toNumber(this.getAttribute(key)), false)
let value = this.getAttribute(key)
if (this._numberProps && this._numberProps[key]) {
value = toNumber(value)
}
this._setProp(camelize(key), value, false)
}

/**
Expand All @@ -263,7 +285,7 @@ export class VueElement extends BaseClass {
if (val !== this._props[key]) {
this._props[key] = val
if (this._instance) {
render(this._createVNode(), this.shadowRoot!)
this._update()
}
// reflect
if (shouldReflect) {
Expand All @@ -278,6 +300,10 @@ export class VueElement extends BaseClass {
}
}

private _update() {
render(this._createVNode(), this.shadowRoot!)
}

private _createVNode(): VNode<any, any> {
const vnode = createVNode(this._def, extend({}, this._props))
if (!this._instance) {
Expand All @@ -298,7 +324,7 @@ export class VueElement extends BaseClass {
if (!(this._def as ComponentOptions).__asyncLoader) {
// reload
this._instance = null
render(this._createVNode(), this.shadowRoot!)
this._update()
}
}
}
Expand Down

0 comments on commit 0cfa211

Please sign in to comment.