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
Specs
t()
Link to this section Callbacks
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.
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.
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.
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.
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.
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
Specs
Makes a synchronous call to the widget server and waits for its reply.
See GenServer.call/3
for more details.
Specs
Sends an asynchronous request to the widget server.
See GenServer.cast/2
for more details.
Specs
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.