`
+ const provider = container.childNodes[0]
+ const consumer = provider.childNodes[0] as VueElement
+ expect(consumer.shadowRoot!.innerHTML).toBe(`injected!
`)
+
+ foo.value = 'changed!'
+ await nextTick()
+ expect(consumer.shadowRoot!.innerHTML).toBe(`changed!
`)
+ })
+ })
+})
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
new file mode 100644
index 000000000..82992ae0e
--- /dev/null
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -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 = {
+ 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(
+ setup: (
+ props: Readonly,
+ ctx: SetupContext
+ ) => RawBindings | RenderFunction
+): VueElementConstructor
+
+// 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
+
+// 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,
+ 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,
+ RawBindings,
+ D,
+ C extends ComputedOptions = {},
+ M extends MethodOptions = {},
+ Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+ Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
+ E extends EmitsOptions = Record,
+ EE extends string = string
+>(
+ options: ComponentOptionsWithObjectProps<
+ PropsOptions,
+ RawBindings,
+ D,
+ C,
+ M,
+ Mixin,
+ Extends,
+ E,
+ EE
+ >
+): VueElementConstructor>
+
+// 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 = {}
+ /**
+ * @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 {
+ 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
+ }
+}
diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts
index 2ad8f2b21..257c554a6 100644
--- a/packages/runtime-dom/src/index.ts
+++ b/packages/runtime-dom/src/index.ts
@@ -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 | HydrationRenderer
+let renderer: Renderer | HydrationRenderer
let enabledHydration = false
function ensureRenderer() {
- return renderer || (renderer = createRenderer(rendererOptions))
+ return (
+ renderer ||
+ (renderer = createRenderer(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
+}) as RootRenderFunction
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'