mirror of https://github.com/vuejs/core.git
fix(custom-element): allow injecting values from app context in nested elements (#13219)
close #13212)
This commit is contained in:
parent
d0253a0b7e
commit
b9910755a5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue