Skip to content

Commit

Permalink
editor: Fixing bug that caused infinite loop during preview
Browse files Browse the repository at this point in the history
  • Loading branch information
berhalak committed Nov 20, 2024
1 parent be9a2ba commit 453bf84
Showing 1 changed file with 66 additions and 41 deletions.
107 changes: 66 additions & 41 deletions custom-widget-builder/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,42 +180,14 @@ function purge(element) {
}
}

let lastListener;
let widgetWindow = null;
function createFrame() {
// remove all data from page_widget
purge(page_widget);
wFrame = document.createElement('iframe');
page_widget.appendChild(wFrame);
const widgetWindow = wFrame.contentWindow;
// Rewire messages between this widget, and the preview
if (lastListener) window.removeEventListener('message', lastListener);
lastListener = e => {
if (e.source === widgetWindow) {
// Hijicack configure message to inform Grist that we have custom configuration.
// data will have { iface: "CustomSectionAPI", meth: "configure", args: [{}] }
if (
e.data?.iface === 'CustomSectionAPI' &&
e.data?.meth === 'configure'
) {
e.data.args ??= [{}];
e.data.args[0].hasCustomOptions = true;
}
window.parent.postMessage(e.data, '*');
} else if (e.source === window.parent) {
// If user clicks `Open confirguration` button, we will switch to the editor.
// The message that we will receive is:
// {"mtype":1,"reqId":6,"iface":"editOptions","meth":"invoke","args":[]}
if (e.data?.iface === 'editOptions' && e.data?.meth === 'invoke') {
if (state() !== 'editor') {
showEditor();
}
} else {
widgetWindow.postMessage(e.data, '*');
}
}
};
window.addEventListener('message', lastListener);
}
widgetWindow = wFrame.contentWindow;
}

function init() {
if (init.invoked) return;
Expand Down Expand Up @@ -332,7 +304,10 @@ const onOptions = function (clb) {
return () => void (listen = false);
};

let lastOptions = null;

onOptions(async options => {
lastOptions = options;
if (!options) {
if (state() === 'installed') {
// If we are already installed, check that we don't have any code shown.
Expand Down Expand Up @@ -369,16 +344,21 @@ onOptions(async options => {

function btnInstall_onClick() {
const options = {
_installed: true,
_js: jsModel.getValue(),
_html: htmlModel.getValue(),
};
grist.setOptions(options);


state('installed');

// Copy file contents into memory.
currentJs(options?._js);
currentHtml(options?._html);
// Compare options with lastOptions, and if they are the same, don't call setOptions.
if (JSON.stringify(options) !== JSON.stringify(lastOptions)) {
grist.setOptions(options);

// Copy file contents into memory.
currentJs(options?._js);
currentHtml(options?._html);
}

// Hide editor.
page_editor.style.display = 'none';
Expand All @@ -395,10 +375,55 @@ function bntReset_onClick() {
jsModel.setValue(DEFAULT_JS);
}

grist.ready({
onEditOptions: () => {
if (state() !== 'editor') {
showEditor();
// We are here very careful to not call configure endpoint, as it will reload the editor
// in case the configuration is different (between editor and the edited widget).
// If the edited widget has column mapping (with mandatory columns), Grist will hide us briefly
// to check the mappings and give user a chance to configure them. But during this period our
// builder will be removed from dom (iframe will be hidden). When columns will be mapped, widget
// will be recreated, but then if we report back to Grist that we don't need any mapping (as we do
// since we don't know yet what the edited widget will require or did send previously) Grist will
// once again hide us to check for mappings. It will then realize that no mappings are needed and
// render us back again. But our edited widget will once more send the mapping, and then we will
// fall into loop.
// So here we call low level API to report that we are ready, without configuring anything.
// This won't tell Grist that we are support `Open configuration`, but since by default we are
// showing a widget, it will call it's ready message we will intercept it in the listener, and
// append this configuration ourselves. This way, Grist will receive consistent configuration
// about mapped columns.
// Sorry for the long comment, but this is a tricky part.
grist.rpc.sendReadyMessage();

// We need to register something here. It doesn't matter what, we intercept the message from Grist
// below in the listener.
grist.rpc.registerFunc('editOptions', () => {});

window.addEventListener('message', e => {
if (e.source === widgetWindow) {
// Hijack configure message to inform Grist that we have custom configuration.
// data will have { iface: "CustomSectionAPI", meth: "configure", args: [{}] }
if (
e.data?.iface === 'CustomSectionAPI' &&
e.data?.meth === 'configure'
) {
e.data.args ??= [{}];
e.data.args[0].hasCustomOptions = true;
// Here is the trick part. Inner widget is calling ready method and configure method (as
// part of grist.ready() call). Since we don't call configure method at all, we just append
// the `hasCustomOptions` to the configure method, and send it back to Grist. This way Grist
// will receive same configuration of mapping columns each time, which will avoid the infinite
// loop problem described above.
}
},
window.parent.postMessage(e.data, '*');
} else if (e.source === window.parent) {
// If user clicks `Open confirguration` button, we will switch to the editor.
// The message that we will receive is:
// {"mtype":1,"reqId":6,"iface":"editOptions","meth":"invoke","args":[]}
if (e.data?.iface === 'editOptions' && e.data?.meth === 'invoke') {
if (state() !== 'editor') {
showEditor();
}
} else {
widgetWindow.postMessage(e.data, '*');
}
}
});

0 comments on commit 453bf84

Please sign in to comment.