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

close #13212)
This commit is contained in:
Adrian Cerbaro 2025-05-15 21:07:32 -03:00 committed by GitHub
parent d0253a0b7e
commit b9910755a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 126 additions and 8 deletions

View File

@ -22,7 +22,7 @@ import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode' import { type VNode, cloneVNode, createVNode } from './vnode'
import type { RootHydrateFunction } from './hydration' import type { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' 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 { version } from '.'
import { installAppCompatProperties } from './compat/global' import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps' import type { NormalizedPropsOptions } from './componentProps'
@ -448,10 +448,18 @@ export function createAppAPI<HostElement>(
provide(key, value) { provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) { if (__DEV__ && (key as string | symbol) in context.provides) {
warn( if (hasOwn(context.provides, key as string | symbol)) {
`App already provides property with key "${String(key)}". ` + warn(
`It will be overwritten with the new value.`, `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 context.provides[key as string | symbol] = value

View File

@ -59,10 +59,12 @@ export function inject(
// to support `app.use` plugins, // to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root // fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp // #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 ? currentApp._context.provides
: instance : instance
? instance.parent == null ? instance.parent == null || instance.ce
? instance.vnode.appContext && instance.vnode.appContext.provides ? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides : instance.parent.provides
: undefined : undefined

View File

@ -708,6 +708,101 @@ describe('defineCustomElement', () => {
`<div>changedA! changedB!</div>`, `<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', () => { describe('styles', () => {

View File

@ -316,7 +316,18 @@ export class VueElement
private _setParent(parent = this._parent) { private _setParent(parent = this._parent) {
if (parent) { if (parent) {
this._instance!.parent = parent._instance 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,
)
} }
} }
@ -417,6 +428,8 @@ export class VueElement
def.name = 'VueElement' def.name = 'VueElement'
} }
this._app = this._createApp(def) this._app = this._createApp(def)
// inherit before configureApp to detect context overwrites
this._inheritParentContext()
if (def.configureApp) { if (def.configureApp) {
def.configureApp(this._app) def.configureApp(this._app)
} }