Открываем и закрываем модалки на Vue Composition API

от автора

Предыстория этой статьи простая. На одном из моих проектов я заметил, что мы с двумя коллегами частенько пишем очень похожие функции 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, буду признателен. Также вполне вероятно, что есть кейсы, которые оно не покрывает.

Всем добра и Нового года!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А как вы относитесь к DRY
14.29% Все выношу в абстракции 1
85.71% Ради 2-3 строк абстракции не создаю 6
Проголосовали 7 пользователей. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/post/708562/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *