Trying to save Custom Widget Builder options RESETS the Widget Code!

Trying to save Custom Widget Builder options RESETS the Widget Code!! (basically, as if it was a new custom widget)

I have this widget I am working on, for a card grid

it has several options when clicking that gear button on the bottom left, like changing colors, number of columns, etc,

but when I try to apply the option to PERSIST the data, when saving, it RESETS the whole widget code!! Really weird.

Really is the JS… please help finding the faulty code

/******************************************************
 * 1) GRIST BUILT-IN COLUMN MAPPING + onRecords
 * 2) TWO-PANEL CONFIG MENU (Column Mapping, Appearance, Child Table)
 * 3) EDIT ICON ON EACH CARD (placeholder)
 ******************************************************/

// Declare expected columns for built-in mapping.
grist.ready({
  requiredAccess: 'full',
  columns: [
    { name: "Title",    optional: false, type: "Any" },
    { name: "Field1",   optional: false, type: "Any" },
    { name: "Field2",   optional: true,  type: "Any" },
    { name: "Field3",   optional: true,  type: "Any" },
    { name: "Field4",   optional: true,  type: "Any" },
    { name: "Field5",   optional: true,  type: "Any" },
    { name: "Field6",   optional: true,  type: "Any" },
    { name: "Ordering", optional: true,  type: "Any" }
  ],
  onEditOptions: () => {
    configPanel.style.display = 'block';
  }
});

let mainRecords = [];      
let childRecords = [];     
let columnLabels = {
  field1: "Platform",
  field2: "Release",
  field3: "Status",
  field4: "Length (minutes)",
  field5: "Budget",
  field6: ""
};
let showChildRecords = true; 

// Load saved Appearance and Column Mapping settings.
grist.onOptions((options) => {
  if (options) {
    // Load Appearance settings
    document.body.style.backgroundColor = options.widgetBgColor || "#ffffff";
    // Load field label overrides
    columnLabels.field1 = options.labelField1 || "Platform";
    columnLabels.field2 = options.labelField2 || "Release";
    columnLabels.field3 = options.labelField3 || "Status";
    columnLabels.field4 = options.labelField4 || "Length (minutes)";
    columnLabels.field5 = options.labelField5 || "Budget";
    columnLabels.field6 = options.labelField6 || "";
    // Load child record toggle
    showChildRecords = options.childRecordsOn !== undefined ? options.childRecordsOn : true;
    // Load other appearance settings if needed...
  } else {
    document.body.style.backgroundColor = "#ffffff";
  }
  renderCards();
});

/******************************************************
 * onRecords: fetch mapped data, fetch child records
 ******************************************************/
grist.onRecords(async (records, mappings) => {
  mainRecords = records.map(r => grist.mapColumnNames(r) || {});
  if (mainRecords.length && mainRecords[0].Ordering !== undefined) {
    mainRecords.sort((a, b) => {
      let A = a.Ordering || "", B = b.Ordering || "";
      return A > B ? 1 : (A < B ? -1 : 0);
    });
  }
  await fetchChildRecords();
  renderCards();
});

/******************************************************
 * Fetch child records (Subform table)
 ******************************************************/
async function fetchChildRecords() {
  try {
    const tableData = await grist.docApi.fetchTable("Subform");
    childRecords = tableData.id.map((_, idx) => ({
      id: tableData.id[idx],
      Ref_Test_Card: tableData.Ref_Test_Card[idx],
      Detail: tableData.Detail[idx],
      ExtraInfo: tableData.ExtraInfo[idx]
    }));
  } catch (err) {
    console.error("Could not fetch Subform table:", err);
  }
}

/******************************************************
 * Create card HTML from a mapped record
 ******************************************************/
function createCard(record) {
  const title = record.Title || "(Untitled)";
  const f1 = record.Field1 ?? "";
  const f2 = record.Field2 ?? "";
  const f3 = record.Field3 ?? "";
  const f4 = record.Field4 ?? "";
  const f5 = record.Field5 ?? "";
  const f6 = record.Field6 ?? "";

  const fieldsHTML = [];
  if (f1 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field1}:</div>
        <div>${f1}</div>
      </div>`);
  if (f2 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field2}:</div>
        <div>${f2}</div>
      </div>`);
  if (f3 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field3}:</div>
        <div>${f3}</div>
      </div>`);
  if (f4 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field4}:</div>
        <div>${f4}</div>
      </div>`);
  if (f5 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field5}:</div>
        <div>${f5}</div>
      </div>`);
  if (f6 !== "") fieldsHTML.push(`
      <div class="field">
        <div class="field-title">${columnLabels.field6}:</div>
        <div>${f6}</div>
      </div>`);

  let childSection = "";
  if (showChildRecords) {
    const rowId = record.id;
    const children = childRecords.filter(ch => ch.Ref_Test_Card === rowId);
    if (children.length) {
      const childRows = children.slice(0,5).map(ch => `
        <tr>
          <td>${ch.Detail || ""}</td>
          <td>${ch.ExtraInfo || ""}</td>
        </tr>`).join("");
      childSection = `
        <div class="card-child-container">
          <div class="field-title">Child Records:</div>
          <div class="child-table-container">
            <table class="child-table">
              <thead>
                <tr><th>Detail</th><th>Extra Info</th></tr>
              </thead>
              <tbody>${childRows}</tbody>
            </table>
          </div>
        </div>`;
    }
  }

  return `
    <div class="card" data-id="${record.id}">
      <div class="edit-icon">&#9998;</div>
      <div class="card-title-container">
        <h3>${title}</h3>
      </div>
      <div class="card-fields-container">
        ${fieldsHTML.join("")}
      </div>
      ${childSection}
    </div>
  `;
}

/******************************************************
 * Render the cards
 ******************************************************/
function renderCards() {
  const container = document.getElementById('card-container');
  if (!mainRecords.length) {
    container.innerHTML = "<p>No records (or incomplete column mapping).</p>";
    return;
  }
  container.innerHTML = mainRecords.map(createCard).join('');

  new Sortable(container, {
    animation: 150,
    onEnd: function (event) {
      console.log(`Card with ID ${event.item.dataset.id} moved from index ${event.oldIndex} to ${event.newIndex}`);
    }
  });

  adjustCardSize();
}

/******************************************************
 * Responsive card sizing
 ******************************************************/
function adjustCardSize() {
  const container = document.getElementById('card-container');
  const width = container.offsetWidth;
  const cards = container.querySelectorAll('.card');

  cards.forEach(card => {
    if (width >= 1600)      card.style.flex = '1 1 calc(20% - 1em)';
    else if (width >= 1200) card.style.flex = '1 1 calc(25% - 1em)';
    else if (width >= 900)  card.style.flex = '1 1 calc(33.33% - 1em)';
    else if (width >= 600)  card.style.flex = '1 1 calc(50% - 1em)';
    else                    card.style.flex = '1 1 calc(100% - 1em)';
  });
}
window.addEventListener('resize', adjustCardSize);
grist.onRecords(() => setTimeout(adjustCardSize, 0));

/******************************************************
 * Configuration Panel Logic
 ******************************************************/
const gearButton = document.getElementById('gear-button');
const configPanel = document.getElementById('config-panel');

// Sidebar menu buttons
const btnColumnMapping = document.getElementById('btn-column-mapping');
const btnAppearance    = document.getElementById('btn-appearance');
const btnChildTable    = document.getElementById('btn-child-table');

// Sub-panel containers
const columnMappingPanel = document.getElementById('column-mapping-panel');
const appearancePanel    = document.getElementById('appearance-panel');
const childTablePanel    = document.getElementById('child-table-panel');

function showSubPanel(panelEl) {
  columnMappingPanel.style.display = 'none';
  appearancePanel.style.display = 'none';
  childTablePanel.style.display = 'none';
  panelEl.style.display = 'block';
}

// Toggle config panel visibility
gearButton.addEventListener('click', () => {
  configPanel.style.display = (!configPanel.style.display || configPanel.style.display === 'none') 
                               ? 'block' : 'none';
  showSubPanel({style:{display:'none'}});
});

// Sidebar button click events
btnColumnMapping.addEventListener('click', () => showSubPanel(columnMappingPanel));
btnAppearance.addEventListener('click', () => showSubPanel(appearancePanel));
btnChildTable.addEventListener('click', () => showSubPanel(childTablePanel));

/******************************************************
 * Appearance Panel Logic (apply changes with persistence)
 ******************************************************/
const defaultValues = {
  "widget-bg-color": "#ffffff",
  "card-bg-color": "#ffffff",
  "card-border-color": "#cccccc",
  "card-border-width": "1",
  "card-shadow": "0 2px 5px rgba(100, 100, 100, 0.1)",
  "font-size": "0.9",
  "card-min-width": "100",
  "card-max-width": "",
  "card-min-height": "",
  "card-max-height": "",
  "column-layout": "3"
};

function revertToDefault(inputId) {
  const defVal = defaultValues[inputId] || "";
  const el = document.getElementById(inputId);
  if (el) el.value = defVal;
}
document.querySelectorAll('.reset-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    const targetId = btn.getAttribute('data-target');
    revertToDefault(targetId);
  });
});

document.getElementById('apply-config').addEventListener('click', async () => {
  const newOptions = {
    widgetBgColor:   document.getElementById('widget-bg-color').value,
    cardBgColor:     document.getElementById('card-bg-color').value,
    cardBorderColor: document.getElementById('card-border-color').value,
    cardBorderWidth: document.getElementById('card-border-width').value,
    cardShadow:      document.getElementById('card-shadow').value,
    fontSize:        document.getElementById('font-size').value,
    cardMinWidth:    document.getElementById('card-min-width').value,
    cardMaxWidth:    document.getElementById('card-max-width').value,
    cardMinHeight:   document.getElementById('card-min-height').value,
    cardMaxHeight:   document.getElementById('card-max-height').value,
    columnLayout:    document.getElementById('column-layout').value
  };

  await grist.setOptions(newOptions);

  // Apply changes locally
  document.body.style.backgroundColor = newOptions.widgetBgColor;
  const allCards = document.querySelectorAll('.card');
  allCards.forEach(card => {
    card.style.backgroundColor = newOptions.cardBgColor;
    card.style.borderColor     = newOptions.cardBorderColor;
    card.style.borderWidth     = newOptions.cardBorderWidth + "px";
    card.style.borderStyle     = "solid";
    card.style.boxShadow       = newOptions.cardShadow;
    card.style.fontSize        = newOptions.fontSize + "em";

    card.style.minWidth  = newOptions.cardMinWidth ? newOptions.cardMinWidth + "px" : "0";
    card.style.maxWidth  = newOptions.cardMaxWidth ? newOptions.cardMaxWidth + "px" : "none";
    card.style.minHeight = newOptions.cardMinHeight ? newOptions.cardMinHeight + "px" : "auto";
    card.style.maxHeight = newOptions.cardMaxHeight ? newOptions.cardMaxHeight + "px" : "none";
  });

  allCards.forEach(card => {
    const fieldsContainer = card.querySelector('.card-fields-container');
    if (!fieldsContainer) return;
    fieldsContainer.style.display = 'grid';
    fieldsContainer.style.gridTemplateColumns = `repeat(${newOptions.columnLayout}, 1fr)`;
    fieldsContainer.style.columnGap = '1em';
    fieldsContainer.style.rowGap = '0.5em';
  });

  adjustCardSize();
  configPanel.style.display = 'none';
});

/******************************************************
 * Column Mapping Panel Logic (apply changes with persistence)
 ******************************************************/
const btnApplyMapping = document.getElementById('apply-column-mapping');
btnApplyMapping.addEventListener('click', async () => {
  const newOptions = {
    labelField1: document.getElementById('label-field1').value || "Platform",
    labelField2: document.getElementById('label-field2').value || "Release",
    labelField3: document.getElementById('label-field3').value || "Status",
    labelField4: document.getElementById('label-field4').value || "Length (minutes)",
    labelField5: document.getElementById('label-field5').value || "Budget",
    labelField6: document.getElementById('label-field6').value || "",
    childRecordsOn: document.getElementById('toggle-child-records').checked
  };

  await grist.setOptions(newOptions);

  columnLabels.field1 = newOptions.labelField1;
  columnLabels.field2 = newOptions.labelField2;
  columnLabels.field3 = newOptions.labelField3;
  columnLabels.field4 = newOptions.labelField4;
  columnLabels.field5 = newOptions.labelField5;
  columnLabels.field6 = newOptions.labelField6;
  showChildRecords   = newOptions.childRecordsOn;

  renderCards();
  configPanel.style.display = 'none';
});

HTML code

<!DOCTYPE html>
<html>
<head>
  <!-- Grist Plugin API -->
  <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
  <!-- SortableJS for drag-and-drop -->
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
  <style>
    body {
      font-family: sans-serif;
      padding: 1em;
      margin: 0;
    }
    #card-container {
      display: flex;
      flex-wrap: wrap;
      gap: 1em; 
      justify-content: flex-start; 
    }
    .card {
      flex: 1 1 calc(33.33% - 1em); /* Default: 3 cards per row */
      max-width: calc(100% - 1em);
      min-width: 100px; 
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 1em;
      box-shadow: 0 2px 5px rgba(100, 100, 100, 0.1);
      background: #fff;
      cursor: grab;
      transition: flex-basis 0.2s ease-in-out;
      position: relative; /* For the edit icon */
    }
    .card:active {
      cursor: grabbing;
    }
    /* EDIT icon in the top-right corner */
    .edit-icon {
      position: absolute;
      top: 8px;
      right: 8px;
      font-size: 1em;
      cursor: pointer;
      color: #555;
    }
    .edit-icon:hover {
      color: #111;
    }
    /* Title container (full width) */
    .card-title-container h3 {
      margin: 0 0 1em 0;
      font-size: 1em;
    }
    /* The .card-fields-container is where we apply CSS Grid for columns (in JS) */
    .card-fields-container {
      margin-bottom: 1em;
      display: block; 
    }
    .card-fields-container .field {
      margin-bottom: 0.5em;
      font-size: 0.9em;
    }
    .card-fields-container .field-title {
      font-weight: bold;
      margin-bottom: 0.2em;
      font-size: 0.85em;
    }
    .card-child-container {
      margin-top: 1em;
      border-top: 1px solid #ccc;
      padding-top: 1em;
    }
    .child-table-container {
      max-height: 150px;
      overflow-y: auto;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
    .child-table {
      width: 100%;
      border-collapse: collapse;
      border-spacing: 0;
      font-size: 0.85em;
    }
    .child-table th {
      position: sticky;
      top: 0;
      background-color: #f4f4f4;
      z-index: 1;
      border-bottom: 2px solid #ccc;
      text-align: left;
      padding: 0.5em;
      font-weight: bold;
    }
    .child-table td {
      padding: 0.5em;
      border-bottom: 1px solid #ccc;
    }
    .child-table tr:nth-child(even) {
      background-color: #f7f7f7 !important; 
    }
    .badge {
      display: inline-block;
      padding: 0.2em 0.5em;
      font-size: 0.8em;
      border-radius: 4px;
      color: #fff;
    }
    /* Platform color classes */
    .platform-instagram { background: #e1306c; }
    .platform-spotify  { background: #1DB954; }
    .platform-youtube  { background: #FF0000; }
    .platform-facebook { background: #4267B2; }
    /* Gear Button (overlay) */
    #gear-button {
      position: fixed;
      bottom: 10px;
      left: 10px;
      font-size: 24px;
      color: #555;
      background: none;
      border: none;
      cursor: pointer;
      z-index: 2000; 
    }
    /* Config Panel: now two columns: sidebar + content area. */
    #config-panel {
      position: fixed;
      bottom: 60px;
      left: 10px;
      background: #fff;
      border: 1px solid #ccc;
      border-radius: 8px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.2);
      display: none; 
      z-index: 2001;
      width: 600px; /* bigger panel for two-column layout */
      height: 400px; /* fix a height for example */
    }
    /* Sidebar on the left */
    #sidebar {
      float: left;
      width: 150px;
      border-right: 1px solid #ccc;
      height: 100%;
      box-sizing: border-box;
      padding: 0.5em;
    }
    #sidebar button {
      display: block;
      width: 100%;
      text-align: left;
      background: none;
      border: none;
      padding: 0.5em;
      font-size: 0.9em;
      cursor: pointer;
    }
    #sidebar button:hover {
      background: #f0f0f0;
    }
    /* Content area on the right */
    #content {
      float: left;
      width: 450px;
      height: 100%;
      box-sizing: border-box;
      padding: 1em;
      overflow-y: auto;
    }
    .sub-panel {
      display: none;
    }
    #config-panel::after {
      content: "";
      display: block;
      clear: both;
    }
    /* Appearance items */
    .appearance-item {
      position: relative;
      margin-bottom: 1em;
    }
    .appearance-item label {
      display: inline-block;
      width: 80%;
    }
    .reset-btn {
      position: absolute;
      right: 1px;
      top: 50%;
      transform: translateY(-50%);
      background: none;
      border: none;
      cursor: pointer;
      font-size: 0.9em;
      color: #888;
    }
    .reset-btn:hover {
      color: #000;
    }
    /* Column Mapping form style */
    .column-mapping-item {
      margin-bottom: 0.8em;
    }
    .column-mapping-item label {
      display: block;
      font-size: 0.9em;
      margin-bottom: 0.3em;
    }
    .toggle-child-records {
      margin-top: 1em;
    }
    .column-mapping-apply {
      margin-top: 1em;
    }
  </style>
</head>
<body>
  <!-- Cards container -->
  <div id="card-container"></div>

  <!-- Gear button -->
  <button id="gear-button" aria-label="Settings">&#9881;</button>

  <!-- Config panel with sidebar + content -->
  <div id="config-panel">
    <div id="sidebar">
      <button id="btn-column-mapping">Column Mapping</button>
      <button id="btn-appearance">Appearance</button>
      <button id="btn-child-table">Child Table Mapping</button>
    </div>
    <div id="content">
      <!-- Column Mapping Panel -->
      <div id="column-mapping-panel" class="sub-panel">
        <h3>Column Mapping</h3>
        <p>Select field labels (except Title is fixed). Toggle child records on/off.</p>

        <div class="column-mapping-item">
          <label>Title (fixed label, no rename)</label>
          <p><em>Selected in Grist’s column mapping UI</em></p>
        </div>
        <div class="column-mapping-item">
          <label>Field 1 (label):
            <input type="text" id="label-field1" value="Platform">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Field 2 (label):
            <input type="text" id="label-field2" value="Release">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Field 3 (label):
            <input type="text" id="label-field3" value="Status">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Field 4 (label):
            <input type="text" id="label-field4" value="Length (minutes)">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Field 5 (label):
            <input type="text" id="label-field5" value="Budget">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Field 6 (label):
            <input type="text" id="label-field6" value="">
          </label>
        </div>
        <div class="column-mapping-item">
          <label>Ordering Column (selected in Grist’s UI; no label rename here)</label>
          <p><em>Optional, sorts cards by that field</em></p>
        </div>
        <div class="toggle-child-records">
          <label>
            <input type="checkbox" id="toggle-child-records" checked>
            Show Child Records
          </label>
        </div>
        <button id="apply-column-mapping" class="column-mapping-apply">Apply Mapping</button>
      </div>

      <!-- Appearance Panel -->
      <div id="appearance-panel" class="sub-panel">
        <h3>Appearance</h3>
        <!-- Each item has label + input + reset icon -->
        <div class="appearance-item">
          <label>Widget Background Color:
            <input type="color" id="widget-bg-color" value="#ffffff">
          </label>
          <button class="reset-btn" data-target="widget-bg-color">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Background Color:
            <input type="color" id="card-bg-color" value="#ffffff">
          </label>
          <button class="reset-btn" data-target="card-bg-color">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Border Color:
            <input type="color" id="card-border-color" value="#cccccc">
          </label>
          <button class="reset-btn" data-target="card-border-color">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Border Width (px):
            <input type="number" id="card-border-width" value="1" min="0">
          </label>
          <button class="reset-btn" data-target="card-border-width">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Shadow (CSS):
            <input type="text" id="card-shadow" value="0 2px 5px rgba(100, 100, 100, 0.1)">
          </label>
          <button class="reset-btn" data-target="card-shadow">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Font Size (em):
            <input type="number" id="font-size" value="0.9" step="0.1" min="0.5">
          </label>
          <button class="reset-btn" data-target="font-size">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Min Width (px):
            <input type="number" id="card-min-width" value="100" min="0">
          </label>
          <button class="reset-btn" data-target="card-min-width">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Max Width (px):
            <input type="number" id="card-max-width" placeholder="none">
          </label>
          <button class="reset-btn" data-target="card-max-width">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Min Height (px):
            <input type="number" id="card-min-height" placeholder="auto">
          </label>
          <button class="reset-btn" data-target="card-min-height">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Card Max Height (px):
            <input type="number" id="card-max-height" placeholder="none">
          </label>
          <button class="reset-btn" data-target="card-max-height">⟲</button>
        </div>
        <div class="appearance-item">
          <label>Column Layout:
            <select id="column-layout">
              <option value="1">1 Column</option>
              <option value="2">2 Columns</option>
              <option value="3" selected>3 Columns</option>
            </select>
          </label>
          <button class="reset-btn" data-target="column-layout">⟲</button>
        </div>
        <button id="apply-config">Apply</button>
      </div>

      <!-- Child Table Mapping Panel (placeholder) -->
      <div id="child-table-panel" class="sub-panel">
        <h3>Child Table Mapping</h3>
        <p>Placeholder for future child table mapping options.</p>
      </div>
    </div>
  </div>

  <script src="script.js"></script>
</body>
</html>

Hi @Rogerio_Penna, in the custom widget builder you can’t use onEditOptions API, this API is used to open the editor by the Widget Builder itself, and it is very sensitive to it.

You need to add some UI element to configure your widget, or host it on some external site.

But thanks for the source code, I’ll try to come up with some clever trick that will allow this API to be used, but it may take some time.