From: Adrian Cerbaro Date: Fri, 16 May 2025 00:07:32 +0000 (-0300) Subject: fix(custom-element): allow injecting values ​​from app context in nested elements... X-Git-Tag: v3.5.15~30 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b9910755a50c7d6c52b28c3aef20cf97810295c9;p=thirdparty%2Fvuejs%2Fcore.git fix(custom-element): allow injecting values ​​from app context in nested elements (#13219) close #13212) --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index cba5e4ede0..b69a1ccd54 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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' @@ -448,10 +448,18 @@ export function createAppAPI( 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 diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index f05d7333da..711c5d84de 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -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 diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index df438d47ee..943dfdc51f 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -708,6 +708,101 @@ describe('defineCustomElement', () => { `
changedA! changedB!
`, ) }) + + // #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('shared'), + inject('outer'), + inject('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('shared'), + inject('outer'), + inject('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('shared'), + inject('outer'), + inject('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 = + '' + + '' + + '' + + '' + + '' + + const outer = container.childNodes[0] as VueElement + expect(outer.shadowRoot!.innerHTML).toBe('
') + + 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', () => { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index aeeaeec9b9..cd21d0d1ce 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -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, + ) } } @@ -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) }