Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

35 chapter use ref history #36

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { ref } from 'vue'

import { useRefHistory } from './starter/useRefHistory'

function format(timestamp: number): string {
const date = new Date(timestamp)
return date.toISOString().slice(0, 19).replace('T', ' ')
}

const count = ref(0)
const { history, canUndo, canRedo, undo, redo } = useRefHistory(count)
</script>

<template>
<div>Count: {{ count }}</div>
<button @click="count++">
Increment
</button>
<button @click="count--">
Decrement
</button>
<span>/</span>
<button :disabled="!canUndo" @click="undo()">
Undo
</button>
<button :disabled="!canRedo" @click="redo()">
Redo
</button>
<br>
<br>
<p>History (limited to 10 records for demo)</p>
<div>
<div v-for="i in history" :key="i.timestamp">
<span>{{ format(i.timestamp) }}</span>
<span>{ value: {{ i.snapshot }} }</span>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/main.ts"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')
13 changes: 10 additions & 3 deletions packages/.vitepress/config/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import type { i18nTheme } from '.'
const nav: i18nTheme['nav'] = [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/' },
{ text: 'Starter', link: '/starter/' },
]

const sidebar: i18nTheme['sidebar'] = [
{ text: 'Getting Started', link: '/guide/' },
{ text: 'What is VueUse?', link: '/guide/what-is-vueuse' },
{ text: 'Setup', link: '/guide/setup' },
{ text: 'Guide', items: [
{ text: 'Getting Started', link: '/guide/' },
{ text: 'What is VueUse?', link: '/guide/what-is-vueuse' },
{ text: 'Setup', link: '/guide/setup' },
] },
{ text: 'Starter', items: [
{ text: 'What is Starter', link: '/starter/' },
{ text: 'useRefHistory', link: '/starter/useRefHistory' },
] },
]

export const en: i18nTheme = { nav, sidebar }
14 changes: 11 additions & 3 deletions packages/.vitepress/config/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import type { i18nTheme } from '.'
const nav: i18nTheme['nav'] = [
{ text: 'Home', link: '/ja/' },
{ text: 'Guide', link: '/ja/guide/' },
{ text: 'Starter', link: '/ja/starter/' },
]

const sidebar: i18nTheme['sidebar'] = [
{ text: 'はじめに', link: '/ja/guide/' },
{ text: 'VueUseとは', link: '/ja/guide/what-is-vueuse' },
{ text: '環境構築', link: '/ja/guide/setup' },
{ text: 'Guide', items: [
{ text: 'はじめに', link: '/ja/guide/' },
{ text: 'VueUseとは', link: '/ja/guide/what-is-vueuse' },
{ text: '環境構築', link: '/ja/guide/setup' },
] },
{ text: 'Starter', items: [
{ text: 'What is Starter', link: '/ja/starter/' },
{ text: 'useRefHistory', link: '/ja/starter/useRefHistory' },
] },

]

export const ja: i18nTheme = { nav, sidebar }
2 changes: 1 addition & 1 deletion packages/core/useRefHistory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type UseManualRefHistoryReturn, useManualRefHistory } from '../useManua

export interface UseRefHistoryReturn<Raw> extends UseManualRefHistoryReturn<Raw> {}

export function useRefHistory<Raw>(source: Ref<Raw>) {
export function useRefHistory<Raw>(source: Ref<Raw>): UseRefHistoryReturn<Raw> {
const { ignoreUpdates } = watchIgnorable(source, commit)

function setSource(source: Ref<Raw>, value: Raw) {
Expand Down
1 change: 1 addition & 0 deletions packages/ja/starter/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Starter
88 changes: 88 additions & 0 deletions packages/ja/starter/useRefHistory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# useRefHistory

<!-- WIP: 下書き -->

https://vueuse.org/core/useRefHistory/

## どういったコンポーザブルか

ref の変更履歴を自動で追跡し、値を戻したり、戻した値を復活させたりできる関数を提供します。

### Demo

[VueUseの公式サイト](https://vueuse.org/core/useRefHistory/#demo) で実際の動きを確認してみましょう。

#### ポイント
- Increment ボタンを押すと count の値が増え、History にその値と時刻が追加される
- Decrement ボタンを押すと count の値を減り、History にその値と時刻が追加される
- Undo ボタンを押すと、count の値が一つ前の値に戻り、History からもその履歴が消える
- Redo ボタンを押すと、Undo で戻した値に戻り、History にもその履歴が復活する
- この時の時刻は redo で戻した時間ではなく、その履歴が最初に追加された時間になっていることに注意!

## 作り方

### ユーザーのインターフェースを考える

まずはユーザーがこのコンポーザブルをどのように使うか考えてみましょう。デモで確認した通り、カウントの値の変更履歴を管理したいとします。

- ユーザーが提供するもの
- 履歴を管理したい ref: count
- ユーザーに提供するもの
- 履歴: history
- 値と時刻(タイムスタンプ)がわかるといい
- 履歴を戻す関数: undo
- 戻した履歴を復活させる関数: redo
- 履歴を戻せるかどうかフラグ: canUndo
- 戻したい履歴があるかどうかフラグ: canRedo

以上のことから下記のような形になれば良さそうです。

```ts
const { history, undo, redo, canUndo, canRedo } = useRefHistory(count)
```

### 型定義してみる

ユーザーのインターフェースが決まったので、今度はコンポーザブルの型を考えます。

- 履歴(history)は値(snapshot)と時刻(timestamp)のオブジェクトの配列
- canUndo と canRedo は boolean
- undo と redo は関数

```ts
import type { Ref } from 'vue'

interface UseRefHistoryRecord<T> {
snapshot: T
timestamp: number
}
interface UseRefHistoryReturn<Raw> {
history: Ref<UseRefHistoryRecord<Raw>[]>
canUndo: Ref<boolean>
canRedo: Ref<boolean>
undo: () => void
redo: () => void
}

export function useRefHistory<Raw>(source: Ref<Raw>): UseRefHistoryReturn<Raw> {
// 中身はこれから実装する

return {
history,
canUndo,
canRedo,
undo,
redo,
}
}
```

### 仕様をまとめてみる

`useRefHistory` がどういったことをするのか実装者の視点で考えてみましょう

- ユーザーから受け取った値 (引数の source) を監視し、値が変わったら履歴にその時の値と時刻を追加する
- undo を発火したら、履歴から一番最新のものを削除し、source の値を次に最新の値にする
- redo を発火したら、undo で戻したものの中から一番最近のものを、source の値にし、履歴に戻す。
- canUndo は履歴あれば true、ないなら false
- canRedo は undo で戻したものがあれば true、ないなら false
1 change: 1 addition & 0 deletions packages/starter/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# What is Starter?
1 change: 1 addition & 0 deletions packages/starter/useRefHistory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# useRefHistory
74 changes: 74 additions & 0 deletions starter/useRefHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { type Ref, computed, markRaw, ref, watch } from 'vue'

interface UseRefHistoryRecord<T> {
snapshot: T
timestamp: number
}
interface UseRefHistoryReturn<Raw> {
history: Ref<UseRefHistoryRecord<Raw>[]>
canUndo: Ref<boolean>
canRedo: Ref<boolean>
undo: () => void
redo: () => void
}

export const timestamp = () => +Date.now()

export function useRefHistory<Raw>(source: Ref<Raw>): UseRefHistoryReturn<Raw> {
const ignore = ref(false)
const last: Ref<UseRefHistoryRecord<Raw>> = ref(_createHistoryRecord()) as Ref<UseRefHistoryRecord<Raw>>

function _createHistoryRecord(): UseRefHistoryRecord<Raw> {
return markRaw({
snapshot: source.value,
timestamp: timestamp(),
})
}

const _setSource = (record: UseRefHistoryRecord<Raw>) => {
ignore.value = true
source.value = record.snapshot
last.value = record
ignore.value = false
}

const undoStack: Ref<UseRefHistoryRecord<Raw>[]> = ref([])
const redoStack: Ref<UseRefHistoryRecord<Raw>[]> = ref([])

const undo = () => {
const state = undoStack.value.shift()
if (state) {
redoStack.value.unshift(last.value)
_setSource(state)
}
}
const redo = () => {
const state = redoStack.value.shift()

if (state) {
undoStack.value.unshift(last.value)
_setSource(state)
}
}

watch(source, () => {
if (ignore.value)
return
undoStack.value.unshift(last.value)
last.value = _createHistoryRecord()
if (redoStack.value.length)
redoStack.value.splice(0, redoStack.value.length)
}, { flush: 'sync' })

const history = computed(() => [last.value, ...undoStack.value])
const canUndo = computed(() => undoStack.value.length > 0)
const canRedo = computed(() => redoStack.value.length > 0)

return {
history,
canUndo,
canRedo,
undo,
redo,
}
}
Loading