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
|
||||
|
||||
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
|
||||
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
|
||||
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)!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
developing custom JavaScript powered kinos. The examples discussed
|
||||
there are kept minimal to introduce the basic concepts without much
|
||||
overhead. In this notebook we take things a bit further and showcase
|
||||
a couple more elaborate use cases.
|
||||
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([
|
||||
|
|
@ -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
|
||||
from text specification using [Mermaid](https://mermaid-js.github.io/mermaid/#/).
|
||||
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.Mermaid do
|
||||
defmodule Kino.HTML do
|
||||
use Kino.JS
|
||||
|
||||
def new(graph) do
|
||||
Kino.JS.new(__MODULE__, graph)
|
||||
def new(html) when is_binary(html) do
|
||||
Kino.JS.new(__MODULE__, html)
|
||||
end
|
||||
|
||||
asset "main.js" do
|
||||
"""
|
||||
import "https://cdn.jsdelivr.net/npm/mermaid@8.13.3/dist/mermaid.min.js";
|
||||
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
|
||||
export function init(ctx, graph) {
|
||||
mermaid.render("graph1", graph, (svgSource, bindListeners) => {
|
||||
ctx.root.innerHTML = svgSource;
|
||||
bindListeners && bindListeners(ctx.root);
|
||||
});
|
||||
export function init(ctx, html) {
|
||||
ctx.root.innerHTML = html;
|
||||
}
|
||||
"""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In this case we pass the graph specification to Mermaid, which
|
||||
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 break it down.
|
||||
|
||||
Let's celebate our new widget with a couple graphs. Feel free
|
||||
to try out other examples from the Mermaid website!
|
||||
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.Mermaid.new("""
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
Kino.HTML.new("""
|
||||
<h3>Look!</h3>
|
||||
|
||||
<p>I wrote this HTML from <strong>Kino</strong>!</p>
|
||||
""")
|
||||
```
|
||||
|
||||
```elixir
|
||||
Kino.Mermaid.new("""
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
||||
""")
|
||||
```
|
||||
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
|
||||
|
||||
Widgets with static data are useful, but they really come down
|
||||
to a piece of JavaScript. 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!
|
||||
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
|
||||
|
|
@ -92,8 +123,8 @@ defmodule Kino.Leaflet do
|
|||
Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
|
||||
end
|
||||
|
||||
def add_marker(widget, location) do
|
||||
Kino.JS.Live.cast(widget, {:add_marker, normalize_location(location)})
|
||||
def add_marker(kino, location) do
|
||||
Kino.JS.Live.cast(kino, {:add_marker, normalize_location(location)})
|
||||
end
|
||||
|
||||
@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
|
||||
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
|
||||
|
||||
|
|
@ -206,8 +237,8 @@ defmodule Kino.Counter do
|
|||
Kino.JS.Live.new(__MODULE__, count)
|
||||
end
|
||||
|
||||
def bump(widget) do
|
||||
Kino.JS.Live.cast(widget, :bump)
|
||||
def bump(kino) do
|
||||
Kino.JS.Live.cast(kino, :bump)
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -261,11 +292,11 @@ defmodule Kino.Counter do
|
|||
end
|
||||
```
|
||||
|
||||
At this point the server mechanics should be clear. On the
|
||||
client side we listen to button clicks and whenever it happens
|
||||
we send the `"bump"` event to the server. This event gets
|
||||
handled by the `handle_event` callback, similarly to other
|
||||
message types.
|
||||
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!
|
||||
|
||||
|
|
@ -285,6 +316,10 @@ Kino.Counter.bump(counter)
|
|||
|
||||
## Final words
|
||||
|
||||
Hopefully these futher examples give you a better idea of the
|
||||
possibilities enabled by custom JavaScript widgets. We would
|
||||
love to see what cool stuff you can build with it! 🚀
|
||||
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! 🚀
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
## 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
|
||||
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
|
||||
|
||||
|
|
@ -215,5 +215,5 @@ for i <- 1..1_000_000 do
|
|||
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)!
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue