mirror of https://github.com/vuejs/core.git
feat(custom-element): support configurable app instance in defineCustomElement
Support configuring via `configureApp` option: ```js defineCustomElement({ // ... }, { configureApp(app) { // ... } }) ``` close #4356 close #4635
This commit is contained in:
parent
261c8b111d
commit
6758c3cd04
|
@ -50,8 +50,18 @@ export interface App<HostElement = any> {
|
||||||
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
|
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
|
||||||
mount(
|
mount(
|
||||||
rootContainer: HostElement | string,
|
rootContainer: HostElement | string,
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
isHydrate?: boolean,
|
isHydrate?: boolean,
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
namespace?: boolean | ElementNamespace,
|
namespace?: boolean | ElementNamespace,
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
vnode?: VNode,
|
||||||
): ComponentPublicInstance
|
): ComponentPublicInstance
|
||||||
unmount(): void
|
unmount(): void
|
||||||
onUnmount(cb: () => void): void
|
onUnmount(cb: () => void): void
|
||||||
|
@ -76,6 +86,11 @@ export interface App<HostElement = any> {
|
||||||
_context: AppContext
|
_context: AppContext
|
||||||
_instance: ComponentInternalInstance | null
|
_instance: ComponentInternalInstance | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal custom element vnode
|
||||||
|
*/
|
||||||
|
_ceVNode?: VNode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* v2 compat only
|
* v2 compat only
|
||||||
*/
|
*/
|
||||||
|
@ -337,7 +352,7 @@ export function createAppAPI<HostElement>(
|
||||||
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
|
` 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.
|
// store app context on the root VNode.
|
||||||
// this will be set on the root instance on initial mount.
|
// this will be set on the root instance on initial mount.
|
||||||
vnode.appContext = context
|
vnode.appContext = context
|
||||||
|
|
|
@ -1136,4 +1136,26 @@ describe('defineCustomElement', () => {
|
||||||
expect(fooVal).toBe('foo')
|
expect(fooVal).toBe('foo')
|
||||||
expect(barVal).toBe('bar')
|
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 = `<my-element-with-app></my-element-with-app>`
|
||||||
|
const e = container.childNodes[0] as VueElement
|
||||||
|
|
||||||
|
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
type App,
|
||||||
type Component,
|
type Component,
|
||||||
type ComponentCustomElementInterface,
|
type ComponentCustomElementInterface,
|
||||||
type ComponentInjectOptions,
|
type ComponentInjectOptions,
|
||||||
|
@ -10,6 +11,7 @@ import {
|
||||||
type ComponentProvideOptions,
|
type ComponentProvideOptions,
|
||||||
type ComputedOptions,
|
type ComputedOptions,
|
||||||
type ConcreteComponent,
|
type ConcreteComponent,
|
||||||
|
type CreateAppFunction,
|
||||||
type CreateComponentPublicInstanceWithMixins,
|
type CreateComponentPublicInstanceWithMixins,
|
||||||
type DefineComponent,
|
type DefineComponent,
|
||||||
type Directive,
|
type Directive,
|
||||||
|
@ -18,7 +20,6 @@ import {
|
||||||
type ExtractPropTypes,
|
type ExtractPropTypes,
|
||||||
type MethodOptions,
|
type MethodOptions,
|
||||||
type RenderFunction,
|
type RenderFunction,
|
||||||
type RootHydrateFunction,
|
|
||||||
type SetupContext,
|
type SetupContext,
|
||||||
type SlotsType,
|
type SlotsType,
|
||||||
type VNode,
|
type VNode,
|
||||||
|
@ -39,7 +40,7 @@ import {
|
||||||
isPlainObject,
|
isPlainObject,
|
||||||
toNumber,
|
toNumber,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { hydrate, render } from '.'
|
import { createApp, createSSRApp, render } from '.'
|
||||||
|
|
||||||
export type VueElementConstructor<P = {}> = {
|
export type VueElementConstructor<P = {}> = {
|
||||||
new (initialProps?: Record<string, any>): VueElement & P
|
new (initialProps?: Record<string, any>): VueElement & P
|
||||||
|
@ -49,6 +50,7 @@ export interface CustomElementOptions {
|
||||||
styles?: string[]
|
styles?: string[]
|
||||||
shadowRoot?: boolean
|
shadowRoot?: boolean
|
||||||
nonce?: string
|
nonce?: string
|
||||||
|
configureApp?: (app: App) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// defineCustomElement provides the same type inference as defineComponent
|
// defineCustomElement provides the same type inference as defineComponent
|
||||||
|
@ -165,14 +167,14 @@ export function defineCustomElement(
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
hydrate?: RootHydrateFunction,
|
_createApp?: CreateAppFunction<Element>,
|
||||||
): VueElementConstructor {
|
): VueElementConstructor {
|
||||||
const Comp = defineComponent(options, extraOptions) as any
|
const Comp = defineComponent(options, extraOptions) as any
|
||||||
if (isPlainObject(Comp)) extend(Comp, extraOptions)
|
if (isPlainObject(Comp)) extend(Comp, extraOptions)
|
||||||
class VueCustomElement extends VueElement {
|
class VueCustomElement extends VueElement {
|
||||||
static def = Comp
|
static def = Comp
|
||||||
constructor(initialProps?: Record<string, any>) {
|
constructor(initialProps?: Record<string, any>) {
|
||||||
super(Comp, initialProps, hydrate)
|
super(Comp, initialProps, _createApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +187,7 @@ export const defineSSRCustomElement = ((
|
||||||
extraOptions?: ComponentOptions,
|
extraOptions?: ComponentOptions,
|
||||||
) => {
|
) => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
return defineCustomElement(options, extraOptions, hydrate)
|
return defineCustomElement(options, extraOptions, createSSRApp)
|
||||||
}) as typeof defineCustomElement
|
}) as typeof defineCustomElement
|
||||||
|
|
||||||
const BaseClass = (
|
const BaseClass = (
|
||||||
|
@ -202,6 +204,14 @@ export class VueElement
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_instance: ComponentInternalInstance | null = null
|
_instance: ComponentInternalInstance | null = null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_app: App | null = null
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_nonce = this._def.nonce
|
||||||
|
|
||||||
private _connected = false
|
private _connected = false
|
||||||
private _resolved = false
|
private _resolved = false
|
||||||
|
@ -225,15 +235,19 @@ export class VueElement
|
||||||
private _slots?: Record<string, Node[]>
|
private _slots?: Record<string, Node[]>
|
||||||
|
|
||||||
constructor(
|
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 _def: InnerComponentDef,
|
||||||
private _props: Record<string, any> = {},
|
private _props: Record<string, any> = {},
|
||||||
hydrate?: RootHydrateFunction,
|
private _createApp: CreateAppFunction<Element> = createApp,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
// TODO handle non-shadowRoot hydration
|
if (this.shadowRoot && _createApp !== createApp) {
|
||||||
if (this.shadowRoot && hydrate) {
|
|
||||||
hydrate(this._createVNode(), this.shadowRoot)
|
|
||||||
this._root = this.shadowRoot
|
this._root = this.shadowRoot
|
||||||
|
// TODO hydration needs to be reworked
|
||||||
|
this._mount(_def)
|
||||||
} else {
|
} else {
|
||||||
if (__DEV__ && this.shadowRoot) {
|
if (__DEV__ && this.shadowRoot) {
|
||||||
warn(
|
warn(
|
||||||
|
@ -303,9 +317,10 @@ export class VueElement
|
||||||
this._ob.disconnect()
|
this._ob.disconnect()
|
||||||
this._ob = null
|
this._ob = null
|
||||||
}
|
}
|
||||||
render(null, this._root)
|
// unmount
|
||||||
|
this._app && this._app.unmount()
|
||||||
this._instance!.ce = undefined
|
this._instance!.ce = undefined
|
||||||
this._instance = null
|
this._app = this._instance = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -371,11 +386,8 @@ export class VueElement
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial render
|
// initial mount
|
||||||
this._update()
|
this._mount(def)
|
||||||
|
|
||||||
// apply expose
|
|
||||||
this._applyExpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const asyncDef = (this._def as ComponentOptions).__asyncLoader
|
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) {
|
private _resolveProps(def: InnerComponentDef) {
|
||||||
const { props } = def
|
const { props } = def
|
||||||
const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
|
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) {
|
protected _setAttr(key: string) {
|
||||||
if (key.startsWith('data-v-')) return
|
if (key.startsWith('data-v-')) return
|
||||||
let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined
|
let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined
|
||||||
|
@ -534,7 +558,7 @@ export class VueElement
|
||||||
}
|
}
|
||||||
this._styleChildren.add(owner)
|
this._styleChildren.add(owner)
|
||||||
}
|
}
|
||||||
const nonce = this._def.nonce
|
const nonce = this._nonce
|
||||||
for (let i = styles.length - 1; i >= 0; i--) {
|
for (let i = styles.length - 1; i >= 0; i--) {
|
||||||
const s = document.createElement('style')
|
const s = document.createElement('style')
|
||||||
if (nonce) s.setAttribute('nonce', nonce)
|
if (nonce) s.setAttribute('nonce', nonce)
|
||||||
|
|
|
@ -108,9 +108,9 @@ export const createApp = ((...args) => {
|
||||||
// rendered by the server, the template should not contain any user data.
|
// rendered by the server, the template should not contain any user data.
|
||||||
component.template = container.innerHTML
|
component.template = container.innerHTML
|
||||||
// 2.x compat check
|
// 2.x compat check
|
||||||
if (__COMPAT__ && __DEV__) {
|
if (__COMPAT__ && __DEV__ && container.nodeType === 1) {
|
||||||
for (let i = 0; i < container.attributes.length; i++) {
|
for (let i = 0; i < (container as Element).attributes.length; i++) {
|
||||||
const attr = container.attributes[i]
|
const attr = (container as Element).attributes[i]
|
||||||
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
|
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
|
||||||
compatUtils.warnDeprecation(
|
compatUtils.warnDeprecation(
|
||||||
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
|
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
|
||||||
|
@ -123,7 +123,9 @@ export const createApp = ((...args) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear content before mounting
|
// clear content before mounting
|
||||||
|
if (container.nodeType === 1) {
|
||||||
container.textContent = ''
|
container.textContent = ''
|
||||||
|
}
|
||||||
const proxy = mount(container, false, resolveRootNamespace(container))
|
const proxy = mount(container, false, resolveRootNamespace(container))
|
||||||
if (container instanceof Element) {
|
if (container instanceof Element) {
|
||||||
container.removeAttribute('v-cloak')
|
container.removeAttribute('v-cloak')
|
||||||
|
@ -154,7 +156,9 @@ export const createSSRApp = ((...args) => {
|
||||||
return app
|
return app
|
||||||
}) as CreateAppFunction<Element>
|
}) as CreateAppFunction<Element>
|
||||||
|
|
||||||
function resolveRootNamespace(container: Element): ElementNamespace {
|
function resolveRootNamespace(
|
||||||
|
container: Element | ShadowRoot,
|
||||||
|
): ElementNamespace {
|
||||||
if (container instanceof SVGElement) {
|
if (container instanceof SVGElement) {
|
||||||
return 'svg'
|
return 'svg'
|
||||||
}
|
}
|
||||||
|
@ -215,7 +219,7 @@ function injectCompilerOptionsCheck(app: App) {
|
||||||
|
|
||||||
function normalizeContainer(
|
function normalizeContainer(
|
||||||
container: Element | ShadowRoot | string,
|
container: Element | ShadowRoot | string,
|
||||||
): Element | null {
|
): Element | ShadowRoot | null {
|
||||||
if (isString(container)) {
|
if (isString(container)) {
|
||||||
const res = document.querySelector(container)
|
const res = document.querySelector(container)
|
||||||
if (__DEV__ && !res) {
|
if (__DEV__ && !res) {
|
||||||
|
|
Loading…
Reference in New Issue