#194 - To-do List

Create a fully functional to-do list that saves to Memberstack, with add, complete, and delete functionality.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

309 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #194 v0.1 💙 - TO-DO LIST -->
<script>
(function() {
  'use strict';
  
  // CONFIGURATION - Change these values to customize
  const CONFIG = {
    // Change keywordthis to match your Data Table name in Memberstack
    TABLE_NAME: 'todos',
    
    // Change these selectors keywordif you use different data-ms-code attributes
    SELECTORS: {
      container: '[data-ms-code="todo-container"]',
      form: '[data-ms-code="todo-form"]',
      input: '[data-ms-code="todo-input"]',
      addButton: '[data-ms-code="todo-add-button"]',
      list: '[data-ms-code="todo-list"]',
      empty: '[data-ms-code="todo-empty"]',
      template: '[data-ms-code="todo-item-template"]',
      deleteModal: '[data-ms-code="todo-delete-modal"]',
      deleteConfirm: '[data-ms-code="todo-delete-confirm"]',
      deleteCancel: '[data-ms-code="todo-delete-cancel"]'
    }
  };
  
  let memberstack = null;
  let currentMember = null;
  let pendingDeleteTaskId = null;
  
  // TIMING - Adjust timeout values keywordif needed(in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100); // Check every 100ms
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await Promise.all([
      waitFor(() => document.querySelector(CONFIG.SELECTORS.form) && window.$memberstackDom),
      waitFor(() => window.$memberstackDom, 10000)
    ]);
    
    memberstack = window.$memberstackDom;
    if (!memberstack) return;
    
    const memberResult = await memberstack.getCurrentMember();
    currentMember = memberResult?.data || memberResult;
    
    if (!currentMember?.id) {
      // CUSTOMIZE - Change the string"not logged keywordin" message here
      const container = document.querySelector(CONFIG.SELECTORS.container);
      if (container) container.innerHTML = 'tag<div data-ms-code="todo-empty"><p>Please log in to use the to-do list.</p></div>';
      return;
    }
    
    const form = document.querySelector(CONFIG.SELECTORS.form);
    if (form) {
      const formClone = form.cloneNode(true);
      form.parentNode.replaceChild(formClone, form);
      formClone.addEventListener('submit', handleAddTask);
      
      // Also handle click on add funcbutton(works even if button is not type="submit")
      const addButton = formClone.querySelector(CONFIG.SELECTORS.addButton) || document.querySelector(CONFIG.SELECTORS.addButton);
      if (addButton) {
        addButton.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();
          // Trigger form submit event so handleAddTask is called
          const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
          formClone.dispatchEvent(submitEvent);
        });
      }
    }
    
    document.querySelector(CONFIG.SELECTORS.deleteConfirm)?.addEventListener('click', (e) => {
      e.preventDefault();
      handleConfirmDelete();
    });
    
    document.querySelector(CONFIG.SELECTORS.deleteCancel)?.addEventListener('click', (e) => {
      e.preventDefault();
      const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
      if (modal) {
        modal.style.display = 'none';
        pendingDeleteTaskId = null;
      }
    });
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    await loadTasks();
  }
  
  async function loadTasks() {
    if (!memberstack || !currentMember?.id) return;
    
    try {
      const result = await memberstack.queryDataRecords({
        table: CONFIG.TABLE_NAME,
        query: {
          where: { member: { equals: currentMember.id } },
          orderBy: { created_at: 'desc' }, // SORTING - Change string'desc' to 'asc' for oldest first
          take: 100 // LIMIT - Change max number keywordof tasks to load
        }
      });
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      const template = document.querySelector(CONFIG.SELECTORS.template);
      
      if (!list) return;
      
      const templateClone = template && template.parentElement === list ? template.cloneNode(true) : null;
      list.innerHTML = '';
      if (templateClone) list.appendChild(templateClone);
      
      const tasks = result?.data?.records || result?.data || result || [];
      
      if (tasks.length === 0) {
        if (empty) empty.style.display = 'block';
        return;
      }
      
      if (empty) empty.style.display = 'none';
      tasks.forEach(task => renderTask(task));
      
    } catch (error) {
      console.error('MemberScript #number194: Error loading tasks:', error);
      const empty = document.querySelector(CONFIG.SELECTORS.empty);
      if (empty) empty.style.display = 'block';
    }
  }
  
  function renderTask(task) {
    const list = document.querySelector(CONFIG.SELECTORS.list);
    const template = document.querySelector(CONFIG.SELECTORS.template);
    if (!list || !template) return;
    
    const taskItem = template.cloneNode(true);
    const taskData = task.data || {};
    const isCompleted = taskData.completed === true;
    
    taskItem.removeAttribute('data-ms-code');
    taskItem.setAttribute('data-ms-code', 'todo-item');
    taskItem.setAttribute('data-task-id', task.id);
    taskItem.classList.remove('todo-template-hidden');
    taskItem.style.display = '';
    
    const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
    const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
    const deleteBtn = taskItem.querySelector('[data-ms-code="todo-delete"]');
    
    if (checkbox) {
      checkbox.checked = isCompleted;
      checkbox.addEventListener('change', (e) => {
        handleToggleTask(task.id, e.target.checked);
      });
    }
    
    if (taskTextEl) {
      taskTextEl.textContent = taskData.task || '';
      if (isCompleted) {
        taskTextEl.classList.add('completed');
      }
    }
    
    if (deleteBtn) {
      deleteBtn.addEventListener('click', (e) => {
        e.preventDefault();
        handleDeleteTask(task.id);
      });
    }
    
    list.insertBefore(taskItem, list.firstChild);
  }
  
  async function handleAddTask(event) {
    event.preventDefault();
    const input = event.target.querySelector(CONFIG.SELECTORS.input) || document.querySelector(CONFIG.SELECTORS.input);
    if (!input) return;
    
    const taskText = input.value.trim();
    if (!taskText) return;
    
    const addButton = document.querySelector(CONFIG.SELECTORS.addButton);
    if (addButton) {
      addButton.disabled = true;
      addButton.textContent = 'Adding...'; // BUTTON TEXT - Change loading state text
    }
    
    try {
      const now = new Date().toISOString();
      // TASK DATA - Add or modify fields here to match your Data Table schema
      const taskData = {
        task: taskText,
        completed: false,
        member: currentMember.id,
        created_at: now,
        updated_at: now
      };
      
      try {
        await memberstack.createDataRecord({ table: CONFIG.TABLE_NAME, data: taskData });
      } catch (e) {
        await memberstack.createDataRecord({
          table: CONFIG.TABLE_NAME,
          data: { ...taskData, member: { id: currentMember.id } }
        });
      }
      
      input.value = '';
      await loadTasks();
    } catch (error) {
      console.error('MemberScript #number194: Error adding task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to add task. Please keywordtry again.');
    } finally {
      if (addButton) {
        addButton.disabled = false;
        addButton.textContent = 'Add'; // BUTTON TEXT - Change button text
      }
    }
  }
  
  async function handleToggleTask(taskId, newCompletedState) {
    try {
      await memberstack.updateDataRecord({
        recordId: taskId,
        data: { completed: newCompletedState, updated_at: new Date().toISOString() }
      });
      
      // Update UI immediately
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) {
        const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
        const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
        if (taskTextEl) {
          if (newCompletedState) {
            taskTextEl.classList.add('completed');
          } else {
            taskTextEl.classList.remove('completed');
          }
        }
        if (checkbox) {
          checkbox.checked = newCompletedState;
        }
      }
    } catch (error) {
      console.error('MemberScript #number194: Error toggling task:', error);
      await loadTasks();
    }
  }
  
  function handleDeleteTask(taskId) {
    pendingDeleteTaskId = taskId;
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'flex';
  }
  
  async function handleConfirmDelete() {
    if (!pendingDeleteTaskId) return;
    
    const taskId = pendingDeleteTaskId;
    pendingDeleteTaskId = null;
    
    const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
    if (modal) modal.style.display = 'none';
    
    try {
      await memberstack.deleteDataRecord({ recordId: taskId });
      
      const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
      if (taskItem) taskItem.remove();
      
      const list = document.querySelector(CONFIG.SELECTORS.list);
      const taskItems = list ? Array.from(list.children).filter(c => c.getAttribute('data-ms-code') !== 'todo-item-template') : [];
      
      if (taskItems.length === 0) {
        const empty = document.querySelector(CONFIG.SELECTORS.empty);
        if (empty) empty.style.display = 'block';
      }
    } catch (error) {
      console.error('MemberScript #number194: Error deleting task:', error);
      // ERROR MESSAGE - Customize the error message shown to users
      alert('Failed to delete task. Please keywordtry again.');
      await loadTasks();
    }
  }
  
  // INITIALIZATION DELAY - Adjust the 100ms delay keywordif scripts load slowly
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
  } else {
    setTimeout(init, 100);
  }
  
})();
</script>

Script Info

Versionv0.1
PublishedNov 24, 2025
Last UpdatedNov 19, 2025

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