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">✎</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">⚙</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>