diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
index f0b13904f08..5a7492d7015 100644
--- a/packages/runtime-core/src/helpers/renderSlot.ts
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -10,12 +10,12 @@ import {
type VNode,
type VNodeArrayChildren,
createBlock,
+ createVNode,
isVNode,
openBlock,
} from '../vnode'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { warn } from '../warning'
-import { createVNode } from '@vue/runtime-core'
import { isAsyncWrapper } from '../apiAsyncComponent'
/**
@@ -37,8 +37,19 @@ export function renderSlot(
isAsyncWrapper(currentRenderingInstance!.parent) &&
currentRenderingInstance!.parent.isCE)
) {
+ // in custom element mode, render as actual slot outlets
+ // wrap it with a fragment because in shadowRoot: false mode the slot
+ // element gets replaced by injected content
if (name !== 'default') props.name = name
- return createVNode('slot', props, fallback && fallback())
+ return (
+ openBlock(),
+ createBlock(
+ Fragment,
+ null,
+ [createVNode('slot', props, fallback && fallback())],
+ PatchFlags.STABLE_FRAGMENT,
+ )
+ )
}
let slot = slots[name]
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index f503b2c0285..ab4d6f47939 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -505,7 +505,7 @@ describe('defineCustomElement', () => {
})
customElements.define('my-el-slots', E)
- test('default slot', () => {
+ test('render slots correctly', () => {
container.innerHTML = `hi`
const e = container.childNodes[0] as VueElement
// native slots allocation does not affect innerHTML, so we just
@@ -777,4 +777,71 @@ describe('defineCustomElement', () => {
)
})
})
+
+ describe('shadowRoot: false', () => {
+ const E = defineCustomElement({
+ shadowRoot: false,
+ props: {
+ msg: {
+ type: String,
+ default: 'hello',
+ },
+ },
+ render() {
+ return h('div', this.msg)
+ },
+ })
+ customElements.define('my-el-shadowroot-false', E)
+
+ test('should work', async () => {
+ function raf() {
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve)
+ })
+ }
+
+ container.innerHTML = ``
+ const e = container.childNodes[0] as VueElement
+ await raf()
+ expect(e).toBeInstanceOf(E)
+ expect(e._instance).toBeTruthy()
+ expect(e.innerHTML).toBe(`
hello
`)
+ expect(e.shadowRoot).toBe(null)
+ })
+
+ const toggle = ref(true)
+ const ES = defineCustomElement({
+ shadowRoot: false,
+ render() {
+ return [
+ renderSlot(this.$slots, 'default'),
+ toggle.value ? renderSlot(this.$slots, 'named') : null,
+ renderSlot(this.$slots, 'omitted', {}, () => [h('div', 'fallback')]),
+ ]
+ },
+ })
+ customElements.define('my-el-shadowroot-false-slots', ES)
+
+ test('should render slots', async () => {
+ container.innerHTML =
+ `` +
+ `defaulttext` +
+ `named
` +
+ ``
+ 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.innerHTML).toBe(
+ `defaulttext` +
+ `named
` +
+ `fallback
`,
+ )
+
+ toggle.value = false
+ await nextTick()
+ expect(e.innerHTML).toBe(
+ `defaulttext` + `` + `fallback
`,
+ )
+ })
+ })
})
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index 97a84ee918d..f87bf266418 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -21,6 +21,7 @@ import {
type SetupContext,
type SlotsType,
type VNode,
+ type VNodeProps,
createVNode,
defineComponent,
nextTick,
@@ -33,21 +34,28 @@ export type VueElementConstructor = {
new (initialProps?: Record): VueElement & P
}
+export interface CustomElementOptions {
+ styles?: string[]
+ shadowRoot?: boolean
+}
+
// 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: Props, ctx: SetupContext) => RawBindings | RenderFunction,
- options?: Pick & {
- props?: (keyof Props)[]
- },
+ options?: Pick &
+ CustomElementOptions & {
+ props?: (keyof Props)[]
+ },
): VueElementConstructor
export function defineCustomElement(
setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
- options?: Pick & {
- props?: ComponentObjectPropsOptions
- },
+ options?: Pick &
+ CustomElementOptions & {
+ props?: ComponentObjectPropsOptions
+ },
): VueElementConstructor
// overload 2: defineCustomElement with options object, infer props from options
@@ -81,27 +89,27 @@ export function defineCustomElement<
: { [key in PropsKeys]?: any },
ResolvedProps = InferredProps & EmitsToProps,
>(
- options: {
+ options: CustomElementOptions & {
props?: (RuntimePropsOptions & ThisType) | PropsKeys[]
} & ComponentOptionsBase<
- ResolvedProps,
- SetupBindings,
- Data,
- Computed,
- Methods,
- Mixin,
- Extends,
- RuntimeEmitsOptions,
- EmitsKeys,
- {}, // Defaults
- InjectOptions,
- InjectKeys,
- Slots,
- LocalComponents,
- Directives,
- Exposed,
- Provide
- > &
+ ResolvedProps,
+ SetupBindings,
+ Data,
+ Computed,
+ Methods,
+ Mixin,
+ Extends,
+ RuntimeEmitsOptions,
+ EmitsKeys,
+ {}, // Defaults
+ InjectOptions,
+ InjectKeys,
+ Slots,
+ LocalComponents,
+ Directives,
+ Exposed,
+ Provide
+ > &
ThisType<
CreateComponentPublicInstanceWithMixins<
Readonly,
@@ -163,7 +171,7 @@ const BaseClass = (
typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
) as typeof HTMLElement
-type InnerComponentDef = ConcreteComponent & { styles?: string[] }
+type InnerComponentDef = ConcreteComponent & CustomElementOptions
export class VueElement extends BaseClass {
/**
@@ -176,14 +184,19 @@ export class VueElement extends BaseClass {
private _numberProps: Record | null = null
private _styles?: HTMLStyleElement[]
private _ob?: MutationObserver | null = null
+ private _root: Element | ShadowRoot
+ private _slots?: Record
+
constructor(
private _def: InnerComponentDef,
private _props: Record = {},
hydrate?: RootHydrateFunction,
) {
super()
+ // TODO handle non-shadowRoot hydration
if (this.shadowRoot && hydrate) {
hydrate(this._createVNode(), this.shadowRoot)
+ this._root = this.shadowRoot
} else {
if (__DEV__ && this.shadowRoot) {
warn(
@@ -191,7 +204,12 @@ export class VueElement extends BaseClass {
`defined as hydratable. Use \`defineSSRCustomElement\`.`,
)
}
- this.attachShadow({ mode: 'open' })
+ if (_def.shadowRoot !== false) {
+ this.attachShadow({ mode: 'open' })
+ this._root = this.shadowRoot!
+ } else {
+ this._root = this
+ }
if (!(this._def as ComponentOptions).__asyncLoader) {
// for sync component defs we can immediately resolve props
this._resolveProps(this._def)
@@ -200,6 +218,9 @@ export class VueElement extends BaseClass {
}
connectedCallback() {
+ if (!this.shadowRoot) {
+ this._parseSlots()
+ }
this._connected = true
if (!this._instance) {
if (this._resolved) {
@@ -218,7 +239,7 @@ export class VueElement extends BaseClass {
this._ob.disconnect()
this._ob = null
}
- render(null, this.shadowRoot!)
+ render(null, this._root)
this._instance = null
}
})
@@ -353,11 +374,16 @@ export class VueElement extends BaseClass {
}
private _update() {
- render(this._createVNode(), this.shadowRoot!)
+ render(this._createVNode(), this._root)
}
private _createVNode(): VNode {
- const vnode = createVNode(this._def, extend({}, this._props))
+ const baseProps: VNodeProps = {}
+ if (!this.shadowRoot) {
+ baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
+ this._renderSlots.bind(this)
+ }
+ const vnode = createVNode(this._def, extend(baseProps, this._props))
if (!this._instance) {
vnode.ce = instance => {
this._instance = instance
@@ -367,7 +393,7 @@ export class VueElement extends BaseClass {
instance.ceReload = newStyles => {
// always reset styles
if (this._styles) {
- this._styles.forEach(s => this.shadowRoot!.removeChild(s))
+ this._styles.forEach(s => this._root.removeChild(s))
this._styles.length = 0
}
this._applyStyles(newStyles)
@@ -416,7 +442,7 @@ export class VueElement extends BaseClass {
styles.forEach(css => {
const s = document.createElement('style')
s.textContent = css
- this.shadowRoot!.appendChild(s)
+ this._root.appendChild(s)
// record for HMR
if (__DEV__) {
;(this._styles || (this._styles = [])).push(s)
@@ -424,4 +450,50 @@ export class VueElement extends BaseClass {
})
}
}
+
+ /**
+ * Only called when shaddowRoot is false
+ */
+ private _parseSlots() {
+ const slots: VueElement['_slots'] = (this._slots = {})
+ let n
+ while ((n = this.firstChild)) {
+ const slotName =
+ (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
+ ;(slots[slotName] || (slots[slotName] = [])).push(n)
+ this.removeChild(n)
+ }
+ }
+
+ /**
+ * Only called when shaddowRoot is false
+ */
+ private _renderSlots() {
+ const outlets = this.querySelectorAll('slot')
+ const scopeId = this._instance!.type.__scopeId
+ for (let i = 0; i < outlets.length; i++) {
+ const o = outlets[i] as HTMLSlotElement
+ const slotName = o.getAttribute('name') || 'default'
+ const content = this._slots![slotName]
+ const parent = o.parentNode!
+ if (content) {
+ for (const n of content) {
+ // for :slotted css
+ if (scopeId && n.nodeType === 1) {
+ const id = scopeId + '-s'
+ const walker = document.createTreeWalker(n, 1)
+ ;(n as Element).setAttribute(id, '')
+ let child
+ while ((child = walker.nextNode())) {
+ ;(child as Element).setAttribute(id, '')
+ }
+ }
+ parent.insertBefore(n, o)
+ }
+ } else {
+ while (o.firstChild) parent.insertBefore(o.firstChild, o)
+ }
+ parent.removeChild(o)
+ }
+ }
}