Skip to content

Commit

Permalink
feat: custom element reflection, casting and edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jul 16, 2021
1 parent bf4893c commit 00f0b3c
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 34 deletions.
55 changes: 54 additions & 1 deletion packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,66 @@ describe('defineCustomElement', () => {
container.appendChild(e)
expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')

// reflect
// should reflect primitive value
expect(e.getAttribute('foo')).toBe('one')
// should not reflect rich data
expect(e.hasAttribute('bar')).toBe(false)

e.foo = 'three'
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
expect(e.getAttribute('foo')).toBe('three')

e.foo = null
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
expect(e.hasAttribute('foo')).toBe(false)

e.bazQux = 'four'
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>four</div>')
expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
expect(e.getAttribute('baz-qux')).toBe('four')
})

test('attribute -> prop type casting', async () => {
const E = defineCustomElement({
props: {
foo: Number,
bar: Boolean
},
render() {
return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
' '
)
}
})
customElements.define('my-el-props-cast', E)
container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
const e = container.childNodes[0] as VueElement
expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)

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

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

test('handling properties set before upgrading', () => {
const E = defineCustomElement({
props: ['foo'],
render() {
return `foo: ${this.foo}`
}
})
const el = document.createElement('my-el-upgrade') as any
el.foo = 'hello'
container.appendChild(el)
customElements.define('my-el-upgrade', E)
expect(el.shadowRoot.innerHTML).toBe(`foo: hello`)
})
})

Expand Down
95 changes: 62 additions & 33 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
nextTick,
warn
} from '@vue/runtime-core'
import { camelize, hyphenate, isArray } from '@vue/shared'
import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
import { hydrate, render } from '.'

type VueElementConstructor<P = {}> = {
Expand Down Expand Up @@ -134,7 +134,7 @@ export function defineCustomElement(
return attrKeys
}
constructor() {
super(Comp, attrKeys, hydate)
super(Comp, attrKeys, propKeys, hydate)
}
}

Expand Down Expand Up @@ -173,12 +173,13 @@ export class VueElement extends HTMLElement {

constructor(
private _def: Component,
private _attrs: string[],
private _attrKeys: string[],
private _propKeys: string[],
hydrate?: RootHydrateFunction
) {
super()
if (this.shadowRoot && hydrate) {
hydrate(this._initVNode(), this.shadowRoot)
hydrate(this._createVNode(), this.shadowRoot)
} else {
if (__DEV__ && this.shadowRoot) {
warn(
Expand All @@ -191,15 +192,23 @@ export class VueElement extends HTMLElement {
}

attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
if (this._attrs.includes(name)) {
this._setProp(camelize(name), newValue)
if (this._attrKeys.includes(name)) {
this._setProp(camelize(name), toNumber(newValue), false)
}
}

connectedCallback() {
this._connected = true
if (!this._instance) {
render(this._initVNode(), this.shadowRoot!)
// check if there are props set pre-upgrade
for (const key of this._propKeys) {
if (this.hasOwnProperty(key)) {
const value = (this as any)[key]
delete (this as any)[key]
this._setProp(key, value)
}
}
render(this._createVNode(), this.shadowRoot!)
}
}

Expand All @@ -213,41 +222,61 @@ export class VueElement extends HTMLElement {
})
}

/**
* @internal
*/
protected _getProp(key: string) {
return this._props[key]
}

protected _setProp(key: string, val: any) {
const oldValue = this._props[key]
this._props[key] = val
if (this._instance && val !== oldValue) {
this._instance.props[key] = val
/**
* @internal
*/
protected _setProp(key: string, val: any, shouldReflect = true) {
if (val !== this._props[key]) {
this._props[key] = val
if (this._instance) {
render(this._createVNode(), this.shadowRoot!)
}
// reflect
if (shouldReflect) {
if (val === true) {
this.setAttribute(hyphenate(key), '')
} else if (typeof val === 'string' || typeof val === 'number') {
this.setAttribute(hyphenate(key), val + '')
} else if (!val) {
this.removeAttribute(hyphenate(key))
}
}
}
}

protected _initVNode(): VNode<any, any> {
const vnode = createVNode(this._def, this._props)
vnode.ce = instance => {
this._instance = instance
instance.isCE = true
private _createVNode(): VNode<any, any> {
const vnode = createVNode(this._def, extend({}, this._props))
if (!this._instance) {
vnode.ce = instance => {
this._instance = instance
instance.isCE = true

// intercept emit
instance.emit = (event: string, ...args: any[]) => {
this.dispatchEvent(
new CustomEvent(event, {
detail: args
})
)
}
// intercept emit
instance.emit = (event: string, ...args: any[]) => {
this.dispatchEvent(
new CustomEvent(event, {
detail: args
})
)
}

// locate nearest Vue custom element parent for provide/inject
let parent: Node | null = this
while (
(parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
) {
if (parent instanceof VueElement) {
instance.parent = parent._instance
break
// locate nearest Vue custom element parent for provide/inject
let parent: Node | null = this
while (
(parent =
parent && (parent.parentNode || (parent as ShadowRoot).host))
) {
if (parent instanceof VueElement) {
instance.parent = parent._instance
break
}
}
}
}
Expand Down

0 comments on commit 00f0b3c

Please sign in to comment.