Notesclub

Building a chat app with Kino.Control

chat_app.livemd

Building a chat app with Kino.Control

Mix.install([
  {:kino, "~> 0.8.0"}
])

Introduction

In this notebook, we will build a chat application using kino. First, let’s have a look at the building blocks that we will need!

Kino.Control

In our introduction to Kino, we learned about inputs and several different outputs, such as tables, frames, and more. In particular, we learned how to use inputs to capture values directly into our notebooks:

name = Kino.Input.text("Your name")

and use them to print something back:

IO.puts("Hello, #{Kino.Input.read(name)}!")

Inputs have one special power: they are shared across all users accessing the notebook. For example, if you copy and paste the URL of this notebook into another tab, the input will share the same value. This plays into Livebook’s strengths of being an interactive and collaborative tool.

Sometimes, however, you don’t want the input to be shared. You want each different user to get their own inputs and perform individual actions. That’s exactly how Kino.Control works. Each control is specific to each user on the page. You then receive each user interaction as a message.

The button control

The simplest control is Kino.Control.button/1. Let’s give it a try:

click_me = Kino.Control.button("Click me!")

Execute the cell above and the button will be rendered. You can click it, but nothing will happen. Luckily, we can subscribe to the button events:

Kino.Control.subscribe(click_me, :click_me)

Now that we have subscribed, every time the button is clicked, we will receive a message tagged with :click_me. Let’s print all messages in our inbox:

Process.info(self(), :messages)

Now execute the cell above, click the button a couple times, and re-execute the cell above. For each click, there is a new message in our inbox. There are several ways we can consume this message. Let’s see a different one in the next example.

The form control

Whenever we want to submit multiple inputs at once, we can use Kino.Control.form/2.

inputs = [
  first_name: Kino.Input.text("First name"),
  last_name: Kino.Input.text("Last name")
]

form = Kino.Control.form(inputs, submit: "Greet")

Execute the cell above and you will see a form rendered. You can now fill in the form and press the submit button. Each submission will trigger a new event. Let’s consume them as a stream. Elixir streams are lazy collections that are consumed as they happen:

for event <- Kino.Control.stream(form) do
  IO.inspect(event)
end

Now, as you submit the form, you should see a new event printed. However, there is a downside: we are now stuck inside this infinite loop of events. Luckily, we started this particular section as a branched section, which means the main execution flow will not be interrupted. But it is something you should keep in mind in the future. You can also stop it by pressing the “Stop” button above the Code cell.

The chat application

We are now equipped with all knowledge necessary to build our chat application. First, we will need a frame. Every time a new message is received, we will append it to the frame:

frame = Kino.Frame.new()

Now we need a form with the user name and their message:

inputs = [
  name: Kino.Input.text("Name"),
  message: Kino.Input.text("Message")
]

form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:message])

Notice we used a new option, called :reset_on_submit, that automatically clears the input once submitted. Finally, let’s stream the form events and post each message to the frame:

for %{data: %{name: name, message: message}} <- Kino.Control.stream(form) do
  content = Kino.Markdown.new("**#{name}**: #{message}")
  Kino.Frame.append(frame, content)
end

Execute the cell above and your chat app should be fully operational. Open up this same notebook across on different tabs and each different user can post their messages.

In the next notebook we will go one step further and develop a multiplayer pong game!