From 561e210157874b216efc1c17be701a6a81c4383b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 27 Jul 2021 17:52:31 -0400 Subject: [PATCH] fix(inject): should auto unwrap injected refs fix #4196 --- .../runtime-core/__tests__/apiOptions.spec.ts | 66 ++++++++++++++++++- packages/runtime-core/src/apiCreateApp.ts | 12 +++- packages/runtime-core/src/componentOptions.ts | 48 +++++++++++--- .../__tests__/customElement.spec.ts | 8 ++- 4 files changed, 119 insertions(+), 15 deletions(-) diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index 10ede8a4638..9c50664b786 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -9,7 +9,8 @@ import { renderToString, ref, defineComponent, - createApp + createApp, + computed } from '@vue/runtime-test' describe('api: options', () => { @@ -426,6 +427,69 @@ describe('api: options', () => { expect(renderToString(h(Root))).toBe(`1111234522`) }) + test('provide/inject refs', async () => { + const n = ref(0) + const np = computed(() => n.value + 1) + const Parent = defineComponent({ + provide() { + return { + n, + np + } + }, + render: () => h(Child) + }) + const Child = defineComponent({ + inject: ['n', 'np'], + render(this: any) { + return this.n + this.np + } + }) + const app = createApp(Parent) + // TODO remove in 3.3 + app.config.unwrapInjectedRef = true + const root = nodeOps.createElement('div') + app.mount(root) + expect(serializeInner(root)).toBe(`1`) + + n.value++ + await nextTick() + expect(serializeInner(root)).toBe(`3`) + }) + + // TODO remove in 3.3 + test('provide/inject refs (compat)', async () => { + const n = ref(0) + const np = computed(() => n.value + 1) + const Parent = defineComponent({ + provide() { + return { + n, + np + } + }, + render: () => h(Child) + }) + const Child = defineComponent({ + inject: ['n', 'np'], + render(this: any) { + return this.n.value + this.np.value + } + }) + const app = createApp(Parent) + + const root = nodeOps.createElement('div') + app.mount(root) + expect(serializeInner(root)).toBe(`1`) + + n.value++ + await nextTick() + expect(serializeInner(root)).toBe(`3`) + + expect(`injected property "n" is a ref`).toHaveBeenWarned() + expect(`injected property "np" is a ref`).toHaveBeenWarned() + }) + test('provide accessing data in extends', () => { const Base = defineComponent({ data() { diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index a22ec8c0d7f..3e438107da0 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -81,16 +81,22 @@ export interface AppConfig { trace: string ) => void + /** + * Options to pass to @vue/compiler-dom. + * Only supported in runtime compiler build. + */ + compilerOptions: RuntimeCompilerOptions + /** * @deprecated use config.compilerOptions.isCustomElement */ isCustomElement?: (tag: string) => boolean /** - * Options to pass to @vue/compiler-dom. - * Only supported in runtime compiler build. + * Temporary config for opt-in to unwrap injected refs. + * TODO deprecate in 3.3 */ - compilerOptions: RuntimeCompilerOptions + unwrapInjectedRef?: boolean } export interface AppContext { diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index bffb215cd5c..c10ef97d0e3 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -17,7 +17,7 @@ import { NOOP, isPromise } from '@vue/shared' -import { computed } from '@vue/reactivity' +import { computed, isRef, Ref } from '@vue/reactivity' import { watch, WatchOptions, @@ -607,15 +607,21 @@ export function applyOptions(instance: ComponentInternalInstance) { // - watch (deferred since it relies on `this` access) if (injectOptions) { - resolveInjections(injectOptions, ctx, checkDuplicateProperties) + resolveInjections( + injectOptions, + ctx, + checkDuplicateProperties, + instance.appContext.config.unwrapInjectedRef + ) } if (methods) { for (const key in methods) { const methodHandler = (methods as MethodOptions)[key] if (isFunction(methodHandler)) { - // In dev mode, we use the `createRenderContext` function to define methods to the proxy target, - // and those are read-only but reconfigurable, so it needs to be redefined here + // In dev mode, we use the `createRenderContext` function to define + // methods to the proxy target, and those are read-only but + // reconfigurable, so it needs to be redefined here if (__DEV__) { Object.defineProperty(ctx, key, { value: methodHandler.bind(publicThis), @@ -810,25 +816,51 @@ export function applyOptions(instance: ComponentInternalInstance) { export function resolveInjections( injectOptions: ComponentInjectOptions, ctx: any, - checkDuplicateProperties = NOOP as any + checkDuplicateProperties = NOOP as any, + unwrapRef = false ) { if (isArray(injectOptions)) { injectOptions = normalizeInject(injectOptions)! } for (const key in injectOptions) { const opt = (injectOptions as ObjectInjectOptions)[key] + let injected: unknown if (isObject(opt)) { if ('default' in opt) { - ctx[key] = inject( + injected = inject( opt.from || key, opt.default, true /* treat default function as factory */ ) } else { - ctx[key] = inject(opt.from || key) + injected = inject(opt.from || key) + } + } else { + injected = inject(opt) + } + if (isRef(injected)) { + // TODO remove the check in 3.3 + if (unwrapRef) { + Object.defineProperty(ctx, key, { + enumerable: true, + configurable: true, + get: () => (injected as Ref).value, + set: v => ((injected as Ref).value = v) + }) + } else { + if (__DEV__) { + warn( + `injected property "${key}" is a ref and will be auto-unwrapped ` + + `and no longer needs \`.value\` in the next minor release. ` + + `To opt-in to the new behavior now, ` + + `set \`app.config.unwrapInjectedRef = true\` (this config is ` + + `temporary and will not be needed in the future.)` + ) + } + ctx[key] = injected } } else { - ctx[key] = inject(opt) + ctx[key] = injected } if (__DEV__) { checkDuplicateProperties!(OptionTypes.INJECT, key) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 1a4437ed360..e2e444073f0 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1,7 +1,9 @@ import { defineCustomElement, h, + inject, nextTick, + Ref, ref, renderSlot, VueElement @@ -231,9 +233,9 @@ describe('defineCustomElement', () => { describe('provide/inject', () => { const Consumer = defineCustomElement({ - inject: ['foo'], - render(this: any) { - return h('div', this.foo.value) + setup() { + const foo = inject('foo')! + return () => h('div', foo.value) } }) customElements.define('my-consumer', Consumer)