diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 44bb0390c99..7c016e12fcf 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -50,8 +50,18 @@ export interface App { directive(name: string, directive: Directive): this mount( rootContainer: HostElement | string, + /** + * @internal + */ isHydrate?: boolean, + /** + * @internal + */ namespace?: boolean | ElementNamespace, + /** + * @internal + */ + vnode?: VNode, ): ComponentPublicInstance unmount(): void onUnmount(cb: () => void): void @@ -76,6 +86,11 @@ export interface App { _context: AppContext _instance: ComponentInternalInstance | null + /** + * @internal custom element vnode + */ + _ceVNode?: VNode + /** * v2 compat only */ @@ -337,7 +352,7 @@ export function createAppAPI( ` you need to unmount the previous app by calling \`app.unmount()\` first.`, ) } - const vnode = createVNode(rootComponent, rootProps) + const vnode = app._ceVNode || createVNode(rootComponent, rootProps) // store app context on the root VNode. // this will be set on the root instance on initial mount. vnode.appContext = context diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index ffa7d28f852..c4dc0f4e034 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1136,4 +1136,26 @@ describe('defineCustomElement', () => { expect(fooVal).toBe('foo') expect(barVal).toBe('bar') }) + + describe('configureApp', () => { + test('should work', () => { + const E = defineCustomElement( + () => { + const msg = inject('msg') + return () => h('div', msg!) + }, + { + configureApp(app) { + app.provide('msg', 'app-injected') + }, + }, + ) + customElements.define('my-element-with-app', E) + + container.innerHTML = `` + const e = container.childNodes[0] as VueElement + + expect(e.shadowRoot?.innerHTML).toBe('
app-injected
') + }) + }) }) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 306d2edf698..39336303019 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -1,4 +1,5 @@ import { + type App, type Component, type ComponentCustomElementInterface, type ComponentInjectOptions, @@ -10,6 +11,7 @@ import { type ComponentProvideOptions, type ComputedOptions, type ConcreteComponent, + type CreateAppFunction, type CreateComponentPublicInstanceWithMixins, type DefineComponent, type Directive, @@ -18,7 +20,6 @@ import { type ExtractPropTypes, type MethodOptions, type RenderFunction, - type RootHydrateFunction, type SetupContext, type SlotsType, type VNode, @@ -39,7 +40,7 @@ import { isPlainObject, toNumber, } from '@vue/shared' -import { hydrate, render } from '.' +import { createApp, createSSRApp, render } from '.' export type VueElementConstructor

= { new (initialProps?: Record): VueElement & P @@ -49,6 +50,7 @@ export interface CustomElementOptions { styles?: string[] shadowRoot?: boolean nonce?: string + configureApp?: (app: App) => void } // defineCustomElement provides the same type inference as defineComponent @@ -165,14 +167,14 @@ export function defineCustomElement( /** * @internal */ - hydrate?: RootHydrateFunction, + _createApp?: CreateAppFunction, ): VueElementConstructor { const Comp = defineComponent(options, extraOptions) as any if (isPlainObject(Comp)) extend(Comp, extraOptions) class VueCustomElement extends VueElement { static def = Comp constructor(initialProps?: Record) { - super(Comp, initialProps, hydrate) + super(Comp, initialProps, _createApp) } } @@ -185,7 +187,7 @@ export const defineSSRCustomElement = (( extraOptions?: ComponentOptions, ) => { // @ts-expect-error - return defineCustomElement(options, extraOptions, hydrate) + return defineCustomElement(options, extraOptions, createSSRApp) }) as typeof defineCustomElement const BaseClass = ( @@ -202,6 +204,14 @@ export class VueElement * @internal */ _instance: ComponentInternalInstance | null = null + /** + * @internal + */ + _app: App | null = null + /** + * @internal + */ + _nonce = this._def.nonce private _connected = false private _resolved = false @@ -225,15 +235,19 @@ export class VueElement private _slots?: Record constructor( + /** + * Component def - note this may be an AsyncWrapper, and this._def will + * be overwritten by the inner component when resolved. + */ private _def: InnerComponentDef, private _props: Record = {}, - hydrate?: RootHydrateFunction, + private _createApp: CreateAppFunction = createApp, ) { super() - // TODO handle non-shadowRoot hydration - if (this.shadowRoot && hydrate) { - hydrate(this._createVNode(), this.shadowRoot) + if (this.shadowRoot && _createApp !== createApp) { this._root = this.shadowRoot + // TODO hydration needs to be reworked + this._mount(_def) } else { if (__DEV__ && this.shadowRoot) { warn( @@ -303,9 +317,10 @@ export class VueElement this._ob.disconnect() this._ob = null } - render(null, this._root) + // unmount + this._app && this._app.unmount() this._instance!.ce = undefined - this._instance = null + this._app = this._instance = null } }) } @@ -371,11 +386,8 @@ export class VueElement ) } - // initial render - this._update() - - // apply expose - this._applyExpose() + // initial mount + this._mount(def) } const asyncDef = (this._def as ComponentOptions).__asyncLoader @@ -388,6 +400,34 @@ export class VueElement } } + private _mount(def: InnerComponentDef) { + if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) { + // @ts-expect-error + def.name = 'VueElement' + } + this._app = this._createApp(def) + if (def.configureApp) { + def.configureApp(this._app) + } + this._app._ceVNode = this._createVNode() + this._app.mount(this._root) + + // apply expose after mount + const exposed = this._instance && this._instance.exposed + if (!exposed) return + for (const key in exposed) { + if (!hasOwn(this, key)) { + // exposed properties are readonly + Object.defineProperty(this, key, { + // unwrap ref to be consistent with public instance behavior + get: () => unref(exposed[key]), + }) + } else if (__DEV__) { + warn(`Exposed property "${key}" already exists on custom element.`) + } + } + } + private _resolveProps(def: InnerComponentDef) { const { props } = def const declaredPropKeys = isArray(props) ? props : Object.keys(props || {}) @@ -412,22 +452,6 @@ export class VueElement } } - private _applyExpose() { - const exposed = this._instance && this._instance.exposed - if (!exposed) return - for (const key in exposed) { - if (!hasOwn(this, key)) { - // exposed properties are readonly - Object.defineProperty(this, key, { - // unwrap ref to be consistent with public instance behavior - get: () => unref(exposed[key]), - }) - } else if (__DEV__) { - warn(`Exposed property "${key}" already exists on custom element.`) - } - } - } - protected _setAttr(key: string) { if (key.startsWith('data-v-')) return let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined @@ -534,7 +558,7 @@ export class VueElement } this._styleChildren.add(owner) } - const nonce = this._def.nonce + const nonce = this._nonce for (let i = styles.length - 1; i >= 0; i--) { const s = document.createElement('style') if (nonce) s.setAttribute('nonce', nonce) diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index e55767b7654..706401ddd89 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -108,9 +108,9 @@ export const createApp = ((...args) => { // rendered by the server, the template should not contain any user data. component.template = container.innerHTML // 2.x compat check - if (__COMPAT__ && __DEV__) { - for (let i = 0; i < container.attributes.length; i++) { - const attr = container.attributes[i] + if (__COMPAT__ && __DEV__ && container.nodeType === 1) { + for (let i = 0; i < (container as Element).attributes.length; i++) { + const attr = (container as Element).attributes[i] if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { compatUtils.warnDeprecation( DeprecationTypes.GLOBAL_MOUNT_CONTAINER, @@ -123,7 +123,9 @@ export const createApp = ((...args) => { } // clear content before mounting - container.textContent = '' + if (container.nodeType === 1) { + container.textContent = '' + } const proxy = mount(container, false, resolveRootNamespace(container)) if (container instanceof Element) { container.removeAttribute('v-cloak') @@ -154,7 +156,9 @@ export const createSSRApp = ((...args) => { return app }) as CreateAppFunction -function resolveRootNamespace(container: Element): ElementNamespace { +function resolveRootNamespace( + container: Element | ShadowRoot, +): ElementNamespace { if (container instanceof SVGElement) { return 'svg' } @@ -215,7 +219,7 @@ function injectCompilerOptionsCheck(app: App) { function normalizeContainer( container: Element | ShadowRoot | string, -): Element | null { +): Element | ShadowRoot | null { if (isString(container)) { const res = document.querySelector(container) if (__DEV__ && !res) {