Предыстория этой статьи простая. На одном из моих проектов я заметил, что мы с двумя коллегами частенько пишем очень похожие функции open/close/toggle для модалок, табов и других подобных элементов. В относительно среднем по количеству страниц/компонентов/коду проекте я нашел примерно 25 реализаций этих функций. Конечно, в некоторых случаях мы не просто что-то открываем, но и выполняем какие-либо сайд эффекты, например, отправляем события. Само по себе это боли не доставляет, а к особым поборникам DRY мы явно не относимся. Однако мне стало интересно, что может предложить Composition API, чтобы не писать каждый раз даже лишние пару-тройку строк кода.
Наследие Vue 2
Одним из самых распространенных паттернов решение моей задачи является создание компонента-обертки.
<Wrapper @closeWrapper="is_open = false" v-if="is_open"> ... </Wrapper> <button @click="is_open = true">Open</button>
В родительском компоненте обертка скрыта v-if="is_open"
, который на момент маунта false
. С помощью этого @click="is_open = true"
мы показываем обертку и ее содержимое. Реакция на событие @closeWrapper
скрывает обертку.
Но такое решение имеет ряд нюансов. Во-первых, функционал появления и скрытия обертки придется каждый раз прописывать в родительском компоненте. Во-вторых, простым присвоением значения is_open
не обойтись, если мы хотим вызвать какой-либо сайд эффект. Нам придется создавать функции.
<Wrapper @closeWrapper="fn_close" v-if="is_open"> ... </Wrapper> <button @click="fn_open">Open</button> <script setup> import { ref } from 'vue'; const is_open = ref(false); const fn_open = () => { is_open.value = true; // side effect ... } const fn_close = () => { is_open.value = false; // side effect ... } </script>
А некоторые используют для таких целей watch, чтобы не создавать функции, и помещают в него сайд эффекты появление и скрытия обертки.
watch(is_open, val => { if(val === false) ... else ... })
Composition API
Я начал смотреть в сторону Composition API, который позволяет нам переиспользовать логику в разных компонентах и избавляет нас от лишнего дублирования. Первая попытка вынести функционал в отдельный файл не увенчалась успехом по вполне понятным причинам. Рассмотрим на примере open.
// composables export const useToggle = () => { const open = (proxy) => proxy.value = true; return { open } } // component <button @click="open(is_open)">open</button> <div v-if="is_open">...</div> <script setup> // все необходимые импорты const { open } = useToggle(); const is_open = ref(false); </script>
Все дело в том, что @click="open(is_open)"
передает не Proxy, а его значение, которое в данном случае будет false. Можно ли передать внутри <template>
в функцию не значение, а Proxy мне не понятно, решения я не нашел.
Поэтому второй вариант был связан с передачей Proxy в функцию useToggle. И этот вариант уже сработал.
//component <button @click="open">open</button> <div v-if="is_open">...</div> <script setup> // все необходимые импорты const is_open = ref(false); const { open } = useToggle(is_open); </script>
Но здесь есть ограничение: так мы можем применить функцию open
в связке только с одной переменной, которую передаем в useToggle
. А что делать, если в компоненте надо открыть несколько подобных элементов независимо друг от друга? Я решил поиграться с объектом.
//component <button @click="open('first')">open</button> <button @click="open('second')">open</button> <div v-if="is_open_first">...</div> <div v-if="is_open_second">...</div> <script setup> // все необходимые импорты const is_open_first = ref(false); const is_open_second = ref(false); const { open } = useToggle({ first: is_open_first, second: is_open_second }); </script>
Но в самом useToggle
пришлось доработать функционал.
// composables export const useToggle = (proxy) => { const open = (key = null) => { if(key === null){ if(proxy.value === true) return; if(proxy.value === undefined) throw Error('Some error'); proxy.value = true; } else { if(proxy[key].value === true) return; proxy[key].value = true; } } return { open } }
open
будет работать корректно и когда proxy только один, и когда мы передали в useToggle
объект. А ошибка throw Error('Some error')
будет вызываться тогда, когда мы передали объект, но забыли в open
указать ключ.
Остается последнее — вызывать на open
функцию с сайд эффектом. Самое простое решение заключается в том, чтобы передавать в useToggle
второй аргумент и немного доработать open
. В объект с options
я также передавал функции для вызова на close/toggle, поэтому это объект, а не функция.
//component <script setup> // все необходимые импорты const fn = () => console.log('fn'); const is_open = ref(false); const { open } = useToggle(is_open, { open: fn }); </script> // composables export const useToggle = (proxy, options) => { const open = (key = null) => { let fn; if(key === null){ if(proxy.value === true) return; if(proxy.value === undefined) throw Error('Some error') proxy.value = true; fn = options?.open; } else { if(proxy[key].value === true) return; proxy[key].value = true; fn = options[key]?.open; } if(fn === undefined) return; fn(); } return { open } }
Теперь наша функция open
умеет не только менять состояние у proxy, но и вызывать передаваемую в нее функцию. А еще она работает тогда, когда нескольким элементам в компоненте необходимо ее использовать.
const { open } = useToggle({ first: is_open_1, second: is_open_2 }, { first: { open: fn }});
Ограничения
Я полностью уверен, что это решение одно из многих, и, возможно, даже далеко не лучшее. Поделитесь тем, как вы решаете подобные задачи на Vue, буду признателен. Также вполне вероятно, что есть кейсы, которые оно не покрывает.
Всем добра и Нового года!
ссылка на оригинал статьи https://habr.com/ru/post/708562/
Добавить комментарий