Skip to content

fix(custom-elements): allow injecting values ​​from app context in nested elements (fix #13212) #13219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode'
import type { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, isFunction, isObject } from '@vue/shared'
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
import { version } from '.'
import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps'
Expand Down Expand Up @@ -449,10 +449,18 @@ export function createAppAPI<HostElement>(

provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
if (hasOwn(context.provides, key as string | symbol)) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
} else {
// #13212, context.provides can inherit the provides object from parent on custom elements
warn(
`App already provides property with key "${String(key)}" inherited from its parent element. ` +
`It will be overwritten with the new value.`,
)
}
}

context.provides[key as string | symbol] = value
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime-core/src/apiInject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ export function inject(
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp
const provides = currentApp
// #13212, for custom elements we must get injected values from its appContext
// as it already inherits the provides object from the parent element
let provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.parent == null || instance.ce
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
Expand Down
95 changes: 95 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,101 @@ describe('defineCustomElement', () => {
`<div>changedA! changedB!</div>`,
)
})

// #13212
test('inherited from app context within nested elements', async () => {
const outerValues: (string | undefined)[] = []
const innerValues: (string | undefined)[] = []
const innerChildValues: (string | undefined)[] = []

const Outer = defineCustomElement(
{
setup() {
outerValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div', [renderSlot(this.$slots, 'default')])
},
},
{
configureApp(app) {
app.provide('shared', 'shared')
app.provide('outer', 'outer')
},
},
)

const Inner = defineCustomElement(
{
setup() {
// ensure values are not self-injected
provide('inner', 'inner-child')

innerValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div', [renderSlot(this.$slots, 'default')])
},
},
{
configureApp(app) {
app.provide('outer', 'override-outer')
app.provide('inner', 'inner')
},
},
)

const InnerChild = defineCustomElement({
setup() {
innerChildValues.push(
inject<string>('shared'),
inject<string>('outer'),
inject<string>('inner'),
)
},
render() {
return h('div')
},
})

customElements.define('provide-from-app-outer', Outer)
customElements.define('provide-from-app-inner', Inner)
customElements.define('provide-from-app-inner-child', InnerChild)

container.innerHTML =
'<provide-from-app-outer>' +
'<provide-from-app-inner>' +
'<provide-from-app-inner-child></provide-from-app-inner-child>' +
'</provide-from-app-inner>' +
'</provide-from-app-outer>'

const outer = container.childNodes[0] as VueElement
expect(outer.shadowRoot!.innerHTML).toBe('<div><slot></slot></div>')

expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
1,
)
expect(
'[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
'It will be overwritten with the new value.',
).toHaveBeenWarnedTimes(1)

expect(outerValues).toEqual(['shared', 'outer', undefined])
expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
expect(innerChildValues).toEqual([
'shared',
'override-outer',
'inner-child',
])
})
})

describe('styles', () => {
Expand Down
15 changes: 14 additions & 1 deletion packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,18 @@ export class VueElement
private _setParent(parent = this._parent) {
if (parent) {
this._instance!.parent = parent._instance
this._instance!.provides = parent._instance!.provides
this._inheritParentContext(parent)
}
}

private _inheritParentContext(parent = this._parent) {
// #13212, the provides object of the app context must inherit the provides
// object from the parent element so we can inject values from both places
if (parent && this._app) {
Object.setPrototypeOf(
this._app._context.provides,
parent._instance!.provides,
)
}
}

Expand Down Expand Up @@ -417,6 +428,8 @@ export class VueElement
def.name = 'VueElement'
}
this._app = this._createApp(def)
// inherit before configureApp to detect context overwrites
this._inheritParentContext()
if (def.configureApp) {
def.configureApp(this._app)
}
Expand Down