Skip to content

Commit b991075

Browse files
authored
fix(custom-element): allow injecting values ​​from app context in nested elements (#13219)
close #13212)
1 parent d0253a0 commit b991075

File tree

4 files changed

+126
-8
lines changed

4 files changed

+126
-8
lines changed

packages/runtime-core/src/apiCreateApp.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { warn } from './warning'
2222
import { type VNode, cloneVNode, createVNode } from './vnode'
2323
import type { RootHydrateFunction } from './hydration'
2424
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
25-
import { NO, extend, isFunction, isObject } from '@vue/shared'
25+
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
2626
import { version } from '.'
2727
import { installAppCompatProperties } from './compat/global'
2828
import type { NormalizedPropsOptions } from './componentProps'
@@ -448,10 +448,18 @@ export function createAppAPI<HostElement>(
448448

449449
provide(key, value) {
450450
if (__DEV__ && (key as string | symbol) in context.provides) {
451-
warn(
452-
`App already provides property with key "${String(key)}". ` +
453-
`It will be overwritten with the new value.`,
454-
)
451+
if (hasOwn(context.provides, key as string | symbol)) {
452+
warn(
453+
`App already provides property with key "${String(key)}". ` +
454+
`It will be overwritten with the new value.`,
455+
)
456+
} else {
457+
// #13212, context.provides can inherit the provides object from parent on custom elements
458+
warn(
459+
`App already provides property with key "${String(key)}" inherited from its parent element. ` +
460+
`It will be overwritten with the new value.`,
461+
)
462+
}
455463
}
456464

457465
context.provides[key as string | symbol] = value

packages/runtime-core/src/apiInject.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ export function inject(
5959
// to support `app.use` plugins,
6060
// fallback to appContext's `provides` if the instance is at root
6161
// #11488, in a nested createApp, prioritize using the provides from currentApp
62-
const provides = currentApp
62+
// #13212, for custom elements we must get injected values from its appContext
63+
// as it already inherits the provides object from the parent element
64+
let provides = currentApp
6365
? currentApp._context.provides
6466
: instance
65-
? instance.parent == null
67+
? instance.parent == null || instance.ce
6668
? instance.vnode.appContext && instance.vnode.appContext.provides
6769
: instance.parent.provides
6870
: undefined

packages/runtime-dom/__tests__/customElement.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,101 @@ describe('defineCustomElement', () => {
708708
`<div>changedA! changedB!</div>`,
709709
)
710710
})
711+
712+
// #13212
713+
test('inherited from app context within nested elements', async () => {
714+
const outerValues: (string | undefined)[] = []
715+
const innerValues: (string | undefined)[] = []
716+
const innerChildValues: (string | undefined)[] = []
717+
718+
const Outer = defineCustomElement(
719+
{
720+
setup() {
721+
outerValues.push(
722+
inject<string>('shared'),
723+
inject<string>('outer'),
724+
inject<string>('inner'),
725+
)
726+
},
727+
render() {
728+
return h('div', [renderSlot(this.$slots, 'default')])
729+
},
730+
},
731+
{
732+
configureApp(app) {
733+
app.provide('shared', 'shared')
734+
app.provide('outer', 'outer')
735+
},
736+
},
737+
)
738+
739+
const Inner = defineCustomElement(
740+
{
741+
setup() {
742+
// ensure values are not self-injected
743+
provide('inner', 'inner-child')
744+
745+
innerValues.push(
746+
inject<string>('shared'),
747+
inject<string>('outer'),
748+
inject<string>('inner'),
749+
)
750+
},
751+
render() {
752+
return h('div', [renderSlot(this.$slots, 'default')])
753+
},
754+
},
755+
{
756+
configureApp(app) {
757+
app.provide('outer', 'override-outer')
758+
app.provide('inner', 'inner')
759+
},
760+
},
761+
)
762+
763+
const InnerChild = defineCustomElement({
764+
setup() {
765+
innerChildValues.push(
766+
inject<string>('shared'),
767+
inject<string>('outer'),
768+
inject<string>('inner'),
769+
)
770+
},
771+
render() {
772+
return h('div')
773+
},
774+
})
775+
776+
customElements.define('provide-from-app-outer', Outer)
777+
customElements.define('provide-from-app-inner', Inner)
778+
customElements.define('provide-from-app-inner-child', InnerChild)
779+
780+
container.innerHTML =
781+
'<provide-from-app-outer>' +
782+
'<provide-from-app-inner>' +
783+
'<provide-from-app-inner-child></provide-from-app-inner-child>' +
784+
'</provide-from-app-inner>' +
785+
'</provide-from-app-outer>'
786+
787+
const outer = container.childNodes[0] as VueElement
788+
expect(outer.shadowRoot!.innerHTML).toBe('<div><slot></slot></div>')
789+
790+
expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
791+
1,
792+
)
793+
expect(
794+
'[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
795+
'It will be overwritten with the new value.',
796+
).toHaveBeenWarnedTimes(1)
797+
798+
expect(outerValues).toEqual(['shared', 'outer', undefined])
799+
expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
800+
expect(innerChildValues).toEqual([
801+
'shared',
802+
'override-outer',
803+
'inner-child',
804+
])
805+
})
711806
})
712807

713808
describe('styles', () => {

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,18 @@ export class VueElement
316316
private _setParent(parent = this._parent) {
317317
if (parent) {
318318
this._instance!.parent = parent._instance
319-
this._instance!.provides = parent._instance!.provides
319+
this._inheritParentContext(parent)
320+
}
321+
}
322+
323+
private _inheritParentContext(parent = this._parent) {
324+
// #13212, the provides object of the app context must inherit the provides
325+
// object from the parent element so we can inject values from both places
326+
if (parent && this._app) {
327+
Object.setPrototypeOf(
328+
this._app._context.provides,
329+
parent._instance!.provides,
330+
)
320331
}
321332
}
322333

@@ -417,6 +428,8 @@ export class VueElement
417428
def.name = 'VueElement'
418429
}
419430
this._app = this._createApp(def)
431+
// inherit before configureApp to detect context overwrites
432+
this._inheritParentContext()
420433
if (def.configureApp) {
421434
def.configureApp(this._app)
422435
}

0 commit comments

Comments
 (0)