diff --git a/CHANGELOG.md b/CHANGELOG.md index 6671e7ca5..a89d4d5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Changes +- [path](https://pikax.me/vue-composable/composable/format/path) - Improve [array path access](https://pikax.me/vue-composable/composable/format/path.html#access) and add dev warnings - [i18n](https://pikax.me/vue-composable/composable/i18n/i18n) - Allow to have factory based locale messages - [i18n](https://pikax.me/vue-composable/composable/i18n/i18n) - Added console warnings when removing locales - [i18n](https://pikax.me/vue-composable/composable/i18n/i18n) - Improve overriding locales diff --git a/docs/composable/format/path.md b/docs/composable/format/path.md index cec4f5146..2c8d1f28e 100644 --- a/docs/composable/format/path.md +++ b/docs/composable/format/path.md @@ -35,6 +35,39 @@ const name = usePath({ user: { name: "test" } }, "user.name"); | ----- | -------- | ------------------------------------------------- | | name | `Ref` | Readonly `ref` with the object value for the path | +## Access + +```js +const o = { + a: { + a: 1, + b: [ + 2, + { + c: { + ["a-b-c-d"]: 3 + } + } + ] + } +}; + +usePath(o, "a[a]"); // result: 1 | equivalent: a.a +usePath(o, "[a]['a']"); // result: 1 | equivalent: a.a +usePath(o, '["a"][`b`][0]'); // result: 2 | equivalent: a.b["0"] +usePath(o, "a.b[1].c[a-b-c-d]"); // result: 3 | equivalent: a.b[1].c["a-b-c-d"] +``` + +## Limitations + +The access in `[]` is limited to this regex expression: + +```regex + /\[[`'"]?([^`'"\]]*)[`'"]?\]/g +``` + +If you want to improve this, please raise an [issue](https://github.com/pikax/vue-composable/issues/new) or create a [PR](https://github.com/pikax/vue-composable/pulls) + ## Example diff --git a/packages/core/__tests__/format/path.spec.ts b/packages/core/__tests__/format/path.spec.ts index 0f0369448..d914d3bf5 100644 --- a/packages/core/__tests__/format/path.spec.ts +++ b/packages/core/__tests__/format/path.spec.ts @@ -1,4 +1,5 @@ import { usePath } from "../../src"; +import { ref } from "@vue/composition-api"; describe("path", () => { it("should return the object value", () => { @@ -35,4 +36,106 @@ describe("path", () => { expect(usePath(o, "array[1]").value).toBe(2); expect(usePath(o, "deep.x[1].a.b").value).toBe(1); }); + + describe("not found", () => { + const notFoundResolverMock = jest.fn().mockImplementation(() => "test"); + + const warnSpy = jest.spyOn(console, "warn"); + + beforeEach(() => { + notFoundResolverMock.mockClear(); + warnSpy.mockClear(); + }); + + test("source `undefined`", () => { + expect( + usePath(ref(undefined), "yey", undefined, notFoundResolverMock).value + ).toBe("test"); + expect(notFoundResolverMock).toHaveBeenLastCalledWith( + "yey", + undefined, + "yey", + undefined + ); + }); + + test("no path", () => { + const o = { + a: 1 + }; + expect(usePath(o, "", undefined, notFoundResolverMock).value).toBe(o); + expect(notFoundResolverMock).not.toBeCalled(); + }); + + test("first path not found", () => { + const o = { + a: 1 + }; + expect(usePath(o, "b", undefined, notFoundResolverMock).value).toBe( + "test" + ); + expect(notFoundResolverMock).toBeCalled(); + expect(warnSpy).toBeCalledWith(`Path "b" doesn't exist on:`, o); + }); + + test("deep path not found", () => { + const o = { + a: { + c: "hello" + } + }; + expect(usePath(o, "a.c.a", undefined, notFoundResolverMock).value).toBe( + "test" + ); + expect(notFoundResolverMock).toBeCalled(); + expect(warnSpy).toBeCalledWith(`Path "a.c.a" doesn't exist on:`, o); + }); + + test("if access with []", () => { + const o = {}; + expect(usePath(o, "[]", undefined, notFoundResolverMock).value).toBe( + "test" + ); + expect(warnSpy).toBeCalledWith(`Path "[]" doesn't exist on:`, o); + }); + + test("access with consecutive []", () => { + const o = { + a: { + a: 1, + b: [ + 2, + { + c: { + ["a-b-c-d"]: 3 + } + } + ] + } + }; + + expect(usePath(o, "a[a]").value).toBe(o.a.a); + expect(usePath(o, "[a]['a']").value).toBe(o.a.a); + expect(usePath(o, '["a"][`b`][0]').value).toBe(o.a.b[0]); + expect(usePath(o, "a.b[1].c[a-b-c-d]").value).toBe( + (o.a.b[1] as any).c["a-b-c-d"] + ); + }); + + test("invalid path parsing", () => { + expect(usePath({}, "a[a]o[a]").value).toBeUndefined(); + expect(warnSpy).toHaveBeenNthCalledWith( + 1, + `[usePath] invalid path "a[a]o[a]"` + ); + }); + + test("invalid array accessor", () => { + expect(usePath({}, "aa]").value).toBeUndefined(); + expect(warnSpy).toHaveBeenNthCalledWith( + 1, + `[usePath] invalid path provided "aa]"` + ); + }); + }); }); diff --git a/packages/core/src/format/path.ts b/packages/core/src/format/path.ts index a3a022173..c12084daf 100644 --- a/packages/core/src/format/path.ts +++ b/packages/core/src/format/path.ts @@ -28,36 +28,98 @@ export function usePath( let c = s; for (let i = 0; i < fragments.length; i++) { let fragmentPath = fragments[i]; - let index: any = -1; if (fragmentPath[fragmentPath.length - 1] === "]") { - const m = fragmentPath.match(/\[(\d+)\]$/); - if (m && m[1]) { - index = +m[1]; + const r = /\[[`'"]?([^`'"\]]*)[`'"]?\]/g; + let path = fragmentPath; + let m = r.exec(path); - fragmentPath = fragmentPath.slice(0, -m[0].length); + if (m) { + let lastLen = m[0].length; + let lastIndex = m.index - lastLen; + let mi = 1; + + do { + if (lastIndex + lastLen !== m.index) { + // istanbul ignore else + if (__DEV__) { + console.warn(`[usePath] invalid path "${fragments[i]}"`); + } + } + lastIndex = m.index; + lastLen = m[0].length; + + fragmentPath = fragmentPath.slice(0, -m[0].length); + fragments.splice(i + mi, 0, m[1]); + + ++mi; + } while ((m = r.exec(path))); + + // if the fragmentPath is empty, eg: [1][1] + // we should continue until the next path + if (!fragmentPath && path[0] === "[" && path.length > 2) { + continue; + } + } else { + fragmentPath = ""; + console.warn(`[usePath] invalid path provided "${path}"`); } } if (isObject(c)) { - c = c[fragmentPath]; - - // array like: when using ref with and array, it becomes arraylike object - if (index >= 0) { - c = (c as any)[index]; + if (!fragmentPath) { + // istanbul ignore else + if (__DEV__) { + console.warn( + `Path "${fragments + .slice(0, i + 1) + .join(separator)}" doesn't exist on:`, + source + ); + } + return notFoundReturn( + fragments.slice(0, i + 1).join(separator), + c, + p, + s + ); } + + c = c[fragmentPath]; } else { + // istanbul ignore else if (__DEV__) { console.warn( - `Path "${fragments.slice(0, i).join(separator)}" doesn't exist on:`, + `Path "${fragments + .slice(0, i + 1) + .join(separator)}" doesn't exist on:`, source ); } - return notFoundReturn(fragments.slice(0, i).join(separator), c, p, s); + return notFoundReturn( + fragments.slice(0, i + 1).join(separator), + c, + p, + s + ); } if (!c) { - return notFoundReturn(fragments.slice(0, i).join(separator), c, p, s); + // istanbul ignore else + if (__DEV__) { + console.warn( + `Path "${fragments + .slice(0, i + 1) + .join(separator)}" doesn't exist on:`, + source + ); + } + return notFoundReturn( + fragments.slice(0, i + 1).join(separator), + c, + p, + s + ); } }