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".







