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 VNode, cloneVNode, createVNode } from './vnode'
|
||||||
import type { RootHydrateFunction } from './hydration'
|
import type { RootHydrateFunction } from './hydration'
|
||||||
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
|
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 { version } from '.'
|
||||||
import { installAppCompatProperties } from './compat/global'
|
import { installAppCompatProperties } from './compat/global'
|
||||||
import type { NormalizedPropsOptions } from './componentProps'
|
import type { NormalizedPropsOptions } from './componentProps'
|
||||||
|
@ -448,10 +448,18 @@ export function createAppAPI<HostElement>(
|
||||||
|
|
||||||
provide(key, value) {
|
provide(key, value) {
|
||||||
if (__DEV__ && (key as string | symbol) in context.provides) {
|
if (__DEV__ && (key as string | symbol) in context.provides) {
|
||||||
warn(
|
if (hasOwn(context.provides, key as string | symbol)) {
|
||||||
`App already provides property with key "${String(key)}". ` +
|
warn(
|
||||||
`It will be overwritten with the new value.`,
|
`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
|
context.provides[key as string | symbol] = value
|
||||||
|
|
|
@ -59,10 +59,12 @@ export function inject(
|
||||||
// to support `app.use` plugins,
|
// to support `app.use` plugins,
|
||||||
// fallback to appContext's `provides` if the instance is at root
|
// fallback to appContext's `provides` if the instance is at root
|
||||||
// #11488, in a nested createApp, prioritize using the provides from currentApp
|
// #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
|
? currentApp._context.provides
|
||||||
: instance
|
: instance
|
||||||
? instance.parent == null
|
? instance.parent == null || instance.ce
|
||||||
? instance.vnode.appContext && instance.vnode.appContext.provides
|
? instance.vnode.appContext && instance.vnode.appContext.provides
|
||||||
: instance.parent.provides
|
: instance.parent.provides
|
||||||
: undefined
|
: undefined
|
||||||
|
|
|
@ -708,6 +708,101 @@ describe('defineCustomElement', () => {
|
||||||
`<div>changedA! changedB!</div>`,
|
`<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', () => {
|
describe('styles', () => {
|
||||||
|
|
|
@ -316,7 +316,18 @@ export class VueElement
|
||||||
private _setParent(parent = this._parent) {
|
private _setParent(parent = this._parent) {
|
||||||
if (parent) {
|
if (parent) {
|
||||||
this._instance!.parent = parent._instance
|
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'
|
def.name = 'VueElement'
|
||||||
}
|
}
|
||||||
this._app = this._createApp(def)
|
this._app = this._createApp(def)
|
||||||
|
// inherit before configureApp to detect context overwrites
|
||||||
|
this._inheritParentContext()
|
||||||
if (def.configureApp) {
|
if (def.configureApp) {
|
||||||
def.configureApp(this._app)
|
def.configureApp(this._app)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue