Use browser APIs from Elixir

View Source

Popcorn.Wasm.run_js/3 runs a JavaScript function in the browser page. The calling BEAM process waits for the result.

Return a value

{:ok, language} =
  Popcorn.Wasm.run_js(
    """
    () => navigator.language
    """,
    %{}
  )

The function receives the argument map as its first argument. Popcorn converts the returned value to a BEAM term.

Use run_js!/3 when a JavaScript error must raise an Elixir exception.

Wait for JavaScript boot

Browser initialization that starts with the application should run in an ordinary supervised Task:

children = [
  {Task, &MyApp.API.run/0}
]

defmodule MyApp.API do
  def run do
    with :ok <- Popcorn.Wasm.await_ready(),
         {:ok, _value} <- Popcorn.Wasm.run_js("() => initializeBrowser()") do
      :ok
    end
  end
end

To avoid racing with remaining JavaScript initialization (after supervision tree started), we use Popcorn.Wasm.await_ready/1.

Use arguments

Pass data separately from the function source:

Popcorn.Wasm.run_js!(
  """
  ({id, text}) => {
    const statusNode = document.querySelector(id);
    statusNode.textContent = text;
  }
  """,
  %{id: "#status", text: "Ready"}
)

Do not build JavaScript source with string interpolation. Separate arguments avoid quoting errors and code injection.

Call back into BEAM

The second function argument contains bridge actions:

Popcorn.Wasm.run_js!(
  """
  ({target}, {send}) => {
    const refreshNode = document.querySelector("#refresh");

    refreshNode.addEventListener("click", () => {
      void send(target, {event: "refresh"});
    });
  }
  """,
  %{target: self()}
)

The BEAM process receives {:wasm, %{"event" => "refresh"}}.

The action object also provides call and cast. Those actions require a running Popcorn.Proxy.

Keep a JavaScript object

Return a tracked value for a DOM node or another object:

element =
  Popcorn.Wasm.run_js!(
    """
    () => {
      const chartNode = document.querySelector("#chart");

      return new TrackedValue(chartNode);
    }
    """,
    %{}
  )

Popcorn.Wasm.run_js!(
  "({element}) => element.replaceChildren()",
  %{element: element}
)

Add an idempotent cleanup function when the object owns a listener, timer, or other resource.

Avoid deadlocks

run_js/3 blocks only the calling BEAM process. Other processes continue to run.

Do not let the JavaScript function call the same GenServer that waits for run_js/3. Neither side can complete in that cycle.

The current bridge evaluates JavaScript source. The page Content Security Policy (CSP) must permit unsafe-eval.