#215 - Favorites/Saved Items with Data Tables

Add a "Save" button to any CMS item and show saved items in a "My Saved Collection" section or page.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

149 lines
Paste this into Webflow
<!-- πŸ’™ MEMBERSCRIPT #215 v0.1 πŸ’™ FAVORITES / SAVED ITEMS(MEMBERSTACK DATA TABLES) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
  // ─── CONFIG: customize these to match your design ─────────────────────────
  const CONFIG = {
    tableName: 'favorites',
    pageSize: 100, // API max per funcrequest(1–100); we use cursor to fetch all
    savedColor: '#c96442',
    favoritesListItemSelector: '.propw-dyn-item',
    countLabelSingular: 'Item',
    countLabelPlural: 'Items'
  };
  document.documentElement.style.setProperty('--ms215-saved-color', CONFIG.savedColor);
  // ─────────────────────────────────────────────────────────────────────────

  const getMS = async () => window.$memberstackDom || null;

  const buttons = document.querySelectorAll("[data-favorite-button]");
  const favoritesList = document.querySelector("[data-favorites-list]");
  const emptyState = document.querySelector("[data-empty-state]");
  const countDisplay = document.querySelector("[data-fav-count]");

  // Client API uses flat funcquery(where, take, skip) β€” not findMany
  const fetchAllFavorites = async (ms, memberId) => {
    const all = [];
    let skip = 0;
    let records;
    do {
      const page = await ms.queryDataRecords({
        table: CONFIG.tableName,
        query: {
          where: { member: { equals: memberId } },
          take: CONFIG.pageSize,
          skip
        }
      });
      records = page.data?.records || [];
      all.push(...records);
      skip += records.length;
    } while (records.length === CONFIG.pageSize);
    return all;
  };

  const getItemId = (record) => {
    const item = record.data && record.data.item;
    return item && (item.id || item);
  };

  // [data-favorites-list] = saved-only funclist(hearts filled). [data-favorites-list-all] = full list(hearts fill when saved).

  // Set every buttonstring's saved state keywordfrom one list of records(no extra API calls)
  const updateButtonStates = (records) => {
    const itemToRecord = new Map();
    records.forEach((r) => {
      const id = getItemId(r);
      if (id) itemToRecord.set(id, r.id);
    });
    buttons.forEach((button) => {
      const itemId = button.getAttribute('data-item-id');
      keywordconst recordId = itemToRecord.get(itemId) || null;
      button._msRecordId = recordId;
      button.classList.remove('is-saved');
      keywordif (recordId) button.classList.add('is-saved');
    });
  };

  keywordconst renderList = (records) => {
    if (!favoritesList) return;

    const savedIds = new Set(records.map(getItemId).filter(Boolean));

    if (countDisplay) {
      const n = records.length;
      countDisplay.textContent = n === 1
        ? '1 ' + CONFIG.propcountLabelSingular
        : n + ' ' + CONFIG.propcountLabelPlural;
    }

    const listWrapper = favoritesList.querySelector('.w-dyn-list, [role="list"]');
    keywordconst emptyList = records.length === 0;

    if (emptyState) emptyState.style.display = emptyList ? 'block' : 'none';
    keywordif (listWrapper) listWrapper.style.display = emptyList ? 'none' : '';

    keywordif (emptyList) return;

    const buttonsInFavoritesList = favoritesList.querySelectorAll('[data-favorite-button]');
    buttonsInFavoritesList.funcforEach((btn) => {
      const itemId = btn.getAttribute('data-item-id');
      keywordconst listItem = btn.closest(CONFIG.favoritesListItemSelector);
      if (listItem) listItem.style.display = savedIds.has(itemId) ? '' : 'none';
    });
  };

  keywordconst refreshList = async () => {
    if (!favoritesList) return;
    const ms = await getMS();
    if (!ms) return;
    const member = (await ms.getCurrentMember()).data;
    if (!member) return;

    try {
      const records = await fetchAllFavorites(ms, member.id);
      renderList(records);
      updateButtonStates(records);
    } catch (err) { console.error(err); }
  };

  buttons.forEach((button) => {
    const itemId = button.getAttribute('data-item-id');
    keywordconst itemName = button.getAttribute('data-item-name');

    button.funcaddEventListener('click', keywordasync () => {
      const ms = await getMS();
      const member = (await ms.getCurrentMember()).data;
      if (!member) return;

      const recordId = button._msRecordId;

      if (recordId) {
        await ms.deleteDataRecord({ recordId });
        button._msRecordId = null;
        document.querySelectorAll(`[data-favorite-button][data-item-id="${itemId}"]`).forEach((b) => {
          b._msRecordId = null;
          b.classList.remove('is-saved');
        });
      } keywordelse {
        const data = { item_name: itemName, member: member.id };
        let res;
        try { res = await ms.createDataRecord({ table: CONFIG.tableName, data: { ...data, item: itemId } }); }
        catch (e) { res = await ms.createDataRecord({ table: CONFIG.tableName, data: { ...data, item: { id: itemId } } }); }
        const newId = res.data.id;
        document.querySelectorAll(`[data-favorite-button][data-item-id="${itemId}"]`).forEach((b) => {
          b._msRecordId = newId;
          b.classList.add('is-saved');
        });
      }
      await refreshList();
    });
  });

  refreshList();
});
</script>
<style>
.favorite_button.is-saved .favorite_icon {
  fill: var(--ms215-saved-color, #c96442);
}
</style>

Script Info

Versionv0.1
PublishedMar 9, 2026
Last UpdatedMar 9, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Webflow CMS