mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-09 13:16:08 +08:00
Further improvements to pong and custom kino notebooks (#886)
This commit is contained in:
parent
000ac88b06
commit
b9b901648c
4 changed files with 853 additions and 399 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
In this guide, we will build a chat application using
|
In this notebook, we will build a chat application using
|
||||||
[`kino`](https://github.com/livebook-dev/kino). Let's
|
[`kino`](https://github.com/livebook-dev/kino). Let's
|
||||||
install it and get started:
|
install it and get started:
|
||||||
|
|
||||||
|
|
@ -149,5 +149,5 @@ fully operational. Open up this same notebook across
|
||||||
on different tabs and each different user can post
|
on different tabs and each different user can post
|
||||||
their messages.
|
their messages.
|
||||||
|
|
||||||
In the next guide we will go one step further and
|
In the next notebook we will go one step further and
|
||||||
[develop a multiplayer pong game](/explore/notebooks/pong)!
|
[develop a multiplayer pong game](/explore/notebooks/pong)!
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
# Custom kinos with JavaScript
|
# Custom Kinos with Elixir and JavaScript
|
||||||
|
|
||||||
## Introduction
|
## Setup
|
||||||
|
|
||||||
The `Kino.JS` and `Kino.JS.Live` docs outline the API that enables
|
Starting from version v0.5, Livebook allows developers to implement
|
||||||
developing custom JavaScript powered kinos. The examples discussed
|
their own kinos. This allows developers to bring their own ideas to
|
||||||
there are kept minimal to introduce the basic concepts without much
|
life and extend Livebook in unexpected ways.
|
||||||
overhead. In this notebook we take things a bit further and showcase
|
|
||||||
a couple more elaborate use cases.
|
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
|
```elixir
|
||||||
Mix.install([
|
Mix.install([
|
||||||
|
|
@ -14,75 +17,103 @@ Mix.install([
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
## HTML rendering
|
## HTML rendering with Kino.JS
|
||||||
|
|
||||||
As a quick recap let's define a widget for rendering diagrams
|
The "hello world" of custom kinos is one that embeds and
|
||||||
from text specification using [Mermaid](https://mermaid-js.github.io/mermaid/#/).
|
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
|
```elixir
|
||||||
defmodule Kino.Mermaid do
|
defmodule Kino.HTML do
|
||||||
use Kino.JS
|
use Kino.JS
|
||||||
|
|
||||||
def new(graph) do
|
def new(html) when is_binary(html) do
|
||||||
Kino.JS.new(__MODULE__, graph)
|
Kino.JS.new(__MODULE__, html)
|
||||||
end
|
end
|
||||||
|
|
||||||
asset "main.js" do
|
asset "main.js" do
|
||||||
"""
|
"""
|
||||||
import "https://cdn.jsdelivr.net/npm/mermaid@8.13.3/dist/mermaid.min.js";
|
export function init(ctx, html) {
|
||||||
|
ctx.root.innerHTML = html;
|
||||||
mermaid.initialize({ startOnLoad: false });
|
|
||||||
|
|
||||||
export function init(ctx, graph) {
|
|
||||||
mermaid.render("graph1", graph, (svgSource, bindListeners) => {
|
|
||||||
ctx.root.innerHTML = svgSource;
|
|
||||||
bindListeners && bindListeners(ctx.root);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
In this case we pass the graph specification to Mermaid, which
|
Let's break it down.
|
||||||
generates an SVG image for us and we embed it directly into the
|
|
||||||
page. Note how we import the package directly from a CDN. Using
|
|
||||||
this approach we can quickly create widgets without setting up
|
|
||||||
a whole JavaScript bundling system.
|
|
||||||
|
|
||||||
Let's celebate our new widget with a couple graphs. Feel free
|
To define a custom kino we need to create a new module,
|
||||||
to try out other examples from the Mermaid website!
|
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
|
```elixir
|
||||||
Kino.Mermaid.new("""
|
Kino.HTML.new("""
|
||||||
graph TD;
|
<h3>Look!</h3>
|
||||||
A-->B;
|
|
||||||
A-->C;
|
<p>I wrote this HTML from <strong>Kino</strong>!</p>
|
||||||
B-->D;
|
|
||||||
C-->D;
|
|
||||||
""")
|
""")
|
||||||
```
|
```
|
||||||
|
|
||||||
```elixir
|
It works!
|
||||||
Kino.Mermaid.new("""
|
|
||||||
erDiagram
|
To learn more about other features provided by `Kino.JS`,
|
||||||
CUSTOMER ||--o{ ORDER : places
|
including persisting the output of your custom kinos to `.livemd` files,
|
||||||
ORDER ||--|{ LINE-ITEM : contains
|
[check out the documentation](https://hexdocs.pm/kino/Kino.JS.html).
|
||||||
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
|
||||||
""")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dynamic maps with Leaflet
|
## Dynamic maps with Leaflet
|
||||||
|
|
||||||
Widgets with static data are useful, but they really come down
|
Kinos with static data are useful, but they offer just a small peek
|
||||||
to a piece of JavaScript. This time we will try out something
|
into what can be achieved with custom kinos. This time we will try out
|
||||||
more exciting. We will set up a simple map and then push points
|
something more exciting. We will set up a simple map and then push points
|
||||||
directly from the Elixir code!
|
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
|
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),
|
when dealing with maps, for our purpose we will use [Leaflet](https://leafletjs.com),
|
||||||
which is an established solution in this area.
|
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
|
```elixir
|
||||||
defmodule Kino.Leaflet do
|
defmodule Kino.Leaflet do
|
||||||
use Kino.JS
|
use Kino.JS
|
||||||
|
|
@ -92,8 +123,8 @@ defmodule Kino.Leaflet do
|
||||||
Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
|
Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_marker(widget, location) do
|
def add_marker(kino, location) do
|
||||||
Kino.JS.Live.cast(widget, {:add_marker, normalize_location(location)})
|
Kino.JS.Live.cast(kino, {:add_marker, normalize_location(location)})
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -186,7 +217,7 @@ Kino.Leaflet.add_marker(map, {51.505 + delta.(), -0.09 + delta.()})
|
||||||
|
|
||||||
We barely scratched the surface of maps, the Leaflet API alone is extremely
|
We barely scratched the surface of maps, the Leaflet API alone is extremely
|
||||||
extensive and there are other packages worth exploring. However, even with
|
extensive and there are other packages worth exploring. However, even with
|
||||||
this simple widget we could already visualize some geographic data in real-time!
|
this simple kino we could already visualize some geographic data in real-time!
|
||||||
|
|
||||||
## Bidirectional live counter
|
## Bidirectional live counter
|
||||||
|
|
||||||
|
|
@ -206,8 +237,8 @@ defmodule Kino.Counter do
|
||||||
Kino.JS.Live.new(__MODULE__, count)
|
Kino.JS.Live.new(__MODULE__, count)
|
||||||
end
|
end
|
||||||
|
|
||||||
def bump(widget) do
|
def bump(kino) do
|
||||||
Kino.JS.Live.cast(widget, :bump)
|
Kino.JS.Live.cast(kino, :bump)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -261,11 +292,11 @@ defmodule Kino.Counter do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
At this point the server mechanics should be clear. On the
|
The server mechanics are quite similar to the Leaflet example.
|
||||||
client side we listen to button clicks and whenever it happens
|
The only difference is that we have a new callback, `handle_event/3`,
|
||||||
we send the `"bump"` event to the server. This event gets
|
for handling events sent from the client. On the client side,
|
||||||
handled by the `handle_event` callback, similarly to other
|
this is done by listening to button clicks and dispatching them
|
||||||
message types.
|
to the server via `pushEvent`.
|
||||||
|
|
||||||
Let's render our counter!
|
Let's render our counter!
|
||||||
|
|
||||||
|
|
@ -285,6 +316,10 @@ Kino.Counter.bump(counter)
|
||||||
|
|
||||||
## Final words
|
## Final words
|
||||||
|
|
||||||
Hopefully these futher examples give you a better idea of the
|
Congratulations, you finished our "course" on Kino! Throughout
|
||||||
possibilities enabled by custom JavaScript widgets. We would
|
those guides, you mastered Kino's API and learned how to use its
|
||||||
love to see what cool stuff you can build with it! 🚀
|
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! 🚀
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
In this chapter we will use `Kino` and `VegaLite`
|
In this notebook, we will use `Kino` and `VegaLite`
|
||||||
to introspect and plot how our system behaves over
|
to introspect and plot how our system behaves over
|
||||||
time. If you are not familiar with VegaLite, [read
|
time. If you are not familiar with VegaLite, [read
|
||||||
our introductory chapter](/explore/notebooks/intro-to-vega-lite).
|
its introductory notebook](/explore/notebooks/intro-to-vega-lite).
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -215,5 +215,5 @@ for i <- 1..1_000_000 do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
In the next chapter, we will learn [how to use `Kino.Control`
|
In the next notebook, we will learn [how to use `Kino.Control`
|
||||||
to build a chat app](/explore/notebooks/chat-app)!
|
to build a chat app](/explore/notebooks/chat-app)!
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue