feat(runtime-dom): defineCustomElement
This commit is contained in:
parent
42ace9577d
commit
8610e1c9e2
@ -279,12 +279,15 @@ export interface ComponentInternalInstance {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
emitsOptions: ObjectEmitsOptions | null
|
emitsOptions: ObjectEmitsOptions | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* resolved inheritAttrs options
|
* resolved inheritAttrs options
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
inheritAttrs?: boolean
|
inheritAttrs?: boolean
|
||||||
|
/**
|
||||||
|
* is custom element?
|
||||||
|
*/
|
||||||
|
isCE?: boolean
|
||||||
|
|
||||||
// the rest are only for stateful components ---------------------------------
|
// the rest are only for stateful components ---------------------------------
|
||||||
|
|
||||||
@ -519,6 +522,11 @@ export function createComponentInstance(
|
|||||||
instance.root = parent ? parent.root : instance
|
instance.root = parent ? parent.root : instance
|
||||||
instance.emit = emit.bind(null, instance)
|
instance.emit = emit.bind(null, instance)
|
||||||
|
|
||||||
|
// apply custom element special handling
|
||||||
|
if (vnode.ce) {
|
||||||
|
vnode.ce(instance)
|
||||||
|
}
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Data } from '../component'
|
import { Data } from '../component'
|
||||||
import { Slots, RawSlots } from '../componentSlots'
|
import { Slots, RawSlots } from '../componentSlots'
|
||||||
import { ContextualRenderFn } from '../componentRenderContext'
|
import {
|
||||||
|
ContextualRenderFn,
|
||||||
|
currentRenderingInstance
|
||||||
|
} from '../componentRenderContext'
|
||||||
import { Comment, isVNode } from '../vnode'
|
import { Comment, isVNode } from '../vnode'
|
||||||
import {
|
import {
|
||||||
VNodeArrayChildren,
|
VNodeArrayChildren,
|
||||||
@ -11,6 +14,7 @@ import {
|
|||||||
} from '../vnode'
|
} from '../vnode'
|
||||||
import { PatchFlags, SlotFlags } from '@vue/shared'
|
import { PatchFlags, SlotFlags } from '@vue/shared'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
|
import { createVNode } from '@vue/runtime-core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiler runtime helper for rendering `<slot/>`
|
* Compiler runtime helper for rendering `<slot/>`
|
||||||
@ -25,6 +29,14 @@ export function renderSlot(
|
|||||||
fallback?: () => VNodeArrayChildren,
|
fallback?: () => VNodeArrayChildren,
|
||||||
noSlotted?: boolean
|
noSlotted?: boolean
|
||||||
): VNode {
|
): VNode {
|
||||||
|
if (currentRenderingInstance!.isCE) {
|
||||||
|
return createVNode(
|
||||||
|
'slot',
|
||||||
|
name === 'default' ? null : { name },
|
||||||
|
fallback && fallback()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let slot = slots[name]
|
let slot = slots[name]
|
||||||
|
|
||||||
if (__DEV__ && slot && slot.length > 1) {
|
if (__DEV__ && slot && slot.length > 1) {
|
||||||
|
@ -25,7 +25,7 @@ import { isAsyncWrapper } from './apiAsyncComponent'
|
|||||||
|
|
||||||
export type RootHydrateFunction = (
|
export type RootHydrateFunction = (
|
||||||
vnode: VNode<Node, Element>,
|
vnode: VNode<Node, Element>,
|
||||||
container: Element
|
container: Element | ShadowRoot
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
const enum DOMNodeTypes {
|
const enum DOMNodeTypes {
|
||||||
|
@ -93,7 +93,7 @@ export interface Renderer<HostElement = RendererElement> {
|
|||||||
createApp: CreateAppFunction<HostElement>
|
createApp: CreateAppFunction<HostElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HydrationRenderer extends Renderer<Element> {
|
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
||||||
hydrate: RootHydrateFunction
|
hydrate: RootHydrateFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,11 +136,6 @@ export interface VNode<
|
|||||||
*/
|
*/
|
||||||
[ReactiveFlags.SKIP]: true
|
[ReactiveFlags.SKIP]: true
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal __COMPAT__ only
|
|
||||||
*/
|
|
||||||
isCompatRoot?: true
|
|
||||||
|
|
||||||
type: VNodeTypes
|
type: VNodeTypes
|
||||||
props: (VNodeProps & ExtraProps) | null
|
props: (VNodeProps & ExtraProps) | null
|
||||||
key: string | number | null
|
key: string | number | null
|
||||||
@ -155,6 +150,7 @@ export interface VNode<
|
|||||||
* - Slot fragment vnodes with :slotted SFC styles.
|
* - Slot fragment vnodes with :slotted SFC styles.
|
||||||
* - Component vnodes (during patch/hydration) so that its root node can
|
* - Component vnodes (during patch/hydration) so that its root node can
|
||||||
* inherit the component's slotScopeIds
|
* inherit the component's slotScopeIds
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
slotScopeIds: string[] | null
|
slotScopeIds: string[] | null
|
||||||
children: VNodeNormalizedChildren
|
children: VNodeNormalizedChildren
|
||||||
@ -167,24 +163,50 @@ export interface VNode<
|
|||||||
anchor: HostNode | null // fragment anchor
|
anchor: HostNode | null // fragment anchor
|
||||||
target: HostElement | null // teleport target
|
target: HostElement | null // teleport target
|
||||||
targetAnchor: HostNode | null // teleport target anchor
|
targetAnchor: HostNode | null // teleport target anchor
|
||||||
staticCount?: number // number of elements contained in a static vnode
|
/**
|
||||||
|
* number of elements contained in a static vnode
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
staticCount: number
|
||||||
|
|
||||||
// suspense
|
// suspense
|
||||||
suspense: SuspenseBoundary | null
|
suspense: SuspenseBoundary | null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
ssContent: VNode | null
|
ssContent: VNode | null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
ssFallback: VNode | null
|
ssFallback: VNode | null
|
||||||
|
|
||||||
// optimization only
|
// optimization only
|
||||||
shapeFlag: number
|
shapeFlag: number
|
||||||
patchFlag: number
|
patchFlag: number
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
dynamicProps: string[] | null
|
dynamicProps: string[] | null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
dynamicChildren: VNode[] | null
|
dynamicChildren: VNode[] | null
|
||||||
|
|
||||||
// application root node only
|
// application root node only
|
||||||
appContext: AppContext | null
|
appContext: AppContext | null
|
||||||
|
|
||||||
// v-for memo
|
/**
|
||||||
|
* @internal attached by v-memo
|
||||||
|
*/
|
||||||
memo?: any[]
|
memo?: any[]
|
||||||
|
/**
|
||||||
|
* @internal __COMPAT__ only
|
||||||
|
*/
|
||||||
|
isCompatRoot?: true
|
||||||
|
/**
|
||||||
|
* @internal custom element interception hook
|
||||||
|
*/
|
||||||
|
ce?: (instance: ComponentInternalInstance) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since v-if and v-for are the two possible ways node structure can dynamically
|
// Since v-if and v-for are the two possible ways node structure can dynamically
|
||||||
|
224
packages/runtime-dom/__tests__/customElement.spec.ts
Normal file
224
packages/runtime-dom/__tests__/customElement.spec.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import {
|
||||||
|
defineCustomElement,
|
||||||
|
h,
|
||||||
|
nextTick,
|
||||||
|
ref,
|
||||||
|
renderSlot,
|
||||||
|
VueElement
|
||||||
|
} from '../src'
|
||||||
|
|
||||||
|
describe('defineCustomElement', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mounting/unmount', () => {
|
||||||
|
const E = defineCustomElement({
|
||||||
|
render: () => h('div', 'hello')
|
||||||
|
})
|
||||||
|
customElements.define('my-element', E)
|
||||||
|
|
||||||
|
test('should work', () => {
|
||||||
|
container.innerHTML = `<my-element></my-element>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
expect(e).toBeInstanceOf(E)
|
||||||
|
expect(e._instance).toBeTruthy()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work w/ manual instantiation', () => {
|
||||||
|
const e = new E()
|
||||||
|
// should lazy init
|
||||||
|
expect(e._instance).toBe(null)
|
||||||
|
// should initialize on connect
|
||||||
|
container.appendChild(e)
|
||||||
|
expect(e._instance).toBeTruthy()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should unmount on remove', async () => {
|
||||||
|
container.innerHTML = `<my-element></my-element>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
container.removeChild(e)
|
||||||
|
await nextTick()
|
||||||
|
expect(e._instance).toBe(null)
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not unmount on move', async () => {
|
||||||
|
container.innerHTML = `<div><my-element></my-element></div>`
|
||||||
|
const e = container.childNodes[0].childNodes[0] as VueElement
|
||||||
|
const i = e._instance
|
||||||
|
// moving from one parent to another - this will trigger both disconnect
|
||||||
|
// and connected callbacks synchronously
|
||||||
|
container.appendChild(e)
|
||||||
|
await nextTick()
|
||||||
|
// should be the same instance
|
||||||
|
expect(e._instance).toBe(i)
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('props', () => {
|
||||||
|
const E = defineCustomElement({
|
||||||
|
props: ['foo', 'bar', 'bazQux'],
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
h('div', null, this.foo),
|
||||||
|
h('div', null, this.bazQux || (this.bar && this.bar.x))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-el-props', E)
|
||||||
|
|
||||||
|
test('props via attribute', async () => {
|
||||||
|
// bazQux should map to `baz-qux` attribute
|
||||||
|
container.innerHTML = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
|
||||||
|
|
||||||
|
// change attr
|
||||||
|
e.setAttribute('foo', 'changed')
|
||||||
|
await nextTick()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div><div>bye</div>')
|
||||||
|
|
||||||
|
e.setAttribute('baz-qux', 'changed')
|
||||||
|
await nextTick()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(
|
||||||
|
'<div>changed</div><div>changed</div>'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('props via properties', async () => {
|
||||||
|
const e = new E()
|
||||||
|
e.foo = 'one'
|
||||||
|
e.bar = { x: 'two' }
|
||||||
|
container.appendChild(e)
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
|
||||||
|
|
||||||
|
e.foo = 'three'
|
||||||
|
await nextTick()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
|
||||||
|
|
||||||
|
e.bazQux = 'four'
|
||||||
|
await nextTick()
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>four</div>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('emits', () => {
|
||||||
|
const E = defineCustomElement({
|
||||||
|
setup(_, { emit }) {
|
||||||
|
emit('created')
|
||||||
|
return () =>
|
||||||
|
h('div', {
|
||||||
|
onClick: () => emit('my-click', 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-el-emits', E)
|
||||||
|
|
||||||
|
test('emit on connect', () => {
|
||||||
|
const e = new E()
|
||||||
|
const spy = jest.fn()
|
||||||
|
e.addEventListener('created', spy)
|
||||||
|
container.appendChild(e)
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('emit on interaction', () => {
|
||||||
|
container.innerHTML = `<my-el-emits></my-el-emits>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
const spy = jest.fn()
|
||||||
|
e.addEventListener('my-click', spy)
|
||||||
|
e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
expect(spy.mock.calls[0][0]).toMatchObject({
|
||||||
|
detail: [1]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('slots', () => {
|
||||||
|
const E = defineCustomElement({
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
h('div', null, [
|
||||||
|
renderSlot(this.$slots, 'default', undefined, () => [
|
||||||
|
h('div', 'fallback')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
h('div', null, renderSlot(this.$slots, 'named'))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-el-slots', E)
|
||||||
|
|
||||||
|
test('default slot', () => {
|
||||||
|
container.innerHTML = `<my-el-slots><span>hi</span></my-el-slots>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
// native slots allocation does not affect innerHTML, so we just
|
||||||
|
// verify that we've rendered the correct native slots here...
|
||||||
|
expect(e.shadowRoot!.innerHTML).toBe(
|
||||||
|
`<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('provide/inject', () => {
|
||||||
|
const Consumer = defineCustomElement({
|
||||||
|
inject: ['foo'],
|
||||||
|
render(this: any) {
|
||||||
|
return h('div', this.foo.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-consumer', Consumer)
|
||||||
|
|
||||||
|
test('over nested usage', async () => {
|
||||||
|
const foo = ref('injected!')
|
||||||
|
const Provider = defineCustomElement({
|
||||||
|
provide: {
|
||||||
|
foo
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('my-consumer')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-provider', Provider)
|
||||||
|
container.innerHTML = `<my-provider><my-provider>`
|
||||||
|
const provider = container.childNodes[0] as VueElement
|
||||||
|
const consumer = provider.shadowRoot!.childNodes[0] as VueElement
|
||||||
|
|
||||||
|
expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
|
||||||
|
|
||||||
|
foo.value = 'changed!'
|
||||||
|
await nextTick()
|
||||||
|
expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('over slot composition', async () => {
|
||||||
|
const foo = ref('injected!')
|
||||||
|
const Provider = defineCustomElement({
|
||||||
|
provide: {
|
||||||
|
foo
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return renderSlot(this.$slots, 'default')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
customElements.define('my-provider-2', Provider)
|
||||||
|
|
||||||
|
container.innerHTML = `<my-provider-2><my-consumer></my-consumer><my-provider-2>`
|
||||||
|
const provider = container.childNodes[0]
|
||||||
|
const consumer = provider.childNodes[0] as VueElement
|
||||||
|
expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
|
||||||
|
|
||||||
|
foo.value = 'changed!'
|
||||||
|
await nextTick()
|
||||||
|
expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
256
packages/runtime-dom/src/apiCustomElement.ts
Normal file
256
packages/runtime-dom/src/apiCustomElement.ts
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ComponentOptionsMixin,
|
||||||
|
ComponentOptionsWithArrayProps,
|
||||||
|
ComponentOptionsWithObjectProps,
|
||||||
|
ComponentOptionsWithoutProps,
|
||||||
|
ComponentPropsOptions,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedOptions,
|
||||||
|
EmitsOptions,
|
||||||
|
MethodOptions,
|
||||||
|
RenderFunction,
|
||||||
|
SetupContext,
|
||||||
|
ComponentInternalInstance,
|
||||||
|
VNode,
|
||||||
|
RootHydrateFunction,
|
||||||
|
ExtractPropTypes,
|
||||||
|
createVNode,
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
warn
|
||||||
|
} from '@vue/runtime-core'
|
||||||
|
import { camelize, hyphenate, isArray } from '@vue/shared'
|
||||||
|
import { hydrate, render } from '.'
|
||||||
|
|
||||||
|
type VueElementConstructor<P = {}> = {
|
||||||
|
new (): VueElement & P
|
||||||
|
}
|
||||||
|
|
||||||
|
// defineCustomElement provides the same type inference as defineComponent
|
||||||
|
// so most of the following overloads should be kept in sync w/ defineComponent.
|
||||||
|
|
||||||
|
// overload 1: direct setup function
|
||||||
|
export function defineCustomElement<Props, RawBindings = object>(
|
||||||
|
setup: (
|
||||||
|
props: Readonly<Props>,
|
||||||
|
ctx: SetupContext
|
||||||
|
) => RawBindings | RenderFunction
|
||||||
|
): VueElementConstructor<Props>
|
||||||
|
|
||||||
|
// overload 2: object format with no props
|
||||||
|
export function defineCustomElement<
|
||||||
|
Props = {},
|
||||||
|
RawBindings = {},
|
||||||
|
D = {},
|
||||||
|
C extends ComputedOptions = {},
|
||||||
|
M extends MethodOptions = {},
|
||||||
|
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
E extends EmitsOptions = EmitsOptions,
|
||||||
|
EE extends string = string
|
||||||
|
>(
|
||||||
|
options: ComponentOptionsWithoutProps<
|
||||||
|
Props,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C,
|
||||||
|
M,
|
||||||
|
Mixin,
|
||||||
|
Extends,
|
||||||
|
E,
|
||||||
|
EE
|
||||||
|
>
|
||||||
|
): VueElementConstructor<Props>
|
||||||
|
|
||||||
|
// overload 3: object format with array props declaration
|
||||||
|
export function defineCustomElement<
|
||||||
|
PropNames extends string,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C extends ComputedOptions = {},
|
||||||
|
M extends MethodOptions = {},
|
||||||
|
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
|
>(
|
||||||
|
options: ComponentOptionsWithArrayProps<
|
||||||
|
PropNames,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C,
|
||||||
|
M,
|
||||||
|
Mixin,
|
||||||
|
Extends,
|
||||||
|
E,
|
||||||
|
EE
|
||||||
|
>
|
||||||
|
): VueElementConstructor<{ [K in PropNames]: any }>
|
||||||
|
|
||||||
|
// overload 4: object format with object props declaration
|
||||||
|
export function defineCustomElement<
|
||||||
|
PropsOptions extends Readonly<ComponentPropsOptions>,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C extends ComputedOptions = {},
|
||||||
|
M extends MethodOptions = {},
|
||||||
|
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
|
>(
|
||||||
|
options: ComponentOptionsWithObjectProps<
|
||||||
|
PropsOptions,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C,
|
||||||
|
M,
|
||||||
|
Mixin,
|
||||||
|
Extends,
|
||||||
|
E,
|
||||||
|
EE
|
||||||
|
>
|
||||||
|
): VueElementConstructor<ExtractPropTypes<PropsOptions>>
|
||||||
|
|
||||||
|
// overload 5: defining a custom element from the returned value of
|
||||||
|
// `defineComponent`
|
||||||
|
export function defineCustomElement(options: {
|
||||||
|
new (...args: any[]): ComponentPublicInstance
|
||||||
|
}): VueElementConstructor
|
||||||
|
|
||||||
|
export function defineCustomElement(
|
||||||
|
options: any,
|
||||||
|
hydate?: RootHydrateFunction
|
||||||
|
): VueElementConstructor {
|
||||||
|
const Comp = defineComponent(options as any)
|
||||||
|
const { props } = options
|
||||||
|
const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
|
||||||
|
const attrKeys = rawKeys.map(hyphenate)
|
||||||
|
const propKeys = rawKeys.map(camelize)
|
||||||
|
|
||||||
|
class VueCustomElement extends VueElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return attrKeys
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super(Comp, attrKeys, hydate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of propKeys) {
|
||||||
|
Object.defineProperty(VueCustomElement.prototype, key, {
|
||||||
|
get() {
|
||||||
|
return this._getProp(key)
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this._setProp(key, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return VueCustomElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defineSSRCustomElement = ((options: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
return defineCustomElement(options, hydrate)
|
||||||
|
}) as typeof defineCustomElement
|
||||||
|
|
||||||
|
export class VueElement extends HTMLElement {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_props: Record<string, any> = {}
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_instance: ComponentInternalInstance | null = null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_connected = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _def: Component,
|
||||||
|
private _attrs: string[],
|
||||||
|
hydrate?: RootHydrateFunction
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
if (this.shadowRoot && hydrate) {
|
||||||
|
hydrate(this._initVNode(), this.shadowRoot)
|
||||||
|
} else {
|
||||||
|
if (__DEV__ && this.shadowRoot) {
|
||||||
|
warn(
|
||||||
|
`Custom element has pre-rendered declarative shadow root but is not ` +
|
||||||
|
`defined as hydratable. Use \`defineSSRCustomElement\`.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.attachShadow({ mode: 'open' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
||||||
|
if (this._attrs.includes(name)) {
|
||||||
|
this._setProp(camelize(name), newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._connected = true
|
||||||
|
if (!this._instance) {
|
||||||
|
render(this._initVNode(), this.shadowRoot!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._connected = false
|
||||||
|
nextTick(() => {
|
||||||
|
if (!this._connected) {
|
||||||
|
render(null, this.shadowRoot!)
|
||||||
|
this._instance = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _initVNode(): VNode<any, any> {
|
||||||
|
const vnode = createVNode(this._def, this._props)
|
||||||
|
vnode.ce = instance => {
|
||||||
|
this._instance = instance
|
||||||
|
instance.isCE = true
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vnode
|
||||||
|
}
|
||||||
|
}
|
@ -28,12 +28,15 @@ const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)
|
|||||||
|
|
||||||
// lazy create the renderer - this makes core renderer logic tree-shakable
|
// lazy create the renderer - this makes core renderer logic tree-shakable
|
||||||
// in case the user only imports reactivity utilities from Vue.
|
// in case the user only imports reactivity utilities from Vue.
|
||||||
let renderer: Renderer<Element> | HydrationRenderer
|
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
|
||||||
|
|
||||||
let enabledHydration = false
|
let enabledHydration = false
|
||||||
|
|
||||||
function ensureRenderer() {
|
function ensureRenderer() {
|
||||||
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
|
return (
|
||||||
|
renderer ||
|
||||||
|
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureHydrationRenderer() {
|
function ensureHydrationRenderer() {
|
||||||
@ -47,7 +50,7 @@ function ensureHydrationRenderer() {
|
|||||||
// use explicit type casts here to avoid import() calls in rolled-up d.ts
|
// use explicit type casts here to avoid import() calls in rolled-up d.ts
|
||||||
export const render = ((...args) => {
|
export const render = ((...args) => {
|
||||||
ensureRenderer().render(...args)
|
ensureRenderer().render(...args)
|
||||||
}) as RootRenderFunction<Element>
|
}) as RootRenderFunction<Element | ShadowRoot>
|
||||||
|
|
||||||
export const hydrate = ((...args) => {
|
export const hydrate = ((...args) => {
|
||||||
ensureHydrationRenderer().hydrate(...args)
|
ensureHydrationRenderer().hydrate(...args)
|
||||||
@ -191,6 +194,13 @@ function normalizeContainer(
|
|||||||
return container as any
|
return container as any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom element support
|
||||||
|
export {
|
||||||
|
defineCustomElement,
|
||||||
|
defineSSRCustomElement,
|
||||||
|
VueElement
|
||||||
|
} from './apiCustomElement'
|
||||||
|
|
||||||
// SFC CSS utilities
|
// SFC CSS utilities
|
||||||
export { useCssModule } from './helpers/useCssModule'
|
export { useCssModule } from './helpers/useCssModule'
|
||||||
export { useCssVars } from './helpers/useCssVars'
|
export { useCssVars } from './helpers/useCssVars'
|
||||||
|
Loading…
Reference in New Issue
Block a user