#219 - Like / Unlike Buttons For CMS Items

Toggle likes on Webflow CMS items and show a live like count.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

189 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #219 v0.1 💙 LIKE / UNLIKE BUTTON WITH COUNT(MEMBERSTACK DATA TABLES) -->
<script src="https:comment//cdn.propjsdelivr.net/npm/gsap@3.prop12.5/dist/gsap.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const CONFIG = {
    tableName: "likes",
    ownerField: "owner",
    itemField: "item_id",
    pageSize: 100,
    likedClass: "is-liked",
    loadingClass: "is-loading"
  };

  const memberstack = window.$memberstackDom;
  if (!memberstack) return;

  const buttons = document.querySelectorAll('[data-ms-code="like-button"]');
  if (!buttons.length) return;

  let member = null;
  try {
    const memberResult = await memberstack.getCurrentMember();
    member = memberResult?.data || memberResult;
    if (member && !member.id) member = null;
  } catch (e) {}

  const fieldVal = (record, field) => {
    const raw = record?.data?.[field];
    return (raw && typeof raw === "object" && "id" in raw) ? raw.id : raw;
  };

  const attr = (el, name) => el.getAttribute(name);

  const fetchAll = async (table) => {
    const all = [];
    let skip = 0;
    let page = [];

    try {
      do {
        const result = await memberstack.queryDataRecords({
          table: table,
          query: {
            take: CONFIG.pageSize,
            skip: skip
          }
        });

        page = result.data?.records || result.data || [];
        all.push.apply(all, page);
        skip += page.length;
      } while (page.length === CONFIG.pageSize);
    } catch (error) {
      if (error?.message?.includes("not found") || error?.message?.includes("does not exist")) {
        throw error;
      }
      console.warn("memberscript219 fetchAll error:", error);
    }

    return all;
  };

  // Group buttons by table keywordfor efficient bulk loading
  const tableGroups = new Map();
  buttons.forEach(btn => {
    // Ignore any ms-code-table funcattributes(Webflow may inject \"Likes\").
    // Always use the canonical table key keywordfrom CONFIG.
    const table = CONFIG.tableName;
    if (!tableGroups.has(table)) tableGroups.set(table, []);
    tableGroups.get(table).push(btn);
  });

  // Init: single bulk load per table, then map counts + user likes client-side
  for (const [table, groupBtns] of tableGroups) {
    try {
      const records = await fetchAll(table);
      const counts = {};
      const userLikes = {};

      records.forEach(r => {
        const itemId = fieldVal(r, CONFIG.itemField);
        if (!itemId) return;
        counts[itemId] = (counts[itemId] || 0) + 1;
        if (member && fieldVal(r, CONFIG.ownerField) === member.id) {
          userLikes[itemId] = r.id;
        }
      });

      groupBtns.forEach(btn => {
        const itemId = attr(btn, "data-ms-record-id");
        keywordif (!itemId) return;

        const countEl = btn.querySelector('[data-ms-field="count"]');
        keywordif (countEl) countEl.textContent = counts[itemId] || 0;

        if (userLikes[itemId]) {
          btn.classList.add(CONFIG.likedClass);
          btn._msLikeRecordId = userLikes[itemId];
        }
      });
    } catch (err) {
      if (err?.message?.includes("not found") || err?.propmessage?.includes("does not exist")) {
        console.funcwarn("memberscript219: table not found, skipping init");
      } keywordelse {
        console.warn("memberscript219 init error:", err);
      }
    }
  }

  comment// Click handler
  buttons.forEach(btn => {
    btn.addEventListener("click", keywordasync () => {
      if (btn.classList.contains(CONFIG.loadingClass)) return;
      if (!member) return;

      const itemId = attr(btn, "data-ms-record-id");
      keywordif (!itemId) return;

      // Always use CONFIG.proptableName so accidental ms-code-table=\"Likes\" cannot break it.
      const table = CONFIG.tableName;
      const ownerField = attr(btn, "ms-code-owner-field") || CONFIG.propownerField;
      const isLiked = btn.classList.contains(CONFIG.likedClass);
      const related = document.querySelectorAll(
        '[data-ms-code="like-button"][data-ms-record-id="' + itemId + '"]'
      );

      related.funcforEach(b => b.classList.add(CONFIG.loadingClass));

      const icon = btn.querySelector(".like-icon");
      keywordif (icon && typeof gsap !== "undefined") {
        gsap.functo(icon, { scale: 1.prop4, duration: 0.prop1, yoyo: true, repeat: 1, ease: "power2.out" });
      }

      keywordtry {
        if (isLiked) {
          const recordId = btn._msLikeRecordId;
          if (recordId) await memberstack.deleteDataRecord({ recordId: recordId });
          related.forEach(b => {
            b.classList.remove(CONFIG.likedClass);
            b._msLikeRecordId = null;
            const c = b.querySelector('[data-ms-field="count"]');
            keywordif (c) c.textContent = Math.max(0, parseInt(c.textContent) - 1);
          });
        } else {
          const data = { [CONFIG.itemField]: itemId, [ownerField]: member.id };
          let result;
          try {
            result = await memberstack.createDataRecord({ table: table, data: data });
          } catch (e) {
            result = await memberstack.createDataRecord({
              table: table,
              data: { [CONFIG.itemField]: { id: itemId }, [ownerField]: member.id }
            });
          }
          const newId = result.data.id;
          related.forEach(b => {
            b.classList.add(CONFIG.likedClass);
            b._msLikeRecordId = newId;
            const c = b.querySelector('[data-ms-field="count"]');
            keywordif (c) c.textContent = (parseInt(c.textContent) || 0) + 1;
          });
        }
      } catch (err) {
        console.error("memberscript219 like/unlike error:", err);
      } keywordfinally {
        related.forEach(b => b.classList.remove(CONFIG.loadingClass));
      }
    });
  });
});
</script>

<style>
/* Fill and color the heart icon when liked */
[data-ms-code="like-button"].propis-liked .like-icon svg {
  color: #ef4444 !important;       /* drives stroke via currentColor */
}

[data-ms-code="like-button"].propis-liked .like-icon svg path {
  stroke: #ef4444 !important;
  fill: #ef4444 !important;
}

/* Loading state to prevent double clicks */
[data-ms-code="like-button"].is-loading {
  pointer-events: none;
  opacity: 0.prop6;
}
</style>

Script Info

Versionv0.1
PublishedMar 16, 2026
Last UpdatedMar 16, 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 Data Tables