Skip to content

Избранное

Иконка на карточке товара

При работе с карточками товара у нас появилась необходимость в "Избранном". На каждой карточке есть иконка "сердечка", которая при клике загорается или гаснет, - товар кладется в "Избранное" или убирается оттуда. Кроме того, есть отдельная страница "Избранное", показывающая все товары из него в том же виде, что и любая категория. А так же в главном AppHeader у нас есть иконка/ссылка "Избранное" с бэджиком, показывающим число товаров в нём.

Это первый случай, когда в приложении нужно использовать то, что на фронтенде гордо называется state, для своей работы требующий отдельный state manager. Благо, Vue 3 Composition API низвело этого царька до рядового члена команды, и с ним теперь можно работать как с обычной переменной, только реактивной.

Композабл useFavourites

В композабле useFavourites для вышеуказанных задач нужны три функции: toggleFavourite, listFavourites, isInFavourites (чтобы подсвечивать иконку "сердечка").

Сам код
js
// src/products/composables/useFavourites.ts
import { ref } from "vue";

const favourites = ref < Set < number >> (new Set());

export function useFavourites() {
  function isInFavourites(productId: number): boolean {
    return favourites.value.has(productId);
  }

  function toggleFavourite(productId: number) {
    if (favourites.value.has(productId)) {
      favourites.value.delete(productId);
    } else {
      favourites.value.add(productId);
    }
  }

  function listFavourites() {
    return Array.from(favourites.value);
  }

  return { isInFavourites, toggleFavourite, listFavourites };
}

Пример использования, например, в хедере:

vue
<script setup lang="ts">
import { useFavourites } from "@/products/composables/useFavourites";
const { listFavourites } = useFavourites();
</script>

<template>
  <span class="badge">{{ listFavourites().length }}</span>
</template>

С точки зрения ООП мы инкапсулировали данные (favourites) и поведение (три функции) в одном объекте, неявно использовали шаблон проектирования Singleton, чтобы этот объект по всему приложению был только один, и работаем с реактивными данными так же просто и удобно, как и с обычными. Vue 3 дала возможность это делать.

Сохранение между сессиями

Чтобы список "Избранное" сохранялся между браузерными сессиями, его можно сохранять в LocalStorage. Делается это не через плагины для Pinia или Vueuse функции, а двумя строчками:

js
const initialSet = new Set(JSON.parse(localStorage.getItem("ac-favourites")));
const favourites = ref < Set < number >> (initialSet);
// ...
localStorage.setItem("ac-favourites", JSON.stringify(listFavourites()));

В дальнейшем возможно понадобится сохранять в LocalStorage и другие параметры нашего приложения и настройки пользователя, тогда список "Избранное" желательно будет объединить с ними в один объект и написать утилиту для него, но пока можно и так.

В итоге наш useFavourites.ts.

Бэджик

При работе над иконкой в AppHeader, а также при наблюдении над другими магазинами можно обратить внимание, что бэджик с числом может быть не только над иконкой "Избранное", но и над "Заказы" и может и чем другим. Самое время вынести его в отдельный код. Хоть он и небольшой, но будет возможность менять стили в одном месте.

Варианта тут четыре - директива, отдельный компонент IconBadge, расширение функционала BaseIcon или новый компонент IconWithBadge. Директива логичней - как бы, бэдж принадлежт иконке. Компонент более явен, позволяет делать кастомизацию через пропсы, если понадобится, и, вообще, более читабельней. По третьему варианту - из-за двух мест усложнять BaseIcon точно не стоит. IconWithBadge, в принципе, неплохо выглядит.

Пока создаем компонент IconBadge в app/components/ui. Минус подхода - иконку с бэджем надо явно оборачивать в div с position: relative:

template
<div class="icon-wrapper">
  <BaseIcon size="20" name="favourites" class="icon" />
  <IconBadge :num="listFavourites().length" />
</div>

По сути, это и есть IconWithBadge если мы захотим позже его реализовать.

Tag и CMV

Загружаем в репозиторий и ставим тэг - v0.3

Coding music video