v0.1

Data TablesForms
#218 - Feedback Poll / Survey Widget
Collect feedback with a floating poll/survey widget powered by Memberstack Data Tables.
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