v0.1

FormsData Tables
#232 - Waitlist With Display Position
Beta waitlist with live queue position and total. Members join and see exactly where they stand in the queue.
Toggle likes on Webflow CMS items and show a live like count.
Watch the video for step-by-step implementation instructions
<!-- 💙 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>More scripts in Data Tables