]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(custom-element): allow injecting values ​​from app context in nested elements...
authorAdrian Cerbaro <adriancerbaro@gmail.com>
Fri, 16 May 2025 00:07:32 +0000 (21:07 -0300)
committerGitHub <noreply@github.com>
Fri, 16 May 2025 00:07:32 +0000 (08:07 +0800)
close #13212)

packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/apiInject.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index cba5e4ede02aedb52829cdab03e1beecec398729..b69a1ccd54087c219eea11b4bef51c3415d8ae66 100644 (file)
@@ -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<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
index f05d7333da6594b12aec1b545b3dc155d0fb02b4..711c5d84de825b5e3087c602593c15053ff2ae6e 100644 (file)
@@ -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
index df438d47eeeadabc0854871d233ea241b2e48021..943dfdc51f7e7cab5d9c9ecca1921cce314f139c 100644 (file)
@@ -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', () => {
index aeeaeec9b9f8445869107370666010ede7761526..cd21d0d1ce13b97d7347631127bab1350f240c20 100644 (file)
@@ -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)
     }