View Source Kino.JS.Live behaviour (Kino v0.5.2)

Introduces state and event-driven capabilities to JavaScript powered widgets.

Make sure to read the introduction to JavaScript widgets in Kino.JS for more context.

Similarly to static widgets, live widgets involve a custom JavaScript code running in the browser. In fact, this part of the API is the same. In addition, each live widget has a server process running on the Elixir side, responsible for maintaining state and able to communicate with the JavaScript side at any time. Again, to illustrate the ideas we start with a minimal example.

Example

We will follow up on our Kino.HTML example by adding support for replacing the content on demand.

defmodule Kino.LiveHTML do
  use Kino.JS
  use Kino.JS.Live

  def new(html) do
    Kino.JS.Live.new(__MODULE__, html)
  end

  def replace(widget, html) do
    Kino.JS.Live.cast(widget, {:replace, html})
  end

  @impl true
  def init(html, ctx) do
    {:ok, assign(ctx, html: html)}
  end

  @impl true
  def handle_connect(ctx) do
    {:ok, ctx.assigns.html, ctx}
  end

  @impl true
  def handle_cast({:replace, html}, ctx) do
    broadcast_event(ctx, "replace", html)
    {:noreply, assign(ctx, html: html)}
  end

  asset "main.js" do
    """
    export function init(ctx, html) {
      ctx.root.innerHTML = html;

      ctx.handleEvent("replace", (html) => {
        ctx.root.innerHTML = html;
      });
    }
    """
  end
end

Just as before we define a module, this time calling it Kino.LiveHTML for clarity. Note many similarities to the previous version, we still call use Kino.JS, define the main.js file and define the new(html) function for building the widget. As a matter of fact, the initial result of Kino.LiveHTML.new(html) will render exactly the same as our previous Kino.HTMl.new(html).

As for the new bits, we added use Kino.JS.Live to define a live widget server. We use Kino.JS.Live.new/2 for creating the widget instance and we implement a few GenServer-like callbacks.

Once the widget server is started with Kino.JS.Live.new/2, the init/2 callback is called with the initial argument. In this case we store the given html in server state.

Whenever the widget is rendered on a new client, the handle_connect/1 callback is called and it builds the initial data for the client. In this case, we always return the stored html. This initial data is then passed to the JavaScript init function. Keep in mind that while the server is initialized once, connect may happen at any point, as the users join/refresh the page.

Finally, the whole point of our example is the ability to replace the HTML content directly from the Elixir side and for this purpose we added the public replace(widget, html) function. Underneath the function uses cast/2 to message our server and the message is handled with handle_cast/2. In this case we store the new html in the server state and broadcast an event with the new value. On the client side, we subscribe to those events with ctx.handleEvent(event, callback) to update the page accordingly.

Event handlers

You must eventually register JavaScript handlers for all events that the client may receive. However, the registration can be deferred, if the initialization is asynchronous. For example, the following is perfectly fine:

export function init(ctx, data) {
  fetch(data.someUrl).then((resp) => {
    ctx.handleEvent("update", (payload) => {
      // ...
    });
  });
}

Or alternatively:

export async function init(ctx, data) {
  const response = await fetch(data.someUrl);

  ctx.handleEvent("update", (payload) => {
    // ...
  });
}

In such case all incoming events are buffered and dispatched once the handler is registered.

Link to this section Summary

Callbacks

Invoked to handle synchronous call/3 messages.

Invoked to handle asynchronous cast/2 messages.

Invoked whenever a new client connects to the server.

Invoked to handle client events.

Invoked to handle all other messages.

Invoked when the widget server started.

Invoked when the server is about to exit.

Functions

Makes a synchronous call to the widget server and waits for its reply.

Sends an asynchronous request to the widget server.

Instantiates a live JavaScript widget defined by module.

Link to this section Types

Link to this section Callbacks

Link to this callback

handle_call(msg, from, ctx)

View Source (optional)

Specs

handle_call(msg :: term(), from :: term(), ctx :: Kino.JS.Live.Context.t()) ::
  {:noreply, ctx :: Kino.JS.Live.Context.t()}
  | {:reply, term(), ctx :: Kino.JS.Live.Context.t()}

Invoked to handle synchronous call/3 messages.

See GenServer.handle_call/3 for more details.

Link to this callback

handle_cast(msg, ctx)

View Source (optional)

Specs

handle_cast(msg :: term(), ctx :: Kino.JS.Live.Context.t()) ::
  {:noreply, ctx :: Kino.JS.Live.Context.t()}

Invoked to handle asynchronous cast/2 messages.

See GenServer.handle_cast/2 for more details.

Specs

handle_connect(ctx :: Kino.JS.Live.Context.t()) ::
  {:ok, data :: term(), ctx :: Kino.JS.Live.Context.t()}

Invoked whenever a new client connects to the server.

The returned data is passed to the JavaScript init function of the connecting client.

Link to this callback

handle_event(event, payload, ctx)

View Source (optional)

Specs

handle_event(
  event :: String.t(),
  payload :: term(),
  ctx :: Kino.JS.Live.Context.t()
) :: {:noreply, ctx :: Kino.JS.Live.Context.t()}

Invoked to handle client events.

Link to this callback

handle_info(msg, ctx)

View Source (optional)

Specs

handle_info(msg :: term(), ctx :: Kino.JS.Live.Context.t()) ::
  {:noreply, ctx :: Kino.JS.Live.Context.t()}

Invoked to handle all other messages.

See GenServer.handle_info/2 for more details.

Link to this callback

init(arg, ctx)

View Source (optional)

Specs

init(arg :: term(), ctx :: Kino.JS.Live.Context.t()) ::
  {:ok, ctx :: Kino.JS.Live.Context.t()}

Invoked when the widget server started.

See GenServer.init/1 for more details.

Link to this callback

terminate(reason, ctx)

View Source (optional)

Specs

terminate(reason, ctx :: Kino.JS.Live.Context.t()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term()

Invoked when the server is about to exit.

See GenServer.terminate/2 for more details.

Link to this section Functions

Link to this function

call(widget, term, timeout \\ 5000)

View Source

Specs

call(t(), term(), timeout()) :: term()

Makes a synchronous call to the widget server and waits for its reply.

See GenServer.call/3 for more details.

Specs

cast(t(), term()) :: :ok

Sends an asynchronous request to the widget server.

See GenServer.cast/2 for more details.

Specs

new(module(), term()) :: t()

Instantiates a live JavaScript widget defined by module.

The given init_arg is passed to the init/2 callback when the underlying widget process is started.