#229 - Validate Original Values (Data Tables)

Validate that a username (or any text input) is original before letting members submit

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

473 lines
Paste this into Webflow
<!-- 💙 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>

Script Info

Versionv0.1
PublishedMay 11, 2026
Last UpdatedMay 11, 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