diff --git a/docs/.vuepress/components/TimelineExample.vue b/docs/.vuepress/components/TimelineExample.vue new file mode 100644 index 000000000..a364eca6b --- /dev/null +++ b/docs/.vuepress/components/TimelineExample.vue @@ -0,0 +1,28 @@ + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7dc67ffb6..f2d168a0c 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -219,7 +219,10 @@ module.exports = { title: "state", sidebarDepth: 1, collapsable: false, - children: [["composable/state/undo", "Undo"]] + children: [ + ["composable/state/timeline", "Timeline"], + ["composable/state/undo", "Undo"] + ] }, { title: "External", diff --git a/docs/README.md b/docs/README.md index 11a82752f..d01e6dd85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -116,6 +116,7 @@ Check out the [examples folder](examples) or start hacking on [codesandbox](http ### State +- [Timeline](composable/state/timeline) - Tracks variable history - [Undo](composable/state/undo) - Tracks variable history, to allow `undo` and `redo` ### Web diff --git a/docs/api/axios.api.md b/docs/api/axios.api.md index e32904b2a..36ee44aa7 100644 --- a/docs/api/axios.api.md +++ b/docs/api/axios.api.md @@ -3,46 +3,34 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AxiosInstance } from "axios"; -import { AxiosRequestConfig } from "axios"; -import { AxiosResponse } from "axios"; -import { ComputedRef } from "@vue/runtime-core"; -import { PromiseResultFactory } from "vue-composable"; -import { Ref } from "@vue/runtime-core"; + +import { AxiosInstance } from 'axios'; +import { AxiosRequestConfig } from 'axios'; +import { AxiosResponse } from 'axios'; +import { ComputedRef } from '@vue/runtime-core'; +import { PromiseResultFactory } from 'vue-composable'; +import { Ref } from '@vue/runtime-core'; // Warning: (ae-forgotten-export) The symbol "MakeAxiosReturn" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function makeAxios( - client: AxiosInstance, - throwException?: boolean -): MakeAxiosReturn; +export function makeAxios(client: AxiosInstance, throwException?: boolean): MakeAxiosReturn; // Warning: (ae-forgotten-export) The symbol "AxiosReturn" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function useAxios( - throwException?: boolean -): AxiosReturn; +export function useAxios(throwException?: boolean): AxiosReturn; // @public (undocumented) -export function useAxios( - url: string, - config?: AxiosRequestConfig, - throwException?: boolean -): AxiosReturn; +export function useAxios(url: string, config?: AxiosRequestConfig, throwException?: boolean): AxiosReturn; // @public (undocumented) -export function useAxios( - url: string, - throwException?: boolean -): AxiosReturn; +export function useAxios(url: string, throwException?: boolean): AxiosReturn; // @public (undocumented) -export function useAxios( - config?: AxiosRequestConfig, - throwException?: boolean -): AxiosReturn; +export function useAxios(config?: AxiosRequestConfig, throwException?: boolean): AxiosReturn; + // (No @packageDocumentation comment for this package) + ``` diff --git a/docs/api/vue-composable.api.md b/docs/api/vue-composable.api.md index 503a7c2cb..05fd8296b 100644 --- a/docs/api/vue-composable.api.md +++ b/docs/api/vue-composable.api.md @@ -4,6 +4,7 @@ ```ts import { ComputedRef } from "@vue/runtime-core"; +import { DeepReadonly } from "@vue/runtime-core"; import { InjectionKey } from "@vue/runtime-core"; import { Plugin as Plugin_2 } from "@vue/runtime-core"; import { provide } from "@vue/runtime-core"; @@ -1008,6 +1009,24 @@ export interface StorageSerializer { stringify(item: T): string; } +// @public (undocumented) +export interface TimelineEntry { + // (undocumented) + date: Date; + // (undocumented) + item: T; +} + +// @public (undocumented) +export interface TimelineOptions { + // (undocumented) + clone: (entry: T) => T; + // (undocumented) + deep: boolean; + // (undocumented) + maxLength: number; +} + // @public (undocumented) export interface UndoOperation { (step: number): void; @@ -1868,6 +1887,12 @@ export function useStorage( sync?: boolean ): LocalStorageReturn; +// @public (undocumented) +export function useTimeline( + value: Ref, + options?: Partial> +): DeepReadonly[]>>; + // @public (undocumented) export function useTitle(overrideTitle?: string | null): Ref; diff --git a/docs/composable/state/timeline.md b/docs/composable/state/timeline.md new file mode 100644 index 000000000..e04a5c10f --- /dev/null +++ b/docs/composable/state/timeline.md @@ -0,0 +1,86 @@ +# useTimeline + +> Tracks variable history + +## Parameters + +```js +import { useTimeline } from "vue-composable"; + +const options = { + deep: Boolean, + maxLength: Number, + clone(entry: T): T +} + +const timeline = useTimeline(value, options); +``` + +| Parameters | Type | Required | Default | Description | +| ---------- | -------------------- | -------- | ----------------------------------------------------------- | ---------------------- | +| value | `Ref` | `true` | | `ref` to track history | +| options | `TimelineOptions` | `false` | `{ deep: false, maxLength: MAX_ARRAY_SIZE, clone: (x)=>x }` | timeline options | + +::: tip + +If tracking object please provide a `options.clone` function. + +```ts +// example +function clone(e) { + return JSON.parse(JSON.stringify(e)); +} +``` + +::: + +## State + +The `useTimeline` function exposes the following reactive state: + +```js +import { useTimeline } from "vue-composable"; + +const timeline = useTimeline(); +``` + +| State | Type | Description | +| -------- | ------------------------------ | ---------------- | +| timeline | `Ref<{item: T, date: Date}[]>` | `timeline` array | + +## Example + + + +### Code + +```vue + + + +``` diff --git a/docs/composable/state/undo.md b/docs/composable/state/undo.md index 6ae1f8072..734b155d5 100644 --- a/docs/composable/state/undo.md +++ b/docs/composable/state/undo.md @@ -46,6 +46,19 @@ useUndo(defaultValue?, options?); | defaultValue | `Ref|T` | `false` | `undefined` | Default value | | options | `(x: T)=>T` | `false` | `defaultOptions` | Configuration options | +::: tip + +If tracking object please provide a `options.clone` function. + +```ts +// example +function clone(e) { + return JSON.parse(JSON.stringify(e)); +} +``` + +::: + ## State The `useUndo` function exposes the following reactive state: diff --git a/packages/axios/package.json b/packages/axios/package.json index 0ecf24c3f..bc6f6cbab 100644 --- a/packages/axios/package.json +++ b/packages/axios/package.json @@ -61,7 +61,7 @@ "typescript": "^3.9.7" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-beta.2", + "@vue/runtime-core": "^3.0.0-rc.2", "axios": "^0.19.2" } -} \ No newline at end of file +} diff --git a/packages/vue-composable/README.md b/packages/vue-composable/README.md index c72bc409a..8ef21236e 100644 --- a/packages/vue-composable/README.md +++ b/packages/vue-composable/README.md @@ -118,6 +118,7 @@ Check our [documentation](https://pikax.me/vue-composable/) ### State +- [Timeline](https://pikax.me/vue-composable/composable/state/timeline) - Tracks variable history - [Undo](https://pikax.me/vue-composable/composable/state/undo) - Tracks variable history, to allow `undo` and `redo` ### Web diff --git a/packages/vue-composable/__tests__/state/timeline.spec.ts b/packages/vue-composable/__tests__/state/timeline.spec.ts new file mode 100644 index 000000000..4451d859a --- /dev/null +++ b/packages/vue-composable/__tests__/state/timeline.spec.ts @@ -0,0 +1,62 @@ +import { useTimeline } from "../../src"; +import { ref } from "../../src/api"; + +describe("timeline", () => { + it("should work", () => { + const value = ref(""); + const timeline = useTimeline(value); + + expect(timeline.value).toMatchObject([]); + + value.value = "1"; + expect(timeline.value).toMatchObject([{ item: "" }]); + + value.value = "2"; + expect(timeline.value).toMatchObject([{ item: "1" }, { item: "" }]); + }); + + it("should not store more than the maxLength", () => { + const value = ref(""); + const timeline = useTimeline(value, { maxLength: 1 }); + + expect(timeline.value).toMatchObject([]); + + value.value = "1"; + expect(timeline.value).toMatchObject([{ item: "" }]); + + value.value = "2"; + expect(timeline.value).toMatchObject([{ item: "1" }]); + }); + + it("should use the clone option", () => { + const value = ref(""); + const clone = jest.fn().mockImplementation(x => `x${x}`); + const timeline = useTimeline(value, { clone }); + + expect(timeline.value).toMatchObject([]); + + value.value = "1"; + expect(clone).toHaveBeenCalledTimes(1); + expect(timeline.value).toMatchObject([{ item: "x" }]); + + value.value = "2"; + expect(clone).toHaveBeenCalledTimes(2); + expect(timeline.value).toMatchObject([{ item: "x1" }, { item: "x" }]); + }); + + it("should watch deep changes", () => { + const value = ref({ a: 1 }); + const timeline = useTimeline(value, { deep: true }); + + expect(timeline.value).toMatchObject([]); + + value.value.a++; + expect(timeline.value).toMatchObject([{ item: value.value }]); + + value.value.a++; + expect(timeline.value).toMatchObject([ + { item: value.value }, + { item: value.value } + ]); + }); +}); diff --git a/packages/vue-composable/package.json b/packages/vue-composable/package.json index fb53fc122..0b08bded9 100644 --- a/packages/vue-composable/package.json +++ b/packages/vue-composable/package.json @@ -51,7 +51,6 @@ "vue": "^2.6.10" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-beta.2", - "vue": "^2.6.10" + "@vue/runtime-core": "^3.0.0-rc.2" } -} \ No newline at end of file +} diff --git a/packages/vue-composable/src/api.2.ts b/packages/vue-composable/src/api.2.ts index ca53a7f78..9524ca244 100644 --- a/packages/vue-composable/src/api.2.ts +++ b/packages/vue-composable/src/api.2.ts @@ -25,6 +25,7 @@ export { VueConstructor as App } from "vue"; import { Ref, set, computed } from "@vue/composition-api"; import Vue, { PluginFunction } from "vue"; +import { unwrap } from "./utils"; export type Plugin = PluginFunction; @@ -32,6 +33,11 @@ export const vueDelete = (x: any, o: string) => Vue.delete(x, o); export const vueSet = set; // FAKE readonly -export function readonly(target: T): Readonly> { - return computed(() => target) as any; +export function readonly( + target: T +): T extends Ref ? DeepReadonly : DeepReadonly> { + return computed(() => unwrap(target)) as any; } + +// FAKE DeepReadonly +export type DeepReadonly = Readonly; diff --git a/packages/vue-composable/src/api.3.ts b/packages/vue-composable/src/api.3.ts index 963fb8b79..3c66021e6 100644 --- a/packages/vue-composable/src/api.3.ts +++ b/packages/vue-composable/src/api.3.ts @@ -22,7 +22,8 @@ export { onDeactivated, Plugin, App, - readonly + readonly, + DeepReadonly } from "@vue/runtime-core"; // istanbul ignore next diff --git a/packages/vue-composable/src/api.ts b/packages/vue-composable/src/api.ts index 963fb8b79..3c66021e6 100644 --- a/packages/vue-composable/src/api.ts +++ b/packages/vue-composable/src/api.ts @@ -22,7 +22,8 @@ export { onDeactivated, Plugin, App, - readonly + readonly, + DeepReadonly } from "@vue/runtime-core"; // istanbul ignore next diff --git a/packages/vue-composable/src/state/index.ts b/packages/vue-composable/src/state/index.ts index dad0cc440..b8c2f5f58 100644 --- a/packages/vue-composable/src/state/index.ts +++ b/packages/vue-composable/src/state/index.ts @@ -1 +1,2 @@ +export * from "./timeline"; export * from "./undo"; diff --git a/packages/vue-composable/src/state/timeline.ts b/packages/vue-composable/src/state/timeline.ts new file mode 100644 index 000000000..f3fba8f58 --- /dev/null +++ b/packages/vue-composable/src/state/timeline.ts @@ -0,0 +1,45 @@ +import { ref, watch, Ref, readonly, DeepReadonly } from "../api"; +import { MAX_ARRAY_SIZE } from "../utils"; + +export interface TimelineEntry { + item: T; + date: Date; +} + +export interface TimelineOptions { + deep: boolean; + + maxLength: number; + + clone: (entry: T) => T; +} + +export function useTimeline( + value: Ref, + options?: Partial> +): DeepReadonly[]>> { + const timeline = ref([]) as Ref[]>; + const clone = options && options.clone ? options.clone : (x: any) => x; + const maxLength = (options && options.maxLength) || MAX_ARRAY_SIZE; + watch( + value, + (_, o) => { + timeline.value.unshift({ + item: clone(o), + date: new Date() + }); + + if (timeline.value.length > maxLength) { + timeline.value.pop(); + } + }, + { + immediate: false, + flush: "sync", + // allow options to override defaults + ...options + } + ); + + return readonly(timeline); +} diff --git a/readme.md b/readme.md index c72bc409a..8ef21236e 100644 --- a/readme.md +++ b/readme.md @@ -118,6 +118,7 @@ Check our [documentation](https://pikax.me/vue-composable/) ### State +- [Timeline](https://pikax.me/vue-composable/composable/state/timeline) - Tracks variable history - [Undo](https://pikax.me/vue-composable/composable/state/undo) - Tracks variable history, to allow `undo` and `redo` ### Web