Inject OJS variable from vanilla JavaScript

Make something you load with vanilla JavaScript be accessible as an OJS variable

Author

James Goldie

Most JavaScript libraries work just fine in OJS. But for a current project I want to use a framework that needs to be loaded in vanilla JavaScript (because we want to make a Quarto extension that uses it, and we can’t inject OJS code into a Quarto doc without considerable work).

The library has a constructor function, and the object it sends back can be configured to have callbacks that respond to browser events. I want to rig those callbacks so that they update an OJS variable—that way, users of our extension can access its state and use it to drive visualisations.

So the question is, can we declare and update an OJS variable from vanilla JavaScript?

I can see that window._ojs exists, and that window._ojs.ojsConnector has a mainModule that appears to (further down) contain variables being declared in my OJS blocks. For example, here’s an OJS variable, ‘myMessage’:

myMessage = 23

if I run this vanilla JS in the browser devtools:

const ojsVars = window._ojs.ojsConnector.mainModule._scope.entries()
const var1 = ojsVars.next().value

var1[0]
// "myMessage" 

var1[1]._value
// 23

Great! There may be a problem checking that the runtime and module have loaded, but we can at least see it.

The OJS runtime docs describe the process of attaching a variable to a module (which is a namespace for variables), and a module to a runtime (an instance of OJS, I think) as something like:

const runtime = new Runtime(builtins);
const module = runtime.module();
const a = module.variable();
// define as a constant
a.define("foo", 42);

// define another variable that takes an argument
const b = module.variable();
b.define(["foo"], foo => foo * 2);

Okay, let’s try to register a new value using the Quarto OJS connector module:

<script>

  // wait 'til dom content is loaded before trying to touch the ojs module
  addEventListener("DOMContentLoaded", (event) => {
    console.log("DOM content loaded, checking for OJS module")

    // check for the ojs connector first
    const ojsModule = window._ojs?.ojsConnector?.mainModule
    if (ojsModule === undefined) {
      console.error("Quarto OJS module not found")
    } else {
      console.log("Quarto OJS module found!")
    }
  
    // register the var
    const myVar = ojsModule.variable();
    myVar.define("myVar", () => {
      async function* myGen() {
        yield 1;
        while (true) {
          yield Promises.delay(1000, Math.random());
        }
      }
    });

    // let's try a different strategy:
    // calling variable.define() on the timer instead
    const anotherVar = ojsModule.variable();
    anotherVar.define("anotherVar", 23);

    setInterval(() => {
      anotherVar.define("anotherVar", Math.random());
    }, 1000);

  });

</script>

What’s the value of myVar? It’s ! And anotherVar is .

console.log("myVar is " + myVar)
console.log("anotherVar is " + anotherVar)

Okay, so I’m able to call variable.define() on a timer and just keep updating it to a new constant (that’s what anotherVar is doing), but I’m having trouble defining the variable once to use a generator (which could potentially improve performance, but I’m not sure), as in myVar.

I think the anotherVar strategy should be enough for me: I’ll call variable.define inside the callback for our framework and we’ll be cookin’ with gas 🥳