diff --git a/src/reactivity/unwrap.ts b/src/reactivity/unwrap.ts new file mode 100644 index 00000000..bf306ac1 --- /dev/null +++ b/src/reactivity/unwrap.ts @@ -0,0 +1,51 @@ +import { isRef } from './ref' +import { proxy, isFunction, isObject, isArray } from '../utils' +import { isReactive } from './reactive' + +export function unwrapRefProxy(value: any) { + if (isFunction(value)) { + return value + } + + if (isRef(value)) { + return value + } + + if (isArray(value)) { + return value + } + + if (isReactive(value)) { + return value + } + + if (!isObject(value)) { + return value + } + + if (!Object.isExtensible(value)) { + return value + } + + const obj: any = {} + + // copy symbols over + Object.getOwnPropertySymbols(value).forEach( + (s) => (obj[s] = (value as any)[s]) + ) + + for (const k of Object.keys(value)) { + const r = value[k] + // if is a ref, create a proxy to retrieve the value, + if (isRef(r)) { + const set = (v: any) => (r.value = v) + const get = () => r.value + + proxy(obj, k, { get, set }) + } else { + obj[k] = unwrapRefProxy(r) + } + } + + return obj +} diff --git a/src/setup.ts b/src/setup.ts index de5bbe0f..b1a40ae2 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -11,6 +11,7 @@ import { resolveSlots, createSlotProxy } from './helper' import { hasOwn, isPlainObject, assert, proxy, warn, isFunction } from './utils' import { ref } from './apis/state' import vmStateManager from './vmStateManager' +import { unwrapRefProxy } from './reactivity/unwrap' import { markReactive } from './reactivity/reactive' function asVmProperty( @@ -220,7 +221,7 @@ export function mixin(Vue: VueConstructor) { bindingValue = bindingValue.bind(vm) } // a non-reactive should not don't get reactivity - bindingValue = ref(markRaw(bindingValue)) + bindingValue = ref(markRaw(unwrapRefProxy(bindingValue))) } } asVmProperty(vm, name, bindingValue) diff --git a/test/setup.spec.js b/test/setup.spec.js index 1c0841ad..e273de06 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -5,6 +5,7 @@ const { createElement: h, provide, inject, + reactive, toRefs, } = require('../src') @@ -466,6 +467,107 @@ describe('setup', () => { .then(done) }) + it('should unwrap on the template', () => { + const vm = new Vue({ + setup() { + const r = ref('r') + const nested = { + a: ref('a'), + aa: { + b: ref('aa'), + bb: { + cc: ref('aa'), + c: 'aa', + }, + }, + + aaa: reactive({ + b: ref('aaa'), + bb: { + c: ref('aaa'), + cc: 'aaa', + }, + }), + + aaaa: { + b: [1], + bb: ref([1]), + bbb: reactive({ + c: [1], + cc: ref([1]), + }), + bbbb: [ref(1)], + }, + } + + const refList = ref([ref('1'), ref('2'), ref('3')]) + const list = [ref('a'), ref('b')] + + return { + r, + nested, + refList, + list, + } + }, + template: `
+

{{r}}

+

{{nested.a}}

+

{{list}}

+

{{refList}}

+ +

{{ nested.aa.b }}

+

{{ nested.aa.bb.c }}

+

{{ nested.aa.bb.cc }}

+ +

{{ nested.aaa.b }}

+

{{ nested.aaa.bb.c }}

+

{{ nested.aaa.bb.cc }}

+ +

{{ nested.aaaa.b }}

+

{{ nested.aaaa.bb }}

+

{{ nested.aaaa.bbb.c }}

+

{{ nested.aaaa.bbb.cc }}

+

{{ nested.aaaa.bbbb }}

+
`, + }).$mount() + + expect(vm.$el.querySelector('#r').textContent).toBe('r') + expect(vm.$el.querySelector('#nested').textContent).toBe('a') + + // shouldn't unwrap arrays + expect( + JSON.parse(vm.$el.querySelector('#list').textContent) + ).toMatchObject([{ value: 'a' }, { value: 'b' }]) + expect( + JSON.parse(vm.$el.querySelector('#refList').textContent) + ).toMatchObject([{ value: '1' }, { value: '2' }, { value: '3' }]) + + expect(vm.$el.querySelector('#nested_aa_b').textContent).toBe('aa') + expect(vm.$el.querySelector('#nested_aa_bb_c').textContent).toBe('aa') + expect(vm.$el.querySelector('#nested_aa_bb_cc').textContent).toBe('aa') + + expect(vm.$el.querySelector('#nested_aaa_b').textContent).toBe('aaa') + expect(vm.$el.querySelector('#nested_aaa_bb_c').textContent).toBe('aaa') + expect(vm.$el.querySelector('#nested_aaa_bb_cc').textContent).toBe('aaa') + + expect( + JSON.parse(vm.$el.querySelector('#nested_aaaa_b').textContent) + ).toMatchObject([1]) + expect( + JSON.parse(vm.$el.querySelector('#nested_aaaa_bb_c').textContent) + ).toMatchObject([1]) + expect( + JSON.parse(vm.$el.querySelector('#nested_aaaa_bbb_cc').textContent) + ).toMatchObject([1]) + expect( + JSON.parse(vm.$el.querySelector('#nested_aaaa_bbb_cc').textContent) + ).toMatchObject([1]) + expect( + JSON.parse(vm.$el.querySelector('#nested_aaaa_bbbb').textContent) + ).toMatchObject([{ value: 1 }]) + }) + describe('Methods', () => { it('binds methods when calling with parenthesis', async () => { let context = null