Possible to call functions defined in Scriptable in WebView?

I’m interested in using WebView to create more complex UIs than what’s possible with UITable. One basic example that would be really nice in UITable is the ability to control font size. And then of course more powerful features like animations and more control over styling.

I’ve been playing around with WebView and it seems like you can only execute JS as a string, which means you can’t access any other functions defined in Scriptable. So for example, it would be cool if this would work:

const getReminders = async () => Reminder.allIncomplete();

const wv = new WebView();
await wv.loadHTML(`
<meta name="viewport" content="width=device-width",initial-scale=1" />
<style>
  body { font-family: -apple-system; }
</style>
<a href="#" id="cta">Click me!</a>
<div id="results"></div>
`);
await wv.evaluateJavaScript(`
  document.getElementById("cta").setAttribute("onClick", async () => {
    const reminders = await getReminders();
    document.getElementById("results").innerHTML = reminders
      .map(({title}) => "<p>" + title + "<p>")
      .join("<br />");
  });
`);
await wv.present();

I have here something, which is not quite the same what you asked, but it is the only workaround I know of.

There is a way to send data to the Scriptable script from the WebView:

Add a function to WebView.shouldAllowRequest that blocks all requests with a URL that starts with scriptable:// for example or something else. Now you can just use window.location.href from the WebView to “navigate” to a URL with your defined keyword at the start. As this request is blocked by Scriptable, the WebView stays on the current page, but the script in Scriptable got the data.

Together with the WebView.evaluateJavaScript function you now have a two way communication set up.

Of course you can make a wrapper around this to have a simpler API.

Edit: You need to use window.location.href, because Scriptable can’t intercept XMLHttpRequest. At least in my tests it haven’t caught these.

1 Like

Very delayed thanks for the tip – I finally got around to trying this out and built a very simple API which I’ll try building on.

const RECEIVED_DATA_PREFIX = 'data://';

const present2WayWebview = async (initHTML, handleReceivedData) => {
  const w = new WebView();
  w.shouldAllowRequest = request => {
    const isPassingData = request.url.startsWith(RECEIVED_DATA_PREFIX);
    if (isPassingData) {
      const receivedData = request.url.split(RECEIVED_DATA_PREFIX)[1];
      handleReceivedData(w, receivedData);
      return false;
    }
    return true;
  };
  w.loadHTML(initHTML);
  await w.present();
};

await present2WayWebview(
  '<a href="${RECEIVED_DATA_PREFIX}selectedTab=A">Click me!</a>',
  (webview, data) => webview.loadHTML(data)
);