diff --git a/src/continuous-object-merge/proxy.ts b/src/continuous-object-merge/proxy.ts index 5306b92..e6327a5 100644 --- a/src/continuous-object-merge/proxy.ts +++ b/src/continuous-object-merge/proxy.ts @@ -21,7 +21,16 @@ export function createHandler (context: MetaContext, pathSegments: PathSegments const keyPath: PathSegments = [...pathSegments, key] const handler = /* #__PURE__ */ createHandler(context, keyPath) - value.__vm_proxy = createProxy(value, handler) + Object.defineProperty( + value, + '__vm_proxy', + { + configurable: false, + enumerable: false, + writable: false, + value: createProxy(value, handler) + } + ) } return value.__vm_proxy @@ -31,17 +40,25 @@ export function createHandler (context: MetaContext, pathSegments: PathSegments key: string, value: any ): boolean { - update(context, pathSegments, key, value) - // target[key] = value - return true + const success = Reflect.set(target, key, value) + + if (success) { + update(context, pathSegments, key, value) + } + + return success }, deleteProperty ( target: { [key: string]: any }, prop: string ) { - remove(context, pathSegments, prop) - delete target[prop] - return true + const success = Reflect.deleteProperty(target, prop) + + if (success) { + remove(context, pathSegments, prop) + } + + return success } } } diff --git a/src/continuous-object-merge/resolve.ts b/src/continuous-object-merge/resolve.ts index e82d170..4d4a0fa 100644 --- a/src/continuous-object-merge/resolve.ts +++ b/src/continuous-object-merge/resolve.ts @@ -1,4 +1,4 @@ -import { hasOwn } from '@vue/shared' +import { hasOwn, isArray } from '@vue/shared' import { clone } from '../utils' import { ActiveNode, GetActiveNode, MetaContext, PathSegments, ShadowNode, GetShadowNodes } from '../types' @@ -11,12 +11,19 @@ export function resolveActive ( ) { let value - const shadowLength = shadowParent[key] ? shadowParent[key].length : 0 + const isUpdatingArrayKey = isArray(activeParent) + + let shadowLength + if (isUpdatingArrayKey) { + shadowLength = shadowParent ? shadowParent.length : 0 + } else { + shadowLength = shadowParent[key] ? shadowParent[key].length : 0 + } if (shadowLength > 1) { // Is using freeze useful? Idea is to prevent the user from messing with these options by mistake - const getShadow: GetShadowNodes = () => Object.freeze(clone(shadowParent[key])) - const getActive: GetActiveNode = () => Object.freeze(clone(activeParent[key])) + const getShadow: GetShadowNodes = () => Object.freeze(clone(isUpdatingArrayKey ? shadowParent : shadowParent[key])) + const getActive: GetActiveNode = () => Object.freeze(clone(isUpdatingArrayKey ? activeParent : activeParent[key])) value = context.resolve( key, @@ -30,6 +37,18 @@ export function resolveActive ( if (value === undefined) { delete activeParent[key] + } else if (isUpdatingArrayKey) { + // set new values + for (const k in value) { + activeParent[k] = value[k] + } + + // delete old values + for (const k in activeParent) { + if (!(k in value)) { + delete activeParent[k] + } + } } else if (!hasOwn(activeParent, key) || activeParent[key] !== value) { activeParent[key] = value } diff --git a/src/continuous-object-merge/set.ts b/src/continuous-object-merge/set.ts index 9b591ff..72ae900 100644 --- a/src/continuous-object-merge/set.ts +++ b/src/continuous-object-merge/set.ts @@ -33,8 +33,6 @@ export function set ( activeParent[key], pathSegments ) - } else if (isArray(value)) { - } let idx = -1 @@ -83,7 +81,6 @@ export function set ( } // Step 3: Update the active data - resolveActive(context, key, pathSegments, shadowParent, activeParent) } diff --git a/test/unit/resolve.test.js b/test/unit/resolve.test.js index be00e12..4a8e05b 100644 --- a/test/unit/resolve.test.js +++ b/test/unit/resolve.test.js @@ -1,3 +1,4 @@ +import { isArray, isPlainObject } from '@vue/shared' import { createProxy, createHandler, setByObject, remove } from '../../src/continuous-object-merge' describe('resolve', () => { @@ -8,9 +9,45 @@ describe('resolve', () => { active = {} shadow = {} - const resolve = (key, pathSegments, getOptions, getCurrentValue) => { + const resolve = (_key, _pathSegments, getOptions, _getCurrentValue) => { const options = getOptions() console.log('RESOLVE', options) + + const hasArrayOption = options.some(option => isArray(option.value)) + if (hasArrayOption) { + const groupedOptions = {} + for (const option of options) { + console.log('OPTION', option) + if (!isArray(option.value)) { + continue + } + + for (const value of option.value) { + if (isPlainObject(value) && 'vmid' in value) { + groupedOptions[value.vmid] = value + } + } + } + console.log(groupedOptions) + const values = [] + for (const option of options) { + if (!isArray(option.value)) { + continue + } + + for (const value of option.value) { + if (!isPlainObject(value) || !('vmid' in value)) { + values.push(value) + } else if (groupedOptions[value.vmid]) { + values.push(groupedOptions[value.vmid]) + delete groupedOptions[value.vmid] + } + } + } + console.log('VALUES', values) + return values + } + return options[options.length - 1].value } @@ -32,7 +69,7 @@ describe('resolve', () => { // Init proxy const handler1 = createHandler(context1) - const proxy1 = createProxy(target1, handler1) + /* const proxy1 = */ createProxy(target1, handler1) setByObject(context2, target2) const handler2 = createHandler(context2) @@ -72,7 +109,7 @@ describe('resolve', () => { // Init proxy const handler1 = createHandler(context1) - const proxy1 = createProxy(target1, handler1) + /* const proxy1 = */ createProxy(target1, handler1) setByObject(context2, target2) const handler2 = createHandler(context2) @@ -102,7 +139,7 @@ describe('resolve', () => { expect(shadow.obj.key.length).toBe(0) }) - test.skip('resolve (array)', () => { + test('resolve (array)', () => { const target1 = { arr: [ 'array value 1' @@ -115,12 +152,8 @@ describe('resolve', () => { ] } - // TODO: test this, is specifying a resolver enough or do we need to add an array-merge option - - // Set initial value + // Set initial value & init proxy setByObject(context1, target1) - - // Init proxy const handler1 = createHandler(context1) const proxy1 = createProxy(target1, handler1) @@ -128,47 +161,52 @@ describe('resolve', () => { const handler2 = createHandler(context2) const proxy2 = createProxy(target2, handler2) - expect(active.arr).toBe('object value 2') + expect(active.arr).toEqual(['array value 1', 'array value 2']) - proxy2.obj.key = 'test' + proxy2.arr[0] = 'test 2' - expect(active.obj.key).toBe('test') + expect(active.arr).toEqual(['array value 1', 'test 2']) - proxy2.obj = { key: 'test again' } + proxy1.arr = ['test 1'] - expect(active.obj.key).toBe('test again') - expect(shadow.obj.key.length).toBe(2) - - remove(context2) - - expect(active.obj.key).toBe('object value 1') + expect(active.arr).toEqual(['test 1', 'test 2']) + expect(shadow.arr.length).toBe(2) remove(context1) - // TODO: should we clean up the obj ref too? - expect(active.obj).toEqual({}) + expect(active.arr).toEqual(['test 2']) - expect(active.obj.key).toBeUndefined() - expect(shadow.obj.key.length).toBe(0) + delete proxy2.arr + + // TODO: should we clean up the obj ref too? + expect(active.arr).toBeUndefined() + expect(shadow.arr.length).toBe(0) + + proxy1.arr = ['test again 1'] + expect(active.arr).toEqual(['test again 1']) + + proxy2.arr = [] + proxy2.arr[0] = 'test again 2' + expect(active.arr).toEqual(['test again 1', 'test again 2']) }) - test.skip('resolve (collection)', () => { + test('resolve (collection)', () => { const target1 = { arr: [ - { key: 'collection value 1' } + { key: 'collection value 1.1' }, + { vmid: 'a', key: 'collection value 1.2' } ] } const target2 = { arr: [ - { key: 'collection value 2' } + { vmid: 'a', key: 'collection value 2.1' }, + { vmid: 'b', key: 'collection value 2.2' } ] } - // Set initial value + // Set initial value & init proxy setByObject(context1, target1) - - // Init proxy const handler1 = createHandler(context1) const proxy1 = createProxy(target1, handler1) @@ -176,22 +214,56 @@ describe('resolve', () => { const handler2 = createHandler(context2) const proxy2 = createProxy(target2, handler2) - expect(active.str).toBe('string value 2') + expect(active.arr).toEqual([ + { key: 'collection value 1.1' }, + { vmid: 'a', key: 'collection value 2.1' }, + { vmid: 'b', key: 'collection value 2.2' } + ]) - proxy2.str = 'test' + proxy1.arr[0].key = 'test 1.1' + proxy1.arr[1].key = 'test 1.2' - expect(active.str).toBe('test') + expect(active.arr).toEqual([ + { key: 'test 1.1' }, + { vmid: 'a', key: 'test 1.2' }, // TODO: this is WRONG, should be collection value 2.1 => setting a prop in a collection needs to trigger the resolveActive for the parent array + { vmid: 'b', key: 'collection value 2.2' } + ]) - expect(shadow.str).toBeInstanceOf(Array) - expect(shadow.str.length).toBe(2) + proxy2.arr = [ + { vmid: 'b', key: 'collection value 2.1' }, + { vmid: 'c', key: 'collection value 2.2' } + ] - remove(context2) + expect(active.arr).toEqual([ + { key: 'test 1.1' }, + { vmid: 'a', key: 'test 1.2' }, + { vmid: 'b', key: 'collection value 2.1' }, + { vmid: 'c', key: 'collection value 2.2' } + ]) - expect(active.str).toBe('string value 1') + expect(shadow.arr.length).toBe(2) remove(context1) - expect(active.str).toBeUndefined() - expect(shadow.str.length).toBe(0) + expect(active.arr).toEqual([ + { vmid: 'b', key: 'collection value 2.1' }, + { vmid: 'c', key: 'collection value 2.2' } + ]) + + delete proxy2.arr + + // TODO: should we clean up the obj ref too? + expect(active.arr).toBeUndefined() + expect(shadow.arr.length).toBe(0) + + proxy1.arr = [{ vmid: 'a', key: 'test again 1' }] + expect(active.arr).toEqual([{ vmid: 'a', key: 'test again 1' }]) + + // TODO: fix + proxy2.arr = [] + proxy2.arr[0] = { vmid: 'a', value: 'test again 2' } + expect(active.arr).toEqual([ + { vmid: 'a', key: 'test again 2' } + ]) }) })