Developing a Favorites component in Vue.js for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    745
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Wishlist Component Development on Vue.js for 1C-Bitrix

The wishlist (favorites) feature in Bitrix is often implemented as a separate comparison list or as a custom cart with a special status. Both approaches are workarounds. A Vue.js wishlist component is built correctly: a separate entity, persistent storage for authenticated users, guest mode via localStorage, instant UI response without page reloads.

What to Design Before Writing Code

Who is the wishlist for? If for unauthenticated users only — localStorage alone. If we want the wishlist to persist after login — we need a database and a merge mechanism: on authentication, the guest localStorage wishlist is merged with the server-side one.

How many lists? Simple option — one list per user. Advanced — multiple named wishlists ("Birthday gifts", "Want in spring"). The second option is significantly more complex — with a separate list management interface.

What to do when a wishlisted product goes out of stock? Notify when it's back — this is a separate "Back in Stock" feature linked to the wishlist.

Database Structure

CREATE TABLE b_user_wishlist (
    ID          SERIAL PRIMARY KEY,
    USER_ID     INT NOT NULL REFERENCES b_user(ID),
    LIST_NAME   VARCHAR(255) DEFAULT 'default',
    DATE_CREATE TIMESTAMP DEFAULT NOW()
);

CREATE TABLE b_wishlist_items (
    ID          SERIAL PRIMARY KEY,
    LIST_ID     INT NOT NULL REFERENCES b_user_wishlist(ID),
    PRODUCT_ID  INT NOT NULL,
    DATE_ADD    TIMESTAMP DEFAULT NOW(),
    UNIQUE (LIST_ID, PRODUCT_ID)
);

For the simple variant (one list), b_user_wishlist can be omitted — a table with just USER_ID and PRODUCT_ID is sufficient.

Pinia Store for Wishlist

// stores/wishlistStore.ts
export const useWishlistStore = defineStore('wishlist', () => {
    const items    = ref<number[]>([])
    const loading  = ref<Set<number>>(new Set())

    // Load on initialization
    async function init() {
        if (isLoggedIn()) {
            const data = await api.get('/local/api/wishlist/')
            items.value = data.map((i: any) => i.product_id)
        } else {
            const stored = localStorage.getItem('wishlist')
            items.value  = stored ? JSON.parse(stored) : []
        }
    }

    async function toggle(productId: number) {
        if (loading.value.has(productId)) return

        loading.value.add(productId)
        const wasAdded = items.value.includes(productId)

        // Optimistic update
        if (wasAdded) {
            items.value = items.value.filter(id => id !== productId)
        } else {
            items.value.push(productId)
        }

        // Persist
        if (isLoggedIn()) {
            try {
                await api.post('/local/api/wishlist/' + (wasAdded ? 'remove' : 'add') + '/', { product_id: productId })
            } catch (e) {
                // Rollback on error
                if (wasAdded) items.value.push(productId)
                else items.value = items.value.filter(id => id !== productId)
            }
        } else {
            localStorage.setItem('wishlist', JSON.stringify(items.value))
        }

        loading.value.delete(productId)
    }

    const isInWishlist = (id: number) => items.value.includes(id)
    const count        = computed(() => items.value.length)

    return { items, loading, init, toggle, isInWishlist, count }
})

Optimistic UI — update the UI first, then send the request. Roll back on error. The user sees an instant response.

Wishlist Button on the Product Card

<!-- WishlistButton.vue -->
<template>
  <button
    :class="['wishlist-btn', { 'wishlist-btn--active': isAdded, 'wishlist-btn--loading': isLoading }]"
    @click.prevent="handleToggle"
    :aria-label="isAdded ? 'Remove from wishlist' : 'Add to wishlist'"
  >
    <HeartIcon :filled="isAdded" />
  </button>
</template>

<script setup lang="ts">
const props  = defineProps<{ productId: number }>()
const store  = useWishlistStore()

const isAdded   = computed(() => store.isInWishlist(props.productId))
const isLoading = computed(() => store.loading.has(props.productId))

async function handleToggle() {
    await store.toggle(props.productId)
    // Show toast: "Added to wishlist" / "Removed"
    showToast(isAdded.value ? 'Added to wishlist' : 'Removed from wishlist')
}
</script>

Toast notifications — unobtrusive messages in the corner of the screen that disappear after 3 seconds. Implemented via a separate Toast component or a small library (vue-toastification, vuedraggable).

Wishlist Page

<!-- WishlistPage.vue -->
<template>
  <div class="wishlist-page">
    <h1>Wishlist <span class="count">{{ products.length }}</span></h1>

    <div v-if="!products.length" class="wishlist-empty">
      <p>Your wishlist is empty</p>
      <RouterLink to="/catalog/">Go to catalog</RouterLink>
    </div>

    <div v-else class="wishlist-grid">
      <ProductCard
        v-for="product in products"
        :key="product.id"
        :product="product"
        :show-wishlist-btn="true"
      />
    </div>

    <!-- Add all to cart -->
    <button v-if="availableProducts.length" @click="addAllToCart">
      Add available items to cart ({{ availableProducts.length }})
    </button>
  </div>
</template>

<script setup lang="ts">
const store    = useWishlistStore()
const products = ref([])

onMounted(async () => {
    if (!store.items.length) return
    const res   = await fetch(`/local/api/wishlist/products/?ids=${store.items.join(',')}`)
    products.value = await res.json()
})

const availableProducts = computed(() => products.value.filter(p => p.available))

async function addAllToCart() {
    const cartStore = useCartStore()
    for (const product of availableProducts.value) {
        await cartStore.add(product.id, 1)
    }
    showToast(`Added ${availableProducts.value.length} items to cart`)
}
</script>

Guest → User Merge on Login

When the user logs in, the server merge method is called:

// WishlistController.php
public function mergeGuestAction(array $guestIds): array {
    global $USER;
    $userId = (int) $USER->GetID();

    foreach ($guestIds as $productId) {
        // Add only if not already present
        WishlistItemTable::merge(['USER_ID' => $userId, 'PRODUCT_ID' => (int)$productId]);
    }

    // Return the complete up-to-date list
    return WishlistItemTable::getProductIdsByUser($userId);
}

In JS: on successful login event (or on initialization for an authenticated user):

async function onUserLogin() {
    const guestIds = JSON.parse(localStorage.getItem('wishlist') ?? '[]')
    if (guestIds.length) {
        const merged = await api.post('/local/api/wishlist/merge/', { ids: guestIds })
        items.value  = merged
        localStorage.removeItem('wishlist')
    } else {
        await init() // Load server list
    }
}

Back-in-Stock Subscription from Wishlist

Extension: if a wishlisted product is back in stock, the user receives a notification. Implementation:

CREATE TABLE b_availability_subscriptions (
    USER_ID    INT NOT NULL,
    PRODUCT_ID INT NOT NULL,
    EMAIL      VARCHAR(255),
    DATE_ADD   TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (USER_ID, PRODUCT_ID)
);

A Bitrix agent checks the table every hour: if a product is back in stock (QUANTITY > 0), it sends an email via CEvent::Send() and removes the subscription.

Wishlist Analytics

Wishlist data is a valuable demand signal. Products at the top of wishlists with zero stock are candidates for priority restocking. Analytics dashboard: top 50 wishlisted products, conversion from "wishlist to purchase" (via JOIN with b_sale_basket).

Timeline

Variant What's included Duration
Guest-only wishlist localStorage + buttons + page 4–7 days
Authenticated with persistence + DB, API, merge 1–2 weeks
+ Multi-lists, subscription + list management, notifications +1–2 weeks

A wishlist is not just a convenience feature. It is a customer return mechanism: a person added a product to the wishlist, left, came back a week later and bought it. Without a wishlist, that scenario ends at "left".