# Custom Kinos with Elixir and JavaScript ```elixir Mix.install([ {:kino, "~> 0.18.0"} ]) ``` ## Introduction Livebook allows developers to implement their own kinos. This allows developers to bring their own ideas to life and extend Livebook in unexpected ways. There are two types of custom kinos: static, via `Kino.JS`, and dynamic, via `Kino.JS.Live`. We will learn to implement both in this notebook. ## HTML rendering with Kino.JS The "hello world" of custom kinos is one that embeds and renders a given HTML string directly on the page. We can implement it using [`Kino.JS`](https://hexdocs.pm/kino/Kino.JS.html) in less than 15 LOC: ```elixir defmodule KinoGuide.HTML do use Kino.JS def new(html) when is_binary(html) do Kino.JS.new(__MODULE__, html) end asset "main.js" do """ export function init(ctx, html) { ctx.root.innerHTML = html; } """ end end ``` Let's break it down. To define a custom kino we need to create a new module. In this case we go with `KinoGuide.HTML`. We start by adding `use Kino.JS`, which makes our module asset-aware. In particular, it allows us to use the `asset/2` macro to define arbitrary files directly in the module source. All custom kinos require a `main.js` file that defines a JavaScript module and becomes the entrypoint on the client side. The JavaScript module is expected to export the `init(ctx, data)` function, where `ctx` is a special object and `data` is the data passed from the Elixir side. In our example the `init` function accesses the root element with `ctx.root` and overrides its content with the given HTML string. Finally, we define the `new(html)` function that builds our kino with the given HTML. Underneath we call `Kino.JS.new/2` specifying our module and the data available in the JavaScript `init` function later. Again, it's a convention for each kino module to define a `new` function to provide uniform experience for the end user. Let's give our Kino a try: ```elixir KinoGuide.HTML.new("""
I wrote this HTML from Kino!
""") ``` It works! To learn more about other features provided by `Kino.JS`, including persisting the output of your custom kinos to `.livemd` files, [check out the documentation](https://hexdocs.pm/kino/Kino.JS.html). ## Bidirectional live counter Kinos with static data are useful, but they offer just a small peek into what can be achieved with custom kinos. This time we will try out something more exciting. Let's use [`Kino.JS.Live`](https://hexdocs.pm/kino/Kino.JS.Live.html) to build a counter that can be incremented both through Elixir calls and client interactions. Not only that, our counter will automatically synchronize across pages as multiple users access our notebook. Our custom kino must use both `Kino.JS` (for the assets) and `Kino.JS.Live`. `Kino.JS.Live` works under the client-server paradigm, where the client is the JavaScript code, and the server is your Elixir code. Your Elixir code has to define a series of callbacks (similar to a [`GenServer`](https://hexdocs.pm/elixir/GenServer.html) and other Elixir behaviours). In particular, we need to define: * A `init/2` callback, that receives the argument and the "server" `ctx` (in contrast to the `ctx` in JavaScript, which is the client context) * A `handle_connect/1` callback, which is invoked whenever a new client connects. In here you must return the initial state of the new client * A `handle_event/3` callback, responsible for handling any messages sent by the client * A `handle_cast/2` callback, responsible for handling any Elixir messages sent via `Kino.JS.Live.cast/2` Here is how the code will look like: ```elixir defmodule KinoGuide.Counter do use Kino.JS use Kino.JS.Live def new(count) do Kino.JS.Live.new(__MODULE__, count) end def bump(kino) do Kino.JS.Live.cast(kino, :bump) end @impl true def init(count, ctx) do {:ok, assign(ctx, count: count)} end @impl true def handle_connect(ctx) do {:ok, ctx.assigns.count, ctx} end @impl true def handle_cast(:bump, ctx) do {:noreply, bump_count(ctx)} end @impl true def handle_event("bump", _, ctx) do {:noreply, bump_count(ctx)} end defp bump_count(ctx) do ctx = update(ctx, :count, &(&1 + 1)) broadcast_event(ctx, "update", ctx.assigns.count) ctx end asset "main.js" do """ export function init(ctx, count) { ctx.root.innerHTML = ` `; const countEl = document.getElementById("count"); const bumpEl = document.getElementById("bump"); countEl.innerHTML = count; ctx.handleEvent("update", (count) => { countEl.innerHTML = count; }); bumpEl.addEventListener("click", (event) => { ctx.pushEvent("bump"); }); } """ end end ``` On the client side, we wrote some JavaScript that listens to button clicks and dispatches them to the server via `pushEvent`. Let's render our counter! ```elixir counter = KinoGuide.Counter.new(0) ``` As an experiment you can open another browser tab to verify that the counter is synchronized. In addition to client events we can also use the Elixir API we defined for our counter. ```elixir KinoGuide.Counter.bump(counter) ``` In the next notebook we will get back to those concepts and [extend Livebook with custom Smart cells](/learn/notebooks/smart-cells)!