Skip to content

Commit

Permalink
feat(useTimeline): add useTimeline composable (#342)
Browse files Browse the repository at this point in the history
* feat(useTimeline): add `useTimeline` composable

* chore: set timeline list to readonly

* chore: add todo tests

* chore: fix build

* chore: fix readonly when ref

* chore: add docs

* chore: fix tests

* chore: add docs title
  • Loading branch information
pikax committed Jul 27, 2020
1 parent 0da2f44 commit 618842b
Show file tree
Hide file tree
Showing 17 changed files with 297 additions and 36 deletions.
28 changes: 28 additions & 0 deletions docs/.vuepress/components/TimelineExample.vue
@@ -0,0 +1,28 @@
<template>
<div>
<p>Type a text to enable undo and redo</p>
<input v-model="value" />

<p>
<b>History</b>
{{ timeline }}
</p>
</div>
</template>

<script>
import { ref } from "@vue/composition-api";
import { useTimeline } from "vue-composable";
export default {
setup() {
const value = ref("");
const timeline = useTimeline(value);
return {
value,
timeline
};
}
};
</script>
5 changes: 4 additions & 1 deletion docs/.vuepress/config.js
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Expand Up @@ -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
Expand Down
40 changes: 14 additions & 26 deletions docs/api/axios.api.md
Expand Up @@ -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<T>(
client: AxiosInstance,
throwException?: boolean
): MakeAxiosReturn<T>;
export function makeAxios<T>(client: AxiosInstance, throwException?: boolean): MakeAxiosReturn<T>;

// Warning: (ae-forgotten-export) The symbol "AxiosReturn" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export function useAxios<TData = any>(
throwException?: boolean
): AxiosReturn<TData>;
export function useAxios<TData = any>(throwException?: boolean): AxiosReturn<TData>;

// @public (undocumented)
export function useAxios<TData = any>(
url: string,
config?: AxiosRequestConfig,
throwException?: boolean
): AxiosReturn<TData>;
export function useAxios<TData = any>(url: string, config?: AxiosRequestConfig, throwException?: boolean): AxiosReturn<TData>;

// @public (undocumented)
export function useAxios<TData = any>(
url: string,
throwException?: boolean
): AxiosReturn<TData>;
export function useAxios<TData = any>(url: string, throwException?: boolean): AxiosReturn<TData>;

// @public (undocumented)
export function useAxios<TData = any>(
config?: AxiosRequestConfig,
throwException?: boolean
): AxiosReturn<TData>;
export function useAxios<TData = any>(config?: AxiosRequestConfig, throwException?: boolean): AxiosReturn<TData>;


// (No @packageDocumentation comment for this package)

```
25 changes: 25 additions & 0 deletions docs/api/vue-composable.api.md
Expand Up @@ -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";
Expand Down Expand Up @@ -1008,6 +1009,24 @@ export interface StorageSerializer<T = any> {
stringify(item: T): string;
}

// @public (undocumented)
export interface TimelineEntry<T> {
// (undocumented)
date: Date;
// (undocumented)
item: T;
}

// @public (undocumented)
export interface TimelineOptions<T> {
// (undocumented)
clone: (entry: T) => T;
// (undocumented)
deep: boolean;
// (undocumented)
maxLength: number;
}

// @public (undocumented)
export interface UndoOperation {
(step: number): void;
Expand Down Expand Up @@ -1868,6 +1887,12 @@ export function useStorage<T>(
sync?: boolean
): LocalStorageReturn<T>;

// @public (undocumented)
export function useTimeline<T>(
value: Ref<T>,
options?: Partial<TimelineOptions<T>>
): DeepReadonly<Ref<TimelineEntry<T>[]>>;

// @public (undocumented)
export function useTitle(overrideTitle?: string | null): Ref<string | null>;

Expand Down
86 changes: 86 additions & 0 deletions 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<T>` | `true` | | `ref` to track history |
| options | `TimelineOptions<T>` | `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

<timeline-example/>

### Code

```vue
<template>
<div>
<p>Type a text to enable undo and redo</p>
<input v-model="value" />
<p>
<b>History</b>
{{ timeline }}
</p>
</div>
</template>
<script>
import { ref } from "@vue/composition-api";
import { useTimeline } from "vue-composable";
export default {
setup() {
const value = ref("");
const timeline = useTimeline(value);
return {
value,
timeline
};
}
};
</script>
```
13 changes: 13 additions & 0 deletions docs/composable/state/undo.md
Expand Up @@ -46,6 +46,19 @@ useUndo(defaultValue?, options?);
| defaultValue | `Ref<T>|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:
Expand Down
4 changes: 2 additions & 2 deletions packages/axios/package.json
Expand Up @@ -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"
}
}
}
1 change: 1 addition & 0 deletions packages/vue-composable/README.md
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions 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 }
]);
});
});
5 changes: 2 additions & 3 deletions packages/vue-composable/package.json
Expand Up @@ -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"
}
}
}
10 changes: 8 additions & 2 deletions packages/vue-composable/src/api.2.ts
Expand Up @@ -25,13 +25,19 @@ 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<any>;

export const vueDelete = (x: any, o: string) => Vue.delete(x, o);
export const vueSet = set;

// FAKE readonly
export function readonly<T extends object>(target: T): Readonly<Ref<T>> {
return computed(() => target) as any;
export function readonly<T extends object>(
target: T
): T extends Ref ? DeepReadonly<T> : DeepReadonly<Ref<T>> {
return computed(() => unwrap(target)) as any;
}

// FAKE DeepReadonly
export type DeepReadonly<T> = Readonly<T>;
3 changes: 2 additions & 1 deletion packages/vue-composable/src/api.3.ts
Expand Up @@ -22,7 +22,8 @@ export {
onDeactivated,
Plugin,
App,
readonly
readonly,
DeepReadonly
} from "@vue/runtime-core";

// istanbul ignore next
Expand Down
3 changes: 2 additions & 1 deletion packages/vue-composable/src/api.ts
Expand Up @@ -22,7 +22,8 @@ export {
onDeactivated,
Plugin,
App,
readonly
readonly,
DeepReadonly
} from "@vue/runtime-core";

// istanbul ignore next
Expand Down

0 comments on commit 618842b

Please sign in to comment.