View Source Kino.Screen behaviour (Kino v0.16.0)
Provides a LiveView-like experience for building forms in Livebook.
The screen receives its initial state and must implement the render/1
callback. Event handlers can be attached by calling the control/2
function.
The first render of the screen is shared across all users and then further
interactions happen within a per-user process.
Dynamic form/select
Here is an example that allows you to build a dynamic form that renders values depending on the chosen options. On submit, you then process the data (with optional validation) and writes the result into a separate frame. The output frame could also be configured to share results across all users.
defmodule MyScreen do
@behaviour Kino.Screen
# Import Kino.Control for forms, Kino.Input for inputs, and Screen for control/2
import Kino.{Control, Input, Screen}
@countries [nil: "", usa: "United States", canada: "Canada"]
@languages [
usa: [nil: "", en: "English", es: "Spanish"],
canada: [nil: "", en: "English", fr: "French"]
]
@defaults %{
country: nil,
language: nil
}
# This is a function we will use to start the screen.
#
# Our screen will be placed in a grid with one additional
# frame to render results into. And the state of the screen
# holds the form data and the result frame itself.
def new do
result_frame = Kino.Frame.new()
state = %{data: @defaults, frame: result_frame}
Kino.Layout.grid([
Kino.Screen.new(__MODULE__, state),
result_frame
])
end
def render(%{data: data}) do
form(
[
country: country_select(data),
language: language_select(data)
],
report_changes: true,
submit: "Submit"
)
|> control(&handle_event/2)
end
defp country_select(data) do
select("Country", @countries, default: data.country)
end
defp language_select(data) do
if languages = @languages[data.country] do
default = if languages[data.language], do: data.language
select("Language", languages, default: default)
end
end
def handle_event(%{data: data, type: :change}, state) do
%{state | data: data}
end
def handle_event(%{data: data, type: :submit, origin: client}, state) do
# If you want to validate the data, you could do
# here and render a different message.
markdown =
Kino.Markdown.new("""
Submitted!
* **Country**: #{data.country}
* **Language**: #{data.language}
""")
# We render the results only for the user who submits it,
# but you can share it across all by removing to: client.
Kino.Frame.render(state.frame, markdown, to: client)
# Reset form values on submission
%{state | data: @defaults}
end
end
MyScreen.new()
Wizard example
Here is an example of how to build wizard-like functionality with Kino.Screen
:
defmodule MyScreen do
@behaviour Kino.Screen
# Import Kino.Control for forms, Kino.Input for inputs, and Screen for control/2
import Kino.{Control, Input, Screen}
# Our screen will guide the user to provide its name and address.
# We also have a field keeping the current page and if there is an error.
def new do
state = %{page: 1, name: nil, address: nil, error: nil}
Kino.Screen.new(__MODULE__, state)
end
# The first screen gets the name.
#
# The `control/2` function comes from `Kino.Screen` and it specifies
# which function to be invoked on form submission.
def render(%{page: 1} = state) do
form(
[name: text("Name", default: state.name)],
submit: "Step one"
)
|> control(&step_one/2)
|> add_layout(state)
end
# The next screen gets the address.
def render(%{page: 2} = state) do
form(
[address: text("Address", default: state.address)],
submit: "Step two"
)
|> control(&step_two/2)
|> add_layout(state)
end
# The final screen shows a success message.
def render(%{page: 3} = state) do
Kino.Text.new("Well done, #{state.name}. You live in #{state.address}.")
|> add_layout(state)
end
# This is the layout shared across all pages.
defp add_layout(element, state) do
prefix = if state.error do
Kino.Text.new("Error: #{state.error}!")
end
suffix = if state.page > 1 do
button("Go back")
|> control(&go_back/2)
end
[prefix, element, suffix]
|> Enum.reject(&is_nil/1)
|> Kino.Layout.grid()
end
## Events handlers
defp step_one(%{data: %{name: name}}, state) do
if name == "" do
%{state | name: name, error: "name can't be blank"}
else
%{state | name: name, error: nil, page: 2}
end
end
defp step_two(%{data: %{address: address}}, state) do
if address == "" do
%{state | address: address, error: "address can't be blank"}
else
%{state | address: address, error: nil, page: 3}
end
end
defp go_back(_, state) do
%{state | page: state.page - 1}
end
end
MyScreen.new()
Summary
Callbacks
Callback invoked to render the screen, whenever there is a control event.
Functions
Receives a control or an input and invokes the given 2-arity function once its actions are triggered.
Creates a new screen with the given module and state.
Types
@type state() :: term()
The state of the screen
Callbacks
Callback invoked to render the screen, whenever there is a control event.
It receives the state and it must return a renderable output.
The first time this function is called, it is done within
a temporary process until the user first interacts with
an element via control/2
. Then all events happen in a
user-specific process.
Functions
@spec control(element, (map(), state() -> state())) :: element when element: Kino.Control.t() | Kino.Input.t()
Receives a control or an input and invokes the given 2-arity function once its actions are triggered.
@spec new(module(), term()) :: Kino.Frame.t()
Creates a new screen with the given module and state.