feat(runtime-dom): defineCustomElement
This commit is contained in:
parent
42ace9577d
commit
8610e1c9e2
@ -279,12 +279,15 @@ export interface ComponentInternalInstance {
|
||||
* @internal
|
||||
*/
|
||||
emitsOptions: ObjectEmitsOptions | null
|
||||
|
||||
/**
|
||||
* resolved inheritAttrs options
|
||||
* @internal
|
||||
*/
|
||||
inheritAttrs?: boolean
|
||||
/**
|
||||
* is custom element?
|
||||
*/
|
||||
isCE?: boolean
|
||||
|
||||
// the rest are only for stateful components ---------------------------------
|
||||
|
||||
@ -519,6 +522,11 @@ export function createComponentInstance(
|
||||
instance.root = parent ? parent.root : instance
|
||||
instance.emit = emit.bind(null, instance)
|
||||
|
||||
// apply custom element special handling
|
||||
if (vnode.ce) {
|
||||
vnode.ce(instance)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Data } from '../component'
|
||||
import { Slots, RawSlots } from '../componentSlots'
|
||||
import { ContextualRenderFn } from '../componentRenderContext'
|
||||
import {
|
||||
ContextualRenderFn,
|
||||
currentRenderingInstance
|
||||
} from '../componentRenderContext'
|
||||
import { Comment, isVNode } from '../vnode'
|
||||
import {
|
||||
VNodeArrayChildren,
|
||||
@ -11,6 +14,7 @@ import {
|
||||
} from '../vnode'
|
||||
import { PatchFlags, SlotFlags } from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
import { createVNode } from '@vue/runtime-core'
|
||||
|
||||
/**
|
||||
* Compiler runtime helper for rendering `<slot/>`
|
||||
@ -25,6 +29,14 @@ export function renderSlot(
|
||||
fallback?: () => VNodeArrayChildren,
|
||||
noSlotted?: boolean
|
||||
): VNode {
|
||||
if (currentRenderingInstance!.isCE) {
|
||||
return createVNode(
|
||||
'slot',
|
||||
name === 'default' ? null : { name },
|
||||
fallback && fallback()
|
||||
)
|
||||
}
|
||||
|
||||
let slot = slots[name]
|
||||
|
||||
if (__DEV__ && slot && slot.length > 1) {
|
||||
|
@ -25,7 +25,7 @@ import { isAsyncWrapper } from './apiAsyncComponent'
|
||||
|
||||
export type RootHydrateFunction = (
|
||||
vnode: VNode<Node, Element>,
|
||||
container: Element
|
||||
container: Element | ShadowRoot
|
||||
) => void
|
||||
|
||||
const enum DOMNodeTypes {
|
||||
|
@ -93,7 +93,7 @@ export interface Renderer<HostElement = RendererElement> {
|
||||
createApp: CreateAppFunction<HostElement>
|
||||
}
|
||||
|
||||
export interface HydrationRenderer extends Renderer<Element> {
|
||||
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
||||
hydrate: RootHydrateFunction
|
||||
}
|
||||
|
||||
|
@ -136,11 +136,6 @@ export interface VNode<
|
||||
*/
|
||||
[ReactiveFlags.SKIP]: true
|
||||
|
||||
/**
|
||||
* @internal __COMPAT__ only
|
||||
*/
|
||||
isCompatRoot?: true
|
||||
|
||||
type: VNodeTypes
|
||||
props: (VNodeProps & ExtraProps) | null
|
||||
key: string | number | null
|
||||
@ -155,6 +150,7 @@ export interface VNode<
|
||||
* - Slot fragment vnodes with :slotted SFC styles.
|
||||
* - Component vnodes (during patch/hydration) so that its root node can
|
||||
* inherit the component's slotScopeIds
|
||||
* @internal
|
||||
*/
|
||||
slotScopeIds: string[] | null
|
||||
children: VNodeNormalizedChildren
|
||||
@ -167,24 +163,50 @@ export interface VNode<
|
||||
anchor: HostNode | null // fragment anchor
|
||||
target: HostElement | null // teleport target
|
||||
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: SuspenseBoundary | null
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ssContent: VNode | null
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ssFallback: VNode | null
|
||||
|
||||
// optimization only
|
||||
shapeFlag: number
|
||||
patchFlag: number
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
dynamicProps: string[] | null
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
dynamicChildren: VNode[] | null
|
||||
|
||||
// application root node only
|
||||
appContext: AppContext | null
|
||||
|
||||
// v-for memo
|
||||
/**
|
||||
* @internal attached by v-memo
|
||||
*/
|
||||
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
|
||||
|
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
|
||||
// in case the user only imports reactivity utilities from Vue.
|
||||
let renderer: Renderer<Element> | HydrationRenderer
|
||||
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
|
||||
|
||||
let enabledHydration = false
|
||||
|
||||
function ensureRenderer() {
|
||||
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
|
||||
return (
|
||||
renderer ||
|
||||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
|
||||
)
|
||||
}
|
||||
|
||||
function ensureHydrationRenderer() {
|
||||
@ -47,7 +50,7 @@ function ensureHydrationRenderer() {
|
||||
// use explicit type casts here to avoid import() calls in rolled-up d.ts
|
||||
export const render = ((...args) => {
|
||||
ensureRenderer().render(...args)
|
||||
}) as RootRenderFunction<Element>
|
||||
}) as RootRenderFunction<Element | ShadowRoot>
|
||||
|
||||
export const hydrate = ((...args) => {
|
||||
ensureHydrationRenderer().hydrate(...args)
|
||||
@ -191,6 +194,13 @@ function normalizeContainer(
|
||||
return container as any
|
||||
}
|
||||
|
||||
// Custom element support
|
||||
export {
|
||||
defineCustomElement,
|
||||
defineSSRCustomElement,
|
||||
VueElement
|
||||
} from './apiCustomElement'
|
||||
|
||||
// SFC CSS utilities
|
||||
export { useCssModule } from './helpers/useCssModule'
|
||||
export { useCssVars } from './helpers/useCssVars'
|
||||
|
Loading…
Reference in New Issue
Block a user