Skip to content

Commit 6758c3c

Browse files
committedAug 7, 2024··
feat(custom-element): support configurable app instance in defineCustomElement
Support configuring via `configureApp` option: ```js defineCustomElement({ // ... }, { configureApp(app) { // ... } }) ``` close #4356 close #4635
1 parent 261c8b1 commit 6758c3c

File tree

4 files changed

+105
-40
lines changed

4 files changed

+105
-40
lines changed
 

‎packages/runtime-core/src/apiCreateApp.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,18 @@ export interface App<HostElement = any> {
5050
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
5151
mount(
5252
rootContainer: HostElement | string,
53+
/**
54+
* @internal
55+
*/
5356
isHydrate?: boolean,
57+
/**
58+
* @internal
59+
*/
5460
namespace?: boolean | ElementNamespace,
61+
/**
62+
* @internal
63+
*/
64+
vnode?: VNode,
5565
): ComponentPublicInstance
5666
unmount(): void
5767
onUnmount(cb: () => void): void
@@ -76,6 +86,11 @@ export interface App<HostElement = any> {
7686
_context: AppContext
7787
_instance: ComponentInternalInstance | null
7888

89+
/**
90+
* @internal custom element vnode
91+
*/
92+
_ceVNode?: VNode
93+
7994
/**
8095
* v2 compat only
8196
*/
@@ -337,7 +352,7 @@ export function createAppAPI<HostElement>(
337352
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
338353
)
339354
}
340-
const vnode = createVNode(rootComponent, rootProps)
355+
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
341356
// store app context on the root VNode.
342357
// this will be set on the root instance on initial mount.
343358
vnode.appContext = context

‎packages/runtime-dom/__tests__/customElement.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1136,4 +1136,26 @@ describe('defineCustomElement', () => {
11361136
expect(fooVal).toBe('foo')
11371137
expect(barVal).toBe('bar')
11381138
})
1139+
1140+
describe('configureApp', () => {
1141+
test('should work', () => {
1142+
const E = defineCustomElement(
1143+
() => {
1144+
const msg = inject('msg')
1145+
return () => h('div', msg!)
1146+
},
1147+
{
1148+
configureApp(app) {
1149+
app.provide('msg', 'app-injected')
1150+
},
1151+
},
1152+
)
1153+
customElements.define('my-element-with-app', E)
1154+
1155+
container.innerHTML = `<my-element-with-app></my-element-with-app>`
1156+
const e = container.childNodes[0] as VueElement
1157+
1158+
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
1159+
})
1160+
})
11391161
})

‎packages/runtime-dom/src/apiCustomElement.ts

+57-33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type App,
23
type Component,
34
type ComponentCustomElementInterface,
45
type ComponentInjectOptions,
@@ -10,6 +11,7 @@ import {
1011
type ComponentProvideOptions,
1112
type ComputedOptions,
1213
type ConcreteComponent,
14+
type CreateAppFunction,
1315
type CreateComponentPublicInstanceWithMixins,
1416
type DefineComponent,
1517
type Directive,
@@ -18,7 +20,6 @@ import {
1820
type ExtractPropTypes,
1921
type MethodOptions,
2022
type RenderFunction,
21-
type RootHydrateFunction,
2223
type SetupContext,
2324
type SlotsType,
2425
type VNode,
@@ -39,7 +40,7 @@ import {
3940
isPlainObject,
4041
toNumber,
4142
} from '@vue/shared'
42-
import { hydrate, render } from '.'
43+
import { createApp, createSSRApp, render } from '.'
4344

4445
export type VueElementConstructor<P = {}> = {
4546
new (initialProps?: Record<string, any>): VueElement & P
@@ -49,6 +50,7 @@ export interface CustomElementOptions {
4950
styles?: string[]
5051
shadowRoot?: boolean
5152
nonce?: string
53+
configureApp?: (app: App) => void
5254
}
5355

5456
// defineCustomElement provides the same type inference as defineComponent
@@ -165,14 +167,14 @@ export function defineCustomElement(
165167
/**
166168
* @internal
167169
*/
168-
hydrate?: RootHydrateFunction,
170+
_createApp?: CreateAppFunction<Element>,
169171
): VueElementConstructor {
170172
const Comp = defineComponent(options, extraOptions) as any
171173
if (isPlainObject(Comp)) extend(Comp, extraOptions)
172174
class VueCustomElement extends VueElement {
173175
static def = Comp
174176
constructor(initialProps?: Record<string, any>) {
175-
super(Comp, initialProps, hydrate)
177+
super(Comp, initialProps, _createApp)
176178
}
177179
}
178180

@@ -185,7 +187,7 @@ export const defineSSRCustomElement = ((
185187
extraOptions?: ComponentOptions,
186188
) => {
187189
// @ts-expect-error
188-
return defineCustomElement(options, extraOptions, hydrate)
190+
return defineCustomElement(options, extraOptions, createSSRApp)
189191
}) as typeof defineCustomElement
190192

191193
const BaseClass = (
@@ -202,6 +204,14 @@ export class VueElement
202204
* @internal
203205
*/
204206
_instance: ComponentInternalInstance | null = null
207+
/**
208+
* @internal
209+
*/
210+
_app: App | null = null
211+
/**
212+
* @internal
213+
*/
214+
_nonce = this._def.nonce
205215

206216
private _connected = false
207217
private _resolved = false
@@ -225,15 +235,19 @@ export class VueElement
225235
private _slots?: Record<string, Node[]>
226236

227237
constructor(
238+
/**
239+
* Component def - note this may be an AsyncWrapper, and this._def will
240+
* be overwritten by the inner component when resolved.
241+
*/
228242
private _def: InnerComponentDef,
229243
private _props: Record<string, any> = {},
230-
hydrate?: RootHydrateFunction,
244+
private _createApp: CreateAppFunction<Element> = createApp,
231245
) {
232246
super()
233-
// TODO handle non-shadowRoot hydration
234-
if (this.shadowRoot && hydrate) {
235-
hydrate(this._createVNode(), this.shadowRoot)
247+
if (this.shadowRoot && _createApp !== createApp) {
236248
this._root = this.shadowRoot
249+
// TODO hydration needs to be reworked
250+
this._mount(_def)
237251
} else {
238252
if (__DEV__ && this.shadowRoot) {
239253
warn(
@@ -303,9 +317,10 @@ export class VueElement
303317
this._ob.disconnect()
304318
this._ob = null
305319
}
306-
render(null, this._root)
320+
// unmount
321+
this._app && this._app.unmount()
307322
this._instance!.ce = undefined
308-
this._instance = null
323+
this._app = this._instance = null
309324
}
310325
})
311326
}
@@ -371,11 +386,8 @@ export class VueElement
371386
)
372387
}
373388

374-
// initial render
375-
this._update()
376-
377-
// apply expose
378-
this._applyExpose()
389+
// initial mount
390+
this._mount(def)
379391
}
380392

381393
const asyncDef = (this._def as ComponentOptions).__asyncLoader
@@ -388,6 +400,34 @@ export class VueElement
388400
}
389401
}
390402

403+
private _mount(def: InnerComponentDef) {
404+
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
405+
// @ts-expect-error
406+
def.name = 'VueElement'
407+
}
408+
this._app = this._createApp(def)
409+
if (def.configureApp) {
410+
def.configureApp(this._app)
411+
}
412+
this._app._ceVNode = this._createVNode()
413+
this._app.mount(this._root)
414+
415+
// apply expose after mount
416+
const exposed = this._instance && this._instance.exposed
417+
if (!exposed) return
418+
for (const key in exposed) {
419+
if (!hasOwn(this, key)) {
420+
// exposed properties are readonly
421+
Object.defineProperty(this, key, {
422+
// unwrap ref to be consistent with public instance behavior
423+
get: () => unref(exposed[key]),
424+
})
425+
} else if (__DEV__) {
426+
warn(`Exposed property "${key}" already exists on custom element.`)
427+
}
428+
}
429+
}
430+
391431
private _resolveProps(def: InnerComponentDef) {
392432
const { props } = def
393433
const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
@@ -412,22 +452,6 @@ export class VueElement
412452
}
413453
}
414454

415-
private _applyExpose() {
416-
const exposed = this._instance && this._instance.exposed
417-
if (!exposed) return
418-
for (const key in exposed) {
419-
if (!hasOwn(this, key)) {
420-
// exposed properties are readonly
421-
Object.defineProperty(this, key, {
422-
// unwrap ref to be consistent with public instance behavior
423-
get: () => unref(exposed[key]),
424-
})
425-
} else if (__DEV__) {
426-
warn(`Exposed property "${key}" already exists on custom element.`)
427-
}
428-
}
429-
}
430-
431455
protected _setAttr(key: string) {
432456
if (key.startsWith('data-v-')) return
433457
let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined
@@ -534,7 +558,7 @@ export class VueElement
534558
}
535559
this._styleChildren.add(owner)
536560
}
537-
const nonce = this._def.nonce
561+
const nonce = this._nonce
538562
for (let i = styles.length - 1; i >= 0; i--) {
539563
const s = document.createElement('style')
540564
if (nonce) s.setAttribute('nonce', nonce)

‎packages/runtime-dom/src/index.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ export const createApp = ((...args) => {
108108
// rendered by the server, the template should not contain any user data.
109109
component.template = container.innerHTML
110110
// 2.x compat check
111-
if (__COMPAT__ && __DEV__) {
112-
for (let i = 0; i < container.attributes.length; i++) {
113-
const attr = container.attributes[i]
111+
if (__COMPAT__ && __DEV__ && container.nodeType === 1) {
112+
for (let i = 0; i < (container as Element).attributes.length; i++) {
113+
const attr = (container as Element).attributes[i]
114114
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
115115
compatUtils.warnDeprecation(
116116
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
@@ -123,7 +123,9 @@ export const createApp = ((...args) => {
123123
}
124124

125125
// clear content before mounting
126-
container.textContent = ''
126+
if (container.nodeType === 1) {
127+
container.textContent = ''
128+
}
127129
const proxy = mount(container, false, resolveRootNamespace(container))
128130
if (container instanceof Element) {
129131
container.removeAttribute('v-cloak')
@@ -154,7 +156,9 @@ export const createSSRApp = ((...args) => {
154156
return app
155157
}) as CreateAppFunction<Element>
156158

157-
function resolveRootNamespace(container: Element): ElementNamespace {
159+
function resolveRootNamespace(
160+
container: Element | ShadowRoot,
161+
): ElementNamespace {
158162
if (container instanceof SVGElement) {
159163
return 'svg'
160164
}
@@ -215,7 +219,7 @@ function injectCompilerOptionsCheck(app: App) {
215219

216220
function normalizeContainer(
217221
container: Element | ShadowRoot | string,
218-
): Element | null {
222+
): Element | ShadowRoot | null {
219223
if (isString(container)) {
220224
const res = document.querySelector(container)
221225
if (__DEV__ && !res) {

0 commit comments

Comments
 (0)
Please sign in to comment.