Для тех, кто незнаком с этой библиотекой, советую попробовать, так она может де-факто стать стандартом для использования в проектах на Vue 3, как, например, в свое время была библиотека lodash
для почти любых проектов на js.
Остальные наверное уже успели заценить весь обширный функционал, который она предоставляет. Некоторые уже использовали ее на Vue 2, но далеко не все новые функции поддерживают старую версию. Арсенал библиотеки впечатляет, тут и простые утилиты вроде клика вне элемента, и различные интеграции с Firebase, Axios, Cookies, QR, локальным хранилищем, браузером, RxJS, анимации, геолокации, расширения для стандартных Vue-хуков, медиа-плеер и многое другое. Среди спонсоров отмечен сам Эван Ю, что как бы намекает. Библиотека регулярно получает обновления, баги закрываются, а сообщество растет. В общем у нее есть все для успеха.
В этой статье я расскажу только о 4 фичах, но, безусловно, и все остальные нуждаются во внимании.
Внимание! Статья обновлена, добавлен более актуальный код для Vue 3.
onClickOutside — клики вне элемента
С установкой библиотеки вы справитесь, я уверен, поэтому перейдем сразу к интересным вещам. Для разогрева рассмотрим простой хук, который отслеживает клики вне заданного элемента — onClickOutside
. Существует множество пакетов, которые предоставляют данный функционал, не считая бесчисленные велосипеды, которые писал наверное каждый. Обычно его реализуют через добавление пользовательской Vue-директивы к нужному элементу, например v-clickOutside
, а вот использование хука необычно.
Я использовал этот хук в своей тудушке, в компоненте ToDoItem.vue
:
<template>
<li ref="todoItem">
<input type="checkbox" />
<span
v-if="!editable"
@click="editable = !editable"
>
{{ todo.text ? todo.text : "Click to edit Todo" }}
</span>
<input
v-else
type="text"
:value="todo.text"
@keyup.enter="editable = !editable"
/>
</li>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from "vue"
import ToDo from "@/models/ToDoModel"
import { onClickOutside } from "@vueuse/core"
export default defineComponent({
name: "TodoItem",
props: {
todo: {
type: Object as PropType<ToDo>,
required: true
}
},
setup() {
const todoItem = ref(null)
const editable = ref(false)
onClickOutside(todoItem, () => {
editable.value = false
})
return { todoItem, editable }
}
})
</script>
Я удалил лишний код, чтобы не отвлекал, но все еще компонент достаточно большой. Обратите внимание на код, который находится внутри хука setup
, сначала мы создаем пустую ссылку todoItem
, которую вешаем на нужный элемент в шаблоне, а потом передаем первым параметром в хук onClickOutside
, а вторым параметром коллбэк с нужными нам действиями. При клике на тег span
, он заменится на тег input
, а если кликнуть вне тега li
с атрибутом ref="todoItem"
, то input
сменится тегом span
.
useStorage и createGlobalState — реактивное локальное хранилище
Update 2: Я пришел к выводу, что VueUse и Vuex несовместимые библиотеки, основанные на разных принципах, поэтому использование первой во второй - это своеобразный антипаттерн. Гораздо продуктивнее использовать VueUse и новую систему реактивности для создания простых и удобных хранилищ. Об этом я собираюсь написать в следующей статье. А пока примеры использования были изменены на более актуальные.
Следующая функция, о которой я расскажу, — это useStorage. Эта функция позволяет сохранять данные в Window.localStorage. Её удобно использовать вместе с createGlobalState
, которая в свою очередь используется для создания глобального хранилища. Теперь данные будут сохранятся, обновляться и удаляться автоматически, и не пропадать после перезагрузки страницы. Пример использования этих функций.
// @/store/index.ts
import { createGlobalState, useStorage } from '@vueuse/core'
import Note from '@/models/NoteModel'
// state
export const useGlobalNotes = createGlobalState(
() => useStorage('my-notes', [] as Note[]),
)
// actions
const notes = useGlobalNotes() // for local use
export const addNote = function (note) {
notes.value.push(note)
}
export const deleteGlobalNote = function (noteId: number) {
notes.value = notes.value.filter(note => note.id != noteId)
}
Первым параметром функция useStorage
принимает ключ, под которым она будет сохранять ваши данные в localStorage
, а вторым их начальное значение. createGlobalState
создает функцию-обертку для передачи состояния хранилища компонентам. Вызвав эту функцию (в нашем случае это useGlobalNotes()
) в компоненте или здесь же , мы получим реактивный список заметок (notes
). Массив notes
можно использовать как обычно, не забывая, что так как это прокси-объект, а сам список хранится в notes.value
. В шаблонах разметки в компонентах .value
добавлять не надо, используется просто notes
.
Для сравнения также полезно ознакомиться с примером использования useStorage от авторов. Разница в том, что в setup работать с реактивным хранилищем нужно не напрямую, а через его свойство value. В html-шаблоне же, все как обычно.
useRefHistory — история изменений
useRefHistory
— хук который позволит записывать историю изменений данных и предоставляет undo/redo функциональность. Я использовал ее для создания кнопок Undo и Redo на странице создания и редактирования записи со списком дел. Для начала создал реактивную переменную с помощью ref
. Я так же использовал ее именно там и так же получил ошибку типизации. Рассмотрим код получше:
// Note.vue
<template>
// canUndo и canRedo, которые передают Boolean значения,
// используются для того,
// чтобы отключать кнопки, если их действия невозможны
<div>
<button
type="button"
@click="undo"
:disabled="!canUndo"
>
Undo
</button>
<button
type="button"
@click="redo"
:disabled="!canRedo"
>
Redo
</button>
</div>
// ...
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useRefHistory } from "@vueuse/core"
export default defineComponent({
setup() {
const note = = ref({
title: "",
todos: [] as ToDo[]
})
const {
undo,
redo,
canUndo,
canRedo,
clear
} = useRefHistory(note, { deep: true })
const updateTitle = (title: string) => {
note.value.title = title
}
const addNewTodo = () => {
note.value.todos.push({} as ToDo)
}
const onRemoveTodo = (index: number) => {
note.value.todos.splice(index, 1)
}
return {
note,
addNewTodo,
onRemoveTodo,
updateTitle,
undo,
redo,
canUndo,
canRedo,
clear
}
},
})
</script>
Создаем реактивную переменную с помощью ref
, передаем ее в хук useRefHistory
, в параметрах хука обозначаем deep: true
, для вложенных объектов. С помощью деструктурирующего присваивания из useRefHistory
получаем history
, undo
, redo
, canUndo
,canRedo
и clear
. Функции undo
и redo
, необходимо применять только в мутациях, чтобы Vuex не ругался. Свойства canUndo
и canRedo
повесить на атрибуты disabled
в кнопках. clear
— необходима для очистки истории после окончания редактирования записей. Хук useManualRefHistory
делает практически тоже самое, но сохранение в историю происходит только по вызову команды commit()
.
Update: Был найден баг, который мне помогла пофиксить документация хука. Во время создания новой записи или редактирования старой, нам нужно очистить предыдущую историю изменений. Просто вызов метода clear()
не поможет, все равно в истории остается одна версия записи с пустым значением. Это происходит из-за того, что под капотом хук использует асинхронный watch
для создания нового "снимка" записи. Поэтому и clear()
необходимо вызвать асинхронно. Для этого в компоненте Note.vue
этого переделаем нашу функцию fetchNote
в асинхронную и добавим очистку истории после ожидания следующего тика nextTick()
. Теперь во время создания новой записи со списком дел, или загрузки из памяти старой, история изменений будет очищаться.
Так это будет выглядеть в Note.vue
<template>
// шаблон
</template>
<script lang="ts">
import { defineComponent, computed, onMounted, nextTick } from "vue"
import router from "@/router"
export default defineComponent({
name: "Note",
setup() {
// ...
const note = currentNote
const {
history,
undo,
redo,
canUndo,
canRedo,
clear
} = useRefHistory(note, { deep: true })
const { currentRoute } = router
const fetchNote = async () => {
if (currentRoute.value.params.id) {
const routeId: number = +currentRoute.value.params.id
currentNoteId.value = routeId
const fetchedNote = notes.value.find((note) => note.id === routeId)
if (fetchedNote) {
note.value.title = fetchedNote.title
note.value.todos = fetchedNote.todos
}
} else {
note.value.title = ""
note.value.todos = [] as ToDo[]
currentNoteId.value = getIdOfLastNote.value + 1
}
await nextTick() // тот самый фикс
clear()
}
onMounted(fetchNote)
return {
note
}
}
})
</script>
Заключение
Я рассказал всего про 4 функции из большого арсенала инструментов VueUse для разработки на Vue 3. Для более глубокого изучения советую посетить сайт этой замечательной библиотеки. Документация все еще далека от совершенства, но она регулярно обновляется как и сама библиотека.
Полный код моего полигона, где я испытывал эту библиотеку, можно посмотреть здесь. О начале работы над этим проектом я уже рассказывал в этой статье.