Detecting when cells are "ready" when embedding a notebook

Hi,

Suppose I am embedding some notebook cells inside an HTML document, which itself contains JavaScript code to interact with the cell values. For example, the embedded Observable cell could draw a rect, and the JavaScript code would listen to mouseover and mouseout events on this rect to update something else.

The problem is that if I load the JavaScript code at the load window event, the rect object may not be created yet, and my code will not work.

So my question is : when embedding cells, is there a way to run custom JavaScript code when we know that all the embedded cell values have been initially computed ?

I hope my question is clear enough, thanks in advance for any help !

A cell can wait on something by doing for example:

hello = {
  let a;
  while (!(a = document.querySelector("#hello"))) await Promises.delay(300);
  return a;
}

I suppose the javascript thingy can wait for the observable cell with the exact same code?

1 Like

Yes, that could be a workaround for most use cases I think. Thanks !

I just wonder : there is no event emitted when the embedded cells are in certain states that could be listened to by custom JavaScript code ? For example a “updating” event, a “ready” event or something like this ?

Couple of ideas here:

How are you embeding the Observable notebooks? If you’re embeding with observablehq’s iframes, you can’t do much, but if you’re using the “Runtime with JavaScript” option when embeding, then you’ll have a few more options, using Observable’s Runtime API. If you save the (new Runtime).module(...) call to a variable, then you can call .value("cellName"), which returns a Promise that resolves the value of the cell cellName after is is done computing, which is effectively the easiest way to “wait for” a cell to be fulfilled. Here’s an example, using the D3 Line chart notebook:

<div class="chart"></div>
<p>Credit: <a href="https://observablehq.com/@d3/line-chart">Line Chart by D3</a></p>

<script type="module">
import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
import define from "https://api.observablehq.com/@d3/line-chart.js?v=3";
const notebookModule = (new Runtime).module(define, name => {
  if (name === "chart") return Inspector.into(".chart")();
});

notebookModule.value("chart").then(chart => {
  // Then runs when the "chart"` cell has been fulfilled and is already mounted to the DOM
  console.log(chart);
})

</script>

See an example here: https://observablehq.com/d/f667c89002a018ad

(I made this snippet by going to the D3 Line chart notebook, selecting “Embed cell” on the chart cell, then chose “Runtime with JavaScript”, then copied the code and added the const notebookModule = line and the notebookModule.value("chart").then(...) lines.)

And for completeness, in the eyes of the Observable runtime, every cell is in 1 of 3 different states: pending, fulfilled, and rejected. All cells start off as pending (before execution), and cells either become fulfilled or rejected depending on if the cell’s code (and all the cell’s ancestors) succeeded or failed. If you really wanted to know when every single cell becomes “fulfilled”, you can take a look at the 2nd parameter in .module(), the observers parameter, which offers a way to “hook into” the state changes of every cell of a given notebook. By default, it uses the Observable notebook Inspector (as seen in the above example), but if you wanted to both 1) have a way to hook into the state changes of every cell in your notebook, and 2) still use the same functionality offered by the official Inspector, you could do something like this:

(new Runtime).module(define, name => {
  const i = new Inspector(document.body.appendChild(document.createElement("div")));
  return {
    pending() {
      console.log(name, "pending");
      i.pending();
    },
    fulfilled(value) {
      console.log(name, "fulfilled");
      i.fulfilled(value);
    },
    rejected(error) {
      console.log(name, "rejected");
      i.rejected(error);
    },
  };
})

It’s probably overkill for any of what you’re doing, but always an option!

9 Likes

Wow, thanks a lot for your very clear and detailed answer !

This is very helpful because I’m looking for a generic solution to work in the robservable R package, not just for one specific use case. I already thought about using the fulfilled Inspector state to maybe emit a custom event, but using the Runtime module as a variable is very interesting too.

Thanks again, I’ll try to implement something based on your suggestions.

Quick followup : I ended up emitting events by using Alexander’s code, and it seems to work well. Thanks again for your help !

2 Likes

Very cool! This is actually what I do for observable-prerender when I need to wait for a cell to be fulfilled (similar-ish to robservable), can’t wait to see the final result!

1 Like