diff --git a/CHANGELOG.md b/CHANGELOG.md index 4105d8c..2a06fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to **dot-diver** will be documented here. Inspired by [keep ## Unreleased +- Fixed `getByPath` not triggering reactivity on Proxy objects (fixes #34, thanks @Tofandel) + ## [1.0.6](https://github.com/clickbar/dot-diver/tree/1.0.6) (2024-03-25) - Fixed breaking change introduced in the type of SearchableObject diff --git a/package.json b/package.json index 4eef19c..366590d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@clickbar/eslint-config-typescript": "^10.2.0", "@types/node": "^20.12.12", + "@vue/reactivity": "^3.4.38", "eslint": "^8.57.0", "prettier": "^3.2.5", "typescript": "^5.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4372e52..c9cfd95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ devDependencies: '@types/node': specifier: ^20.12.12 version: 20.12.12 + '@vue/reactivity': + specifier: ^3.4.38 + version: 3.4.38 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -930,10 +933,20 @@ packages: vue-template-compiler: 2.7.16 dev: true + /@vue/reactivity@3.4.38: + resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==} + dependencies: + '@vue/shared': 3.4.38 + dev: true + /@vue/shared@3.4.27: resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} dev: true + /@vue/shared@3.4.38: + resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} + dev: true + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: diff --git a/src/index.ts b/src/index.ts index fe853fc..2f667d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -239,6 +239,7 @@ function getByPath & string>( if ( typeof current !== 'object' || current === null || + (current as SafeObject)[pathPart] === undefined || !hasOwnProperty.call(current, pathPart) ) { return undefined diff --git a/test/index.test.ts b/test/index.test.ts index f78eebc..649106c 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,5 @@ -import { expect, it } from 'vitest' +import { ref, reactive, computed } from '@vue/reactivity' +import { expect, it, test } from 'vitest' import { getByPath, setByPath } from '../src' @@ -261,3 +262,105 @@ it('Test for prototype pollution', () => { setByPath(object3, '__proto__.polluted', true) }).toThrowError('__proto__') }) + +test('Vue 3 ref/reactive support', () => { + const objectRef = ref({ + a: 'hello', + b: { + c: 42, + d: { + e: 'world', + }, + }, + f: [{ g: 'array-item-1' }, { g: 'array-item-2' }], + }) + + const value1 = getByPath(objectRef.value, 'a') // Output: 'hello' + + expect(value1).toBe('hello') + + const value2 = getByPath(objectRef.value, 'b.c') // Output: 42 + + expect(value2).toBe(42) + + const value3 = getByPath(objectRef.value, 'b.d') // Output: { e: 'world' } + expect(value3).toStrictEqual({ e: 'world' }) + + const value4 = getByPath(objectRef.value, 'f.0') // Output: { g: 'array-item-1' } + expect(value4).toStrictEqual({ g: 'array-item-1' }) + + const objectReactive = reactive({ + a: 'hello', + b: { + c: 42, + d: { + e: 'world', + }, + }, + f: [{ g: 'array-item-1' }, { g: 'array-item-2' }], + }) + + const value11 = getByPath(objectReactive, 'a') // Output: 'hello' + + expect(value11).toBe('hello') + + const value12 = getByPath(objectReactive, 'b.c') // Output: 42 + + expect(value12).toBe(42) + + const value13 = getByPath(objectReactive, 'b.d') // Output: { e: 'world' } + expect(value13).toStrictEqual({ e: 'world' }) + + const value14 = getByPath(objectReactive, 'f.0') // Output: { g: 'array-item-1' } + expect(value14).toStrictEqual({ g: 'array-item-1' }) +}) + +test('Vue 3 reactivity support', () => { + const object = ref({ + a: 'hello', + b: { + c: 42, + d: { + e: 'world', + }, + }, + f: [{ g: 'array-item-1' }, { g: 'array-item-2' }], + h: {} as Record, + }) + + const value1 = computed(() => getByPath(object.value, 'a')) // Output: 'hello' + expect(value1.value).toBe('hello') + + const value2 = computed(() => getByPath(object.value, 'b.c')) // Output: 42 + expect(value2.value).toBe(42) + + const value3 = computed(() => getByPath(object.value, 'b.d')) // Output: { e: 'world' } + expect(value3.value).toStrictEqual({ e: 'world' }) + + const value4 = computed(() => getByPath(object.value, 'f.0')) // Output: { g: 'array-item-1' } + expect(value4.value).toStrictEqual({ g: 'array-item-1' }) + + const value5 = computed(() => getByPath(object.value, 'h.j')) // Output: 'array-item-2' + expect(value5.value).toBe(undefined) + + setByPath(object.value, 'a', 'new hello') + setByPath(object.value, 'b.c', 100) + setByPath(object.value, 'b.d', { e: 'new world' }) + setByPath(object.value, 'f.0', { g: 'new array-item-1' }) + setByPath(object.value, 'h.j', 'new object-item-2') + + expect(object.value.a).toBe('new hello') + expect(object.value.b.c).toBe(100) + expect(object.value.b.d).toStrictEqual({ e: 'new world' }) + expect(object.value.f[0]).toStrictEqual({ g: 'new array-item-1' }) + expect(object.value.h.j).toBe('new object-item-2') + + expect(value1.value).toBe('new hello') + expect(value2.value).toBe(100) + expect(value3.value).toStrictEqual({ e: 'new world' }) + expect(value4.value).toStrictEqual({ g: 'new array-item-1' }) + expect(value5.value).toBe('new object-item-2') + + const value11 = computed(() => getByPath(object, 'value.a')) // undefined + expect(value11.value).toBe(undefined) // currently not supported to include the ref value in the path +})