# Custom Kinos with Elixir and JavaScript

## Setup

Starting from version v0.5, 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.

First, let's install `kino`:

```elixir
Mix.install([
  {:kino, "~> 0.5.0"}
])
```

## 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 Kino.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,
conventionally under the `Kino.` prefix, so that the end
user can easily autocomplete all available kinos. In
this case we went with `Kino.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
Kino.HTML.new("""
<h3>Look!</h3>

<p>I wrote this HTML from <strong>Kino</strong>!</p>
""")
```

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).

## Dynamic maps with Leaflet

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. We will set up a simple map and then push points
directly from the Elixir code with [`Kino.JS.Live`](https://hexdocs.pm/kino/Kino.JS.Live.html).

There is a number of different JavaScript packages to pick from
when dealing with maps, for our purpose we will use [Leaflet](https://leafletjs.com),
which is an established solution in this area.

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

You can also optionally define a `handle_cast/2` callback, responsible
for handling any messages sent via `Kino.JS.Live.cast/2`. Here is how
the code will look like:

```elixir
defmodule Kino.Leaflet do
  use Kino.JS
  use Kino.JS.Live

  def new(center, zoom) do
    Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
  end

  def add_marker(kino, location) do
    Kino.JS.Live.cast(kino, {:add_marker, normalize_location(location)})
  end

  @impl true
  def init({center, zoom}, ctx) do
    {:ok, assign(ctx, center: center, zoom: zoom, locations: [])}
  end

  @impl true
  def handle_connect(ctx) do
    data = %{
      center: ctx.assigns.center,
      zoom: ctx.assigns.zoom,
      locations: ctx.assigns.locations
    }

    {:ok, data, ctx}
  end

  @impl true
  def handle_cast({:add_marker, location}, ctx) do
    broadcast_event(ctx, "add_marker", location)
    ctx = update(ctx, :locations, &[location | &1])
    {:noreply, ctx}
  end

  defp normalize_location({lag, lng}), do: [lag, lng]

  asset "main.js" do
    """
    import * as L from "https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.esm.js";

    export async function init(ctx, data) {
      ctx.root.style.height = "400px";

      // Leaflet requires styles to be present before creating the map,
      // so we await for the import to finish
      await ctx.importCSS("https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.css");

      const { center, zoom, locations } = data;
      const map = L.map(ctx.root, { center, zoom });

      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
      }).addTo(map);

      locations.forEach((location) => {
        L.marker(location).addTo(map);
      });

      ctx.handleEvent("add_marker", (location) => {
        L.marker(location).addTo(map);
      });
    }
    """
  end
end
```

This is a bit more code, however the flow is very straightforward.
The map is initialized with the central location and zoom, we store
those in the server state and pass to each client when they connect.

Additionally we keep a list of locations that we want to mark on the
map. The public `add_marker` function allows for pushing new locations
to the server, in which case we send the it to the client. On the
client we render all initial markers we get and subscribe to any new
that appear later on.

Note that we keep track of all locations on the server, this way
whenever a new user joins the page, we can send them all of the
locations we already have. To verify this behaviour you can refresh
the page and you should see all of the markers still in place. Feel
free to try this out in separte browser tabs too!

```elixir
map = Kino.Leaflet.new({51.505, -0.09}, 13)
```

The below cell marks a random location, so you can evaluate it
multiple times for better results.

```elixir
delta = fn -> (:rand.uniform() - 0.5) * 0.05 end

Kino.Leaflet.add_marker(map, {51.505 + delta.(), -0.09 + delta.()})
```

We barely scratched the surface of maps, the Leaflet API alone is extremely
extensive and there are other packages worth exploring. However, even with
this simple kino we could already visualize some geographic data in real-time!

## Bidirectional live counter

The map example reiterated how we can send events from the server
to the clients, however communication in the other direction is
possible as well!

Let's build a counter that can be incremented both through Elixir
calls and client interactions.

```elixir
defmodule Kino.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 = `
        <div id="count"></div>
        <button id="bump">Bump</button>
      `;

      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
```

The server mechanics are quite similar to the Leaflet example.
The only difference is that we have a new callback, `handle_event/3`,
for handling events sent from the client. On the client side,
this is done by listening to button clicks and dispatching them
to the server via `pushEvent`.

Let's render our counter!

```elixir
counter = Kino.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
Kino.Counter.bump(counter)
```

## Final words

Congratulations, you finished our "course" on Kino! Throughout
those guides, you mastered Kino's API and learned how to use its
building blocks to build a chat app and a multiplayer pong game.

Then, in this notebook, you learned how you can take Kino anywhere
you want by implementing your own kinos with Elixir and JavaScript.
We are looking forward to see what you can build with it! 🚀
