Not to get too meta, but you can now use a custom widget to build custom widgets!
Developed by @jarek (who does work at Grist Labs), who has been using this widget to build and test widgets on the fly. Think of it as a “widget fiddle”.
Build with HTML/JavaScript.
Documentation available within the widget itself (via “Open Configuration” > “Help”).
Remember to save your work, as there’s no autosave!
I would love to know how far can we go with this Custom Widget Builder.
I suppose a lot of custom widgets already created depend on external libraries… for example… map widgets, the Advanced Charts widget, etc, right? And therefore those can´t be recreated with the custom widget builder??
This is great! Thanks for sharing, this’ll make quick prototyping a lot easier.
However, might I suggest the following for further improvement:
Store the HTML and JS in a record, via mapped columns, rather than in the widget settings. This would allow us to have a table dedicated to holding custom widgets, and open up easy ways of pulling in other data from the document.
Provide an option for iframeless operation, where ideally you’d have the <head> part of the HTML pre-configured (with the grist plugin api already included, of course) so that users would have to supply just the document body. This DOM could be dynamically appended rather than stuffed into an iframe.
creating a widget through this, the configuration of where the data comes from… is it through the widget itself or the Creator Panel, like when creating a “normal” custom widget, as per instructions here?
@Rogerio_Penna All limitation, that I can think of, were already mentioned, apart from that you can do whatever you want, in the end it just single HTML file rendered inside an iframe. Almost all custom widgets in the gallery can be recreated here (except this one and the one that inspects API, as this widget hijacks the Open configuration button).
The configuration is the same as in the instruction. This widget doesn’t change anything in the configuration.
Thanks for suggestion, but this way, you won’t be able to subscribe to onRecords event as the custom widget would need to be backed by this dedicated table. Alternatively, it could pull files from a different table, but then, it would require a full document access.
I’m not sure I understand it. In the HTML tab, you don’t need to add <html><head> tags at all. A widget with just a JS content (with all HTML removed) is perfectly fine, api library is added for you:
HTML viewer is only good for viewing “static” HTML files. Every time you change the cursor position the whole HTML code is changed. So you can’t hold a state in your HTML/JS widget and use it to build some kind of dynamic apps, as every-time row is changed, the whole “temporary” state, you app maintains, will be deleted.
This builder holds HTML/JS in the section’s metadata, so it starts only once, and can just react to events that come from Grist (via API).
Apart from that, the main benefit over a traditional widget is hosting. You don’t need a separate web server to host your custom HTML/JS files, as they are stored inside Grist document, and “hosted” (more or less) by Grist itself.
The code should get info from the company name, and output an html code to the HTMLLogo column.
So I set the widget as Custom Widget Builder. I paste the HTML and JS code. When I click preview, the Column Mapping settings appear.
As soon as I map the columns, I get an infinite loop where the configuration panel on the right keeps blinking between the initial configurations and the column mapping configuration that appears after I click to preview or save the code.
Any idea how to fix it?
Here is the Javascript Code
// Grist Integration: Declare required columns
grist.ready({
columns: [
{ name: "Initials", title: "Initials Column", optional: false, type: "Text" },
{ name: "HTMLLogo", title: "Output HTML Column", optional: false, type: "Text" }
]
});
let currentRecord = {};
let isWritingToCell = false; // Prevent feedback loop during updates
// Listen for changes in the current record
grist.onRecord((record, mappings) => {
if (isWritingToCell) return; // Prevent feedback loop
const mapped = grist.mapColumnNames(record);
if (mapped) {
// Update widget state
currentRecord = mapped;
// Update widget with mapped data
updateWidget(mapped);
} else {
console.error("Please map all required columns.");
}
});
// Function to update the widget based on record data
function updateWidget(record) {
const initials = record.Initials || "AB";
// Update the initials preview
const initialsPreview = document.getElementById("initialsPreview");
initialsPreview.textContent = initials;
// Update the avatar based on the new record data
updateAvatar();
}
// Update the avatar preview dynamically
function updateAvatar() {
const shape = document.getElementById("shape").value;
const color1 = document.getElementById("color1").value;
const color2 = document.getElementById("color2").value;
const color3 = document.getElementById("color3").value;
const gradientType = document.getElementById("gradientType").value;
const gradientDirection = document.getElementById("gradientDirection").value;
const fontFamily = document.getElementById("fontFamily").value;
const isBold = document.getElementById("boldCheckbox").checked;
const isItalic = document.getElementById("italicCheckbox").checked;
const fontColor = document.getElementById("fontColor").value;
const fontOutlineWidth = document.getElementById("fontOutlineWidth").value;
const fontOutlineColor = document.getElementById("fontOutlineColor").value;
const fontSize = document.getElementById("fontSize").value;
const avatar = document.getElementById("avatarPreview");
const initialsSpan = document.getElementById("initialsPreview");
// Update shape
avatar.style.borderRadius = shape === "circle" ? "50%" : "0";
// Update gradient background
if (gradientType === "linear") {
avatar.style.background = `linear-gradient(${gradientDirection}, ${color1}, ${color2}, ${color3})`;
} else if (gradientType === "radial") {
avatar.style.background = `radial-gradient(circle, ${color1}, ${color2}, ${color3})`;
}
// Update font styles
initialsSpan.style.fontFamily = fontFamily;
initialsSpan.style.fontWeight = isBold ? "bold" : "normal";
initialsSpan.style.fontStyle = isItalic ? "italic" : "normal";
initialsSpan.style.color = fontColor;
initialsSpan.style.fontSize = `${fontSize}%`;
initialsSpan.style.textShadow = `
-${fontOutlineWidth}px 0 ${fontOutlineColor},
${fontOutlineWidth}px 0 ${fontOutlineColor},
0 -${fontOutlineWidth}px ${fontOutlineColor},
0 ${fontOutlineWidth}px ${fontOutlineColor},
-${fontOutlineWidth}px -${fontOutlineWidth}px ${fontOutlineColor},
${fontOutlineWidth}px -${fontOutlineWidth}px ${fontOutlineColor},
-${fontOutlineWidth}px ${fontOutlineWidth}px ${fontOutlineColor},
${fontOutlineWidth}px ${fontOutlineWidth}px ${fontOutlineColor}
`;
}
// Generate the HTML code and save it to the mapped HTMLLogo column
document.getElementById("generateHTML").addEventListener("click", function () {
const avatarHTML = document.getElementById("avatarPreview").outerHTML;
// Display the generated HTML in the textarea
document.getElementById("htmlOutput").value = avatarHTML;
// Save the HTML back to the mapped `HTMLLogo` cell (current row)
if (currentRecord.id) {
isWritingToCell = true; // Prevent feedback loop
grist.docApi
.updateCells({
updates: [
{
id: currentRecord.id, // Use the current record's ID
fields: {
HTMLLogo: avatarHTML // Write to the `HTMLLogo` column for the current row
}
}
]
})
.finally(() => {
isWritingToCell = false; // Allow updates again
});
} else {
console.error("No record ID available to write to.");
}
});