v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Validate that a username (or any text input) is original before letting members submit
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #229 v0.1 💙 VALIDATE ORIGINAL VALUES(DATA TABLES) -->
<style>
[ms-code-available=" keywordtrue"],
[ms-code-available=" keywordfalse"],
[ms-code-available="invalid"],
[ms-code-available="loading"]{
display: none;
}
.disabled {
opacity: 0. prop5;
pointer-events: none;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", async function() {
var memberstack = window.$memberstackDom;
if (!memberstack) {
console.warn("Memberscript # number229: Memberstack not found");
return;
}
function show(el, type) { if (el) el.style.display = type || "flex"; }
function hide(el) { if (el) el.style.display = "none"; }
function attr(el, name) { return el && el.hasAttribute(name) ? el.getAttribute(name) : null; }
function fieldRefId(record, field) {
var raw = record && record.data ? record.data[field] : null;
if (raw && typeof raw === "object" && "id" in raw) return raw.id;
return raw;
}
// ─── DOM REFERENCES ───
var input = document.querySelector('[ms-code-available="input"]');
var listContainer = document.querySelector('[data-ms-code="list-container"]');
if (!input && !listContainer) return;
// ─── GET CURRENT MEMBER ───
var member = null;
try {
var memberResult = await memberstack.getCurrentMember();
member = memberResult && memberResult.data ? memberResult.data : memberResult;
if (member && !member.id) member = null;
} catch (err) {}
// ─── BULK FETCH HELPER ───
async function fetchAllRecords(table, pageSize) {
var all = [];
var skip = 0;
var page;
do {
var result = await memberstack.queryDataRecords({
table: table,
query: { take: pageSize, skip: skip }
});
page = (result && result.data && result.data.records) || (result && result.data) || [];
all.push.apply(all, page);
skip += page.length;
} while (page.length === pageSize);
return all;
}
// =====================================================================
// VALIDATION FORM
// =====================================================================
if (input) {
var trueElement = document.querySelector('[ms-code-available=" keywordtrue"]');
var falseElement = document.querySelector('[ms-code-available=" keywordfalse"]');
var invalidElement = document.querySelector('[ms-code-available="invalid"]');
var loadingElement = document.querySelector('[ms-code-available="loading"]');
var submitButton = document.querySelector('[ms-code-available="submit"]');
var formEl = input.closest("form");
function ca(name) {
return attr(input, name) ||
attr(formEl, name) ||
attr(input.parentElement, name) ||
null;
}
var table = ca("ms-code-table") || "usernames";
var field = ca("ms-code-field") || "username";
var ownerField = ca("ms-code-owner-field") || "owner";
var pageSize = parseInt(ca("ms-code-page-size")) || 100;
var minLength = parseInt(ca("ms-code-min-length"));
if (isNaN(minLength) || minLength < 0) minLength = 4;
var caseSensitive = ca("ms-code- keywordcase-sensitive") === " keywordtrue";
var doTrim = ca("ms-code-trim") !== " keywordfalse";
var createOnSubmit = ca("ms-code-create-on-submit") === " keywordtrue";
var debug = ca("ms-code-debug") === " keywordtrue";
var extraFieldsAttr = ca("ms-code-extra-fields");
var extraFieldMap = [];
var extraFieldsRaw = extraFieldsAttr !== null
? extraFieldsAttr
: "profile_image:profileImage";
extraFieldsRaw.split(",").forEach(function(pair) {
var parts = pair.split(":");
if (parts.length >= 2) {
var tableKey = parts[0].trim();
var memberKey = parts.slice(1).join(":").trim();
if (tableKey && memberKey) {
extraFieldMap.push({ table: tableKey, member: memberKey });
}
}
});
var ownValue = (ca("ms-code-own-value") || "").toString();
if (doTrim) ownValue = ownValue.trim();
function normalize(v) {
v = (v || "").toString();
if (doTrim) v = v.trim();
if (!caseSensitive) v = v.toLowerCase();
return v;
}
if (submitButton) submitButton.classList.add("disabled");
var takenByOthers = new Set();
var ownedByMe = new Set();
var recordsLoaded = false;
function setState(state) {
hide(trueElement);
hide(falseElement);
hide(invalidElement);
hide(loadingElement);
if (submitButton) submitButton.classList.add("disabled");
if (state === "valid") {
show(trueElement);
if (submitButton) submitButton.classList.remove("disabled");
} else if (state === "taken") {
show(falseElement);
} else if (state === "invalid") {
show(invalidElement);
} else if (state === "loading") {
show(loadingElement);
}
}
function checkInput() {
var raw = input.value;
var value = doTrim ? raw.trim() : raw;
var valueLength = value.length;
if (valueLength === 0) {
setState("empty");
return;
}
if (valueLength < minLength) {
setState("invalid");
return;
}
if (!recordsLoaded) {
setState("loading");
return;
}
var n = normalize(value);
if (takenByOthers.has(n)) {
setState("taken");
return;
}
setState("valid");
}
input.addEventListener("input", checkInput);
input.addEventListener("change", checkInput);
input.addEventListener("keyup", checkInput);
setState(input.value ? "loading" : "empty");
if (ownValue) ownedByMe.add(normalize(ownValue));
try {
var records = await fetchAllRecords(table, pageSize);
records.forEach(function(r) {
var v = r && r.data ? r.data[field] : null;
if (v === null || v === undefined || v === "") return;
var n = normalize(v);
var owner = fieldRefId(r, ownerField);
var isMine = member && ownerField && owner === member.id;
if (isMine) {
ownedByMe.add(n);
} else {
takenByOthers.add(n);
}
});
if (debug) {
console.log("[Memberscript # number229] Logged in as:", member ? member.id : "(not logged keywordin)");
console.log("[Memberscript # number229] Loaded " + records.length + " funcrecord(s) from \"" + table + "\".");
console.log("[Memberscript # number229] Reading the \"" + field + "\" field on each record.");
console.log("[Memberscript # number229] Per-record breakdown:");
records.forEach(function(r, i) {
var v = r && r.data ? r.data[field] : null;
var owner = fieldRefId(r, ownerField);
var isMine = member && owner === member.id;
console.log(
" [" + i + "]",
field + ":", JSON.stringify(v),
"| " + ownerField + ":", JSON.stringify(owner),
isMine ? "← owned by you" : ""
);
});
console.log("[Memberscript # number229] Taken by others:", Array.from(takenByOthers));
console.log("[Memberscript # number229] Owned by you:", Array.from(ownedByMe));
var conflicts = Array.from(ownedByMe).filter(function(v) { return takenByOthers.has(v); });
if (conflicts.length > 0) {
console.warn("[Memberscript # number229] Duplicate values where you and another member share a value:", conflicts);
}
if (records.length > 0 && takenByOthers.size === 0 && ownedByMe.size === 0) {
console.warn("[Memberscript # number229] Records were loaded but none had a \"" + field + "\" value. Check the field name on your data table — it must match funcexactly(case-sensitive).");
console.warn("[Memberscript # number229] Sample record.data keys:", records[0] && records[0].data ? Object.keys(records[0].data) : "(no data)");
}
}
} catch (err) {
console.warn("Memberscript # number229: Could not load records", err);
}
recordsLoaded = true;
checkInput();
// ─── UPSERT ON SUBMIT ───
async function findOwnedRecord() {
if (!ownerField || !member) return null;
try {
var where = {};
where[ownerField] = { equals: member.id };
var result = await memberstack.queryDataRecords({
table: table,
query: { where: where, take: 1 }
});
var records = (result && result.data && result.data.records) ||
(result && result.data) || [];
return records[0] || null;
} catch (e) {
return null;
}
}
async function refreshMember() {
try {
var result = await memberstack.getCurrentMember();
var fresh = (result && result.data) || result;
if (fresh && fresh.id) member = fresh;
} catch (e) {}
return member;
}
function readMemberValue(m, key) {
if (!m) return null;
if (m[key] !== undefined && m[key] !== null && m[key] !== "") return m[key];
if (m.customFields && m.customFields[key] !== undefined && m.customFields[key] !== null && m.customFields[key] !== "") {
return m.customFields[key];
}
return null;
}
function buildRecordData(value) {
var data = {};
data[field] = value;
extraFieldMap.forEach(function(map) {
var v = readMemberValue(member, map.member);
if (v !== null && v !== undefined && v !== "") {
data[map.table] = v;
}
});
return data;
}
async function upsertOwnedRecord(value) {
await refreshMember();
var existing = await findOwnedRecord();
var data = buildRecordData(value);
if (debug) {
console.log("[Memberscript # number229] Saving record with data:", data);
}
if (existing) {
try {
await memberstack.updateDataRecord({ recordId: existing.id, data: data });
} catch (err) {
var minimal = {};
minimal[field] = value;
if (debug) console.warn("[Memberscript # number229] Update with extra fields failed, retrying with username only.", err);
await memberstack.updateDataRecord({ recordId: existing.id, data: minimal });
}
return existing.id;
}
if (ownerField) data[ownerField] = member.id;
try {
var created = await memberstack.createDataRecord({ table: table, data: data });
return created && created.data ? created.data.id : null;
} catch (innerErr) {
if (!ownerField) throw innerErr;
data[ownerField] = { id: member.id };
try {
var created2 = await memberstack.createDataRecord({ table: table, data: data });
return created2 && created2.data ? created2.data.id : null;
} catch (innerErr2) {
var minimal2 = {};
minimal2[field] = value;
if (ownerField) minimal2[ownerField] = member.id;
if (debug) console.warn("[Memberscript # number229] Create with extra fields failed, retrying with username + owner only.", innerErr2);
var created3 = await memberstack.createDataRecord({ table: table, data: minimal2 });
return created3 && created3.data ? created3.data.id : null;
}
}
}
if (createOnSubmit && formEl) {
var hasSaved = false;
formEl.addEventListener("submit", async function(e) {
if (hasSaved) return;
if (submitButton && submitButton.classList.contains("disabled")) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if (!member) return;
var value = input.value;
if (doTrim) value = value.trim();
if (value.length < minLength) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if (takenByOthers.has(normalize(value))) {
e.preventDefault();
e.stopImmediatePropagation();
setState("taken");
return;
}
e.preventDefault();
e.stopImmediatePropagation();
// Re-fetch right before save to keywordcatch any racing claims.
try {
var freshRecords = await fetchAllRecords(table, pageSize);
var claimedByOther = false;
freshRecords.forEach(function(r) {
var v = r && r.data ? r.data[field] : null;
if (v === null || v === undefined || v === "") return;
if (normalize(v) !== normalize(value)) return;
var owner = fieldRefId(r, ownerField);
if (!member || owner !== member.id) claimedByOther = true;
});
if (claimedByOther) {
takenByOthers.add(normalize(value));
setState("taken");
if (debug) {
console.warn("[Memberscript # number229] \"" + value + "\" was claimed by another member after page load. Refusing to create duplicate.");
}
return;
}
} catch (err) {
console.warn("[Memberscript # number229] Could not re-verify uniqueness before save", err);
}
try {
await upsertOwnedRecord(value);
ownedByMe.add(normalize(value));
} catch (err) {
console.error("Memberscript # number229: Could not save record", err);
return;
}
hasSaved = true;
if (typeof formEl.requestSubmit === " keywordfunction") {
formEl.requestSubmit();
} else {
formEl.submit();
}
}, true);
}
}
// =====================================================================
// USERS LIST
// =====================================================================
if (listContainer) {
var listTemplate = listContainer.querySelector('[data-ms-code="list-template"]');
if (!listTemplate) return;
var listTable = attr(listContainer, "ms-code-table") || "usernames";
var listLoading = listContainer.querySelector('[data-ms-code="list-loading"]');
var listEmpty = listContainer.querySelector('[data-ms-code="list-empty"]');
var listError = listContainer.querySelector('[data-ms-code="list-error"]');
var listSort = attr(listContainer, "ms-code-sort") || "createdAt:desc";
var listPageSize = parseInt(attr(listContainer, "ms-code-page-size")) || 100;
var templateClone = listTemplate.cloneNode(true);
templateClone.removeAttribute("data-ms-code");
hide(listTemplate);
hide(listEmpty);
hide(listError);
show(listLoading);
try {
var listRecords = await fetchAllRecords(listTable, listPageSize);
var sortParts = listSort.split(":");
var sortField = sortParts[0];
var sortDir = sortParts[1] || "desc";
listRecords.sort(function(a, b) {
var va = sortField === "createdAt"
? a.createdAt
: (a.data && a.data[sortField]) || "";
var vb = sortField === "createdAt"
? b.createdAt
: (b.data && b.data[sortField]) || "";
if (va < vb) return sortDir === "asc" ? -1 : 1;
if (va > vb) return sortDir === "asc" ? 1 : -1;
return 0;
});
hide(listLoading);
if (listRecords.length === 0) {
show(listEmpty);
} else {
listRecords.forEach(function(record) {
var item = templateClone.cloneNode(true);
var data = record.data || {};
var fieldEls = item.querySelectorAll("[data-ms-field]");
fieldEls.forEach(function(el) {
var name = el.getAttribute("data-ms-field");
var type = el.getAttribute("ms-field") || "text";
var value = data[name];
if (value === undefined || value === null || value === "") {
if (el.hasAttribute("data-ms-hide-empty")) hide(el);
return;
}
if (type === "image") {
el.src = value;
} else if (type === "html") {
el.innerHTML = value;
} else if (type === "link") {
el.href = value;
} else {
el.textContent = value;
}
});
listContainer.appendChild(item);
});
}
} catch (err) {
console.error("Memberscript # number229: Could not load list", err);
hide(listLoading);
show(listError);
}
}
});
</script>More scripts in Data Tables