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'
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
// 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
`<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', () => {
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,
+ )
}
}
def.name = 'VueElement'
}
this._app = this._createApp(def)
+ // inherit before configureApp to detect context overwrites
+ this._inheritParentContext()
if (def.configureApp) {
def.configureApp(this._app)
}