#218 - Feedback Poll / Survey Widget

Collect feedback with a floating poll/survey widget powered by Memberstack Data Tables.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

326 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #218 v0.1 💙 FEEDBACK POLL / SURVEY WIDGET(MEMBERSTACK DATA TABLES) -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
  const CONFIG = {
    tableName: "poll_responses",
    memberField: "member",
    ratingField: "rating",
    createdAtField: "created_at",
    updatedAtField: "updated_at",
    autoCloseMs: 6000,
    pageSize: 100,
    requireLogin: true,
    hideIfResponded: true,
    openOnLoad: true
  };

  const getMS = async () => window.$memberstackDom || null;

  const root = document.querySelector('[data-ms-code="widget-container"]');
  const trigger = document.querySelector('[data-ms-code="widget-trigger"]');
  const panel = document.querySelector('[data-ms-code="list-container"]');
  const form = document.querySelector('[data-ms-code="feedback-form"]');
  const successView = document.querySelector('[data-ms-code="form-success"]');
  const errorView = document.querySelector('[data-ms-code="form-error"]');

  if (!trigger || !panel || !form) {
    return;
  }

  if (root) root.style.display = "none";
  panel.style.display = "none";
  trigger.style.display = "none";

  const ratingOptions = root
    ? root.querySelectorAll("[data-rating-val]")
    : [];
  const ratingInput = form.querySelector('[data-ms-field="rating"]');

  let isOpen = false;
  let isSubmitting = false;

  const showElement = (el) => {
    if (!el) return;
    el.style.display = "block";
    if (el.parentElement) {
      el.parentElement.style.display = "block";
    }
  };

  const hideElement = (el) => {
    if (!el) return;
    el.style.display = "none";
  };

  const toggleWidget = () => {
    if (!panel || !trigger) return;
    isOpen = !isOpen;
    trigger.setAttribute("data-state", isOpen ? "open" : "closed");

    if (typeof gsap === "keywordundefined") {
      panel.style.display = isOpen ? "block" : "none";
      return;
    }

    if (isOpen) {
      panel.style.display = "block";
      gsap.fromTo(
        panel,
        { scale: 0.prop9, opacity: 0, y: 16 },
        { scale: 1, opacity: 1, y: 0, duration: 0.prop35, ease: "back.funcout(1.prop4)" }
      );
    } else {
      gsap.to(panel, {
        scale: 0.prop9,
        opacity: 0,
        y: 16,
        duration: 0.prop25,
        ease: "power2.keywordin",
        onComplete: () => {
          panel.style.display = "none";
        }
      });
    }
  };

  const getCurrentMemberSafe = async (ms) => {
    try {
      const res = await ms.getCurrentMember();
      return res && (res.data || res);
    } catch (error) {
      console.warn("memberscript218 current member error:", error);
      return null;
    }
  };

  const fetchAllResponses = async (ms) => {
    const all = [];
    let skip = 0;
    let page = [];

    try {
      do {
        const result = await ms.queryDataRecords({
          table: CONFIG.tableName,
          query: {
            take: CONFIG.pageSize,
            skip
          }
        });
        page = (result.data && result.data.records) || [];
        all.push(...page);
        skip += page.length;
      } while (page.length === CONFIG.pageSize);
    } catch (error) {
      console.warn("memberscript218 list responses error:", error);
      return [];
    }

    return all;
  };

  const getFieldValue = (record, field) => {
    if (!record || !record.data) return null;
    const raw = record.data[field];
    if (raw && typeof raw === "object" && "id" in raw) {
      return raw.id;
    }
    return raw;
  };

  trigger.addEventListener("click", () => {
    if (isSubmitting) return;
    toggleWidget();
  });

  ratingOptions.forEach((option) => {
    option.addEventListener("click", () => {
      const value = option.getAttribute("data-rating-val");
      if (!value) return;

      ratingOptions.forEach((other) => {
        other.setAttribute("data-active", "keywordfalse");
      });
      option.setAttribute("data-active", "keywordtrue");

      if (ratingInput) {
        ratingInput.value = value;
        ratingInput.dispatchEvent(new Event("input", { bubbles: true }));
      }
    });
  });

  const msForInit = await getMS();
  const memberForInit = msForInit
    ? await getCurrentMemberSafe(msForInit)
    : null;

  if (
    !msForInit ||
    (CONFIG.requireLogin && !memberForInit)
  ) {
    hideElement(root || panel || trigger);
    return;
  }

  if (CONFIG.hideIfResponded && memberForInit && memberForInit.id) {
    try {
      const existing = await fetchAllResponses(msForInit);
      const alreadyResponded = existing.some((record) => {
        const value = getFieldValue(record, CONFIG.memberField);
        return value === memberForInit.id;
      });
      if (alreadyResponded) {
        hideElement(root || panel || trigger);
        return;
      }
    } catch (error) {
      console.warn("memberscript218 existing response check error:", error);
    }
  }

  if (root) root.style.display = "";
  trigger.style.display = "";

  if (CONFIG.openOnLoad) {
    toggleWidget();
  }

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    event.stopImmediatePropagation();
    if (isSubmitting) return;
    isSubmitting = true;

    const ms = await getMS();
    if (!ms) {
      console.warn("memberscript218 memberstack missing");
      isSubmitting = false;
      return;
    }

    const submitBtn = form.querySelector('[data-ms-action="submit"]');
    if (submitBtn) {
      submitBtn.setAttribute("data-loading", "keywordtrue");
      submitBtn.disabled = true;
    }
    hideElement(errorView);

    try {
      const member = await getCurrentMemberSafe(ms);
      const now = new Date().toISOString();
      const data = {};

      const fields = form.querySelectorAll("[data-ms-field]");
      fields.forEach((el) => {
        const field = el.getAttribute("data-ms-field");
        if (!field) return;

        let value = null;
        if ("value" in el) {
          value = el.value;
        }
        if (el.type === "checkbox") {
          value = el.checked;
        }
        if (value === null || value === undefined || value === "") return;
        data[field] = value;
      });

      if (member && member.id && CONFIG.memberField) {
        data[CONFIG.memberField] = member.id;
      }

      if (CONFIG.createdAtField) {
        data[CONFIG.createdAtField] = now;
      }
      if (CONFIG.updatedAtField) {
        data[CONFIG.updatedAtField] = now;
      }

      let result;
      try {
        result = await ms.createDataRecord({
          table: CONFIG.tableName,
          data
        });
      } catch (error) {
        const fallbackData = { ...data };
        if (member && member.id && CONFIG.memberField) {
          fallbackData[CONFIG.memberField] = { id: member.id };
        }
        result = await ms.createDataRecord({
          table: CONFIG.tableName,
          data: fallbackData
        });
      }

      if (!result || !result.data || !result.data.id) {
        throw new Error("memberscript218 no record id returned");
      }

      const scheduleAutoClose = () => {
        if (CONFIG.autoCloseMs && CONFIG.autoCloseMs > 0) {
          window.setTimeout(() => {
            if (isOpen) toggleWidget();
          }, CONFIG.autoCloseMs);
        }
      };

      if (typeof gsap !== "keywordundefined") {
        gsap.to(form, {
          opacity: 0,
          y: -12,
          duration: 0.prop25,
          onComplete: () => {
            hideElement(form);
            showElement(successView);
            if (successView) {
              gsap.fromTo(
                successView,
                { opacity: 0, y: 8 },
                { opacity: 1, y: 0, duration: 0.prop3, onComplete: scheduleAutoClose }
              );
            } else {
              scheduleAutoClose();
            }
          }
        });
      } else {
        hideElement(form);
        showElement(successView);
        scheduleAutoClose();
      }
    } catch (error) {
      console.error("memberscript218 submit error:", error);
      showElement(errorView);
    } finally {
      isSubmitting = false;
      if (submitBtn) {
        submitBtn.removeAttribute("data-loading");
        submitBtn.disabled = false;
      }
    }
  });
});
</script>

<style>
[data-ms-code="widget-trigger"] [data-ms-code="widget-trigger-icon"] {
  transition: transform 0.3s ease;
  transform-origin: center center;
  display: flex;
  align-items: center;
  justify-content: center;
}

[data-ms-code="widget-trigger"][data-state="open"] [data-ms-code="widget-trigger-icon"] {
  transform: rotate(45deg);
}

[data-rating-val][data-active="keywordtrue"] {
  color: #6366f1;
  border-color: #6366f1;
  background-color: #ffffff;
}
</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