4

I'm converting a script of mine to be an add on. One of the needs is to configure a template, so I have programmed a sidebar that launchs a field picker. As the sidebar does not have enough room for the picker, I have to launch it from a modal dialog that I create from the sidebar, by calling this code in the server side:

var html = HtmlService.createHtmlOutputFromFile('TemplatePicker.html')
  .setWidth(600).setHeight(425);
SpreadsheetApp.getUi().showModalDialog(html, 'Select the file with the template');

My problem is that once the user picks the file, when I have the id of the chosen file, I'm not able to pass that id to the sidebar. I tried invoking someJSFunctionOfSidebar(id) and parent.someJSFunctionOfSidebar(id), but it didn't work, so I finally ended passing the value to the server side and reloading the sidebar from there, but it's very slow and the resulting effect is ugly.

My question is:

Is there a way to pass a value at client level from a modal dialog created with SpreadsheetApp.getUi().showModalDialog to its parent? Perhaps it's not really its parent and that's the reason for it not working.

TheMaster
  • 45,448
  • 6
  • 62
  • 85
fdediego
  • 115
  • 1
  • 6
  • I don't believe there is a way to do this. I believe the sidebar and the modal behave like two completely separate apps, or more like two separate sessions. Even the mailchimp mail merge addon has to reload the sidebar after the file picker has been chosen. – rGil Apr 27 '14 at 18:26
  • If you run both elements by current user than you may use the [Properties Service](https://developers.google.com/apps-script/reference/properties/) – contributorpw Aug 08 '14 at 14:31
  • @AlexanderIvanov I know, and in fact I am doing that to share data between them, but that does not solve the part about communicating the UI without reloading the sidebar. – fdediego Aug 10 '14 at 08:05

2 Answers2

9

Perhaps it's not really its parent and that's the reason for it not working.

Right - there isn't actually a DOM parent / child relationship in play here. Both the sidebar and the modal dialog were launched from server-side scripts, and are independent. They can both communicate with the server, though, so you can use a store-and-forward technique to get the result from your picker to the sidebar.

Basic idea:

  • The sidebar will start polling the server for the picker's result as soon as it requests the picker's launch.
  • The picker's result will be sent to the server, using google.script.run.
  • The server will store the result temporarily - this could be as simple as a global variable, depending on your situation.
  • Once there is a result, the next poll will retrieve it.

Have a look at How to poll a Google Doc from an add-on for the basic idea of a poller.

Message Sequence Chart

Community
  • 1
  • 1
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
  • Wow, this one could well become a hit :-) so let me be the first to vote it up! – Serge insas Aug 14 '14 at 16:48
  • Yes, given the restrictions, I think this is the path to follow, thank you! – fdediego Aug 19 '14 at 06:04
  • @Mogsdad, I didn't even try, as the simplest way (reload the sidebar an communicate through user properties) was enough for my client. – fdediego Sep 15 '14 at 09:44
  • *Perhaps it's not really its parent and that's the reason for it not working.* True, but both of them have the same parent(they're siblings) and it is [possible](https://stackoverflow.com/a/58186493/) to communicate directly through the parent window (`window.top`). – TheMaster Oct 11 '19 at 15:31
  • 1
    @themaster It's possible _now_, not in 2014 - a 2019 answer would be welcome by current users. – Mogsdad Oct 11 '19 at 22:24
7

Issue:

Sidebar and modal dialog(siblings) are not able to communicate despite having same origin.

Solution:

It is possible to get a reference to the sidebar html from modal dialog through the ancestor parent window.top, even though the parent is cross origin. From there, it is possible to

  • directly communicate with each other
  • use window.postMessage() to communicate with each other

Without a reference to each other, it is still possible to communicate with each other through

  • the server and script properties service. However, Here, one of them needs to poll the server at set intervals to get any updates from the other(as illustrated here).
  • use cookies/localstorage to communicate with each other

To Read:

Sample script(using direct access through cross origin frame window.top):

addOn.html[Sidebar]

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Addon</title>
    <style>
      #spinner {
        display: none;
        background-color: tomato;
        position: absolute;
        top: 1%;
        width: 100%;
        justify-items: center;
      }
    </style>
  </head>
  <body>
    <div id="spinner"><p>Loading modal dialog...</p></div>
    <div id="output"></div>
    <script charset="utf-8">
      google.script.run.withSuccessHandler(spinner).testModal();
      function spinner(e) {
        document.getElementById('spinner').style.display = e || 'flex';
      }
      (async () => {
        //After modal dialog has finished, receiver will be resolved
        let receiver = new Promise((res, rej) => {
          window.modalDone = res;
        });
        var message = await receiver;
        document.querySelector('#output').innerHTML = message;
        //Do what you want here
      })();
    </script>
  </body>
</html>

modalAddOn.html[Modal dialog/picker]

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>
  <body>
    Modal Dialog
    <script>
      (function findSideBar(limit) {
        let f = window.top.frames;
        for (let i = 0; i < limit; ++i) {
          try {
            if (
              f[i] /*/iframedAppPanel*/ &&
              f[i].length &&
              f[i][0] && //#sandboxFrame
              f[i][0][0] && //#userHtmlFrame
              window !== f[i][0][0] //!== self
            ) {
              console.info('Sidebar found ');
              alert('Removing loadbar and closing self');
              var sidebar = f[i][0][0];
              sidebar.spinner('none'); //Remove sidebar spinner
              sidebar.modalDone('Modal says Hi'); //Modal has finished
              google.script.host.close();
            }
          } catch (e) {
            console.error(e);
            continue;
          }
        }
      })(10);
    </script>
  </body>
</html>

code.gs[Server]

function testModal() {
  SpreadsheetApp.getUi().showModelessDialog(
    HtmlService.createHtmlOutputFromFile('modalAddOn')
      .setHeight(500)
      .setWidth(300),
    ' '
  );
}

function onOpen(e) {
  SpreadsheetApp.getUi()
    .createMenu('Sidebar')
    .addItem('Show Add-On', 'showSidebar')
    .addToUi();
}

function showSidebar() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createTemplateFromFile('addOn.html').evaluate()
  );
}

Related questions:

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Thank you. Just a question: What does this line do: google.script.run.withSuccessHandler(spinner)? – 0xh8h Oct 02 '19 at 03:06
  • 1
    @Hoang I wanted to show spinner after testModal is called. testModal is called>server script finishes> Spinner function is called >Hidden spinner is shown. – TheMaster Oct 02 '19 at 05:36
  • One more question, instead of window.modalDone = res; I can use res(window.modelDone), right? – 0xh8h Dec 04 '19 at 08:37
  • 1
    @Hoang I don't think so. `res` is a function, which when called, `res`olves the pending promise(`receiver`). `window.modalDone = res;` is the same as `var modalDone = res;` except the variable `modalDone` is declared in the "global scope": it's the same as `modalDone=res;`. Why am I declaring a new variable in the global scope with a value of `res`, a function? So that it may be called from the modal dialog directly: `sidebar.modalDone('Modal says Hi')`... the same as `res('Modal says Hi')`- the promise `receiver` is resolved with `Modal says Hi`. Hope it helps. – TheMaster Dec 04 '19 at 10:07
  • How does the `f[i] /*/iframedAppPanel*/` part evaluate to a true/false? To a JS noob `/*/iframedAppPanel*/` doesn't seem to be valid JavaScript. – ScrapeHeap Nov 08 '20 at 16:57
  • 1
    @ScrapeHeap Anything between `/*` and `*/` is considered a comment and not relevant. Also note the syntax color/ highlight and see how the entire comment portion is in light color here and in gas editor as well. `f[i]` is needed because if there are only 2 frames in widow and `i` is 3, then `f[3]` should evaluate to `undefined` => `undefined` is `falsy`. See mdn for truthy and falsy – TheMaster Nov 08 '20 at 18:40
  • Wow, I had no idea you can use multiline comments within a line of code... so `console/*/////////*/.log('b')` is still valid JS – ScrapeHeap Nov 08 '20 at 20:22
  • Would you consider your direct approach better than window.postMessage()? Any reason you would prefer one to the other? – ScrapeHeap Nov 08 '20 at 20:28
  • @ScrapeHeap For one thing, You have direct access to the other window object. You can do stuff directly rather than using a receiver => get a string message => act on it. Direct method is pretty flexible without restrictions. – TheMaster Nov 09 '20 at 04:07
  • This is an amazing solution! Is there a way to reinitialise the async function so that a user could conceivably press have a button that each time they pressed it, it would open the modal dialogue? Currently, this solution only works once and once the async function finishes it is no longer listening. – BJG87 Mar 16 '21 at 23:34
  • 1
    @BJG87 Should be possible. Just name the asyc function and call the function again from inside the function at `//Do what you want here` – TheMaster Mar 18 '21 at 17:22
  • I can confirm this works. I tried that before but I am new to async function and hadn't come across arrow syntax before so I wasn't naming the function correctly. Once I removed the arrow syntax and declared the function in the traditional function () {} it works great! – BJG87 Mar 18 '21 at 22:07