Packages
livebook
0.8.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/output/input_component.ex
defmodule LivebookWeb.Output.InputComponent do
use LivebookWeb, :live_component
@impl true
def mount(socket) do
{:ok, assign(socket, local: false, counter: 0)}
end
@impl true
def update(%{event: :change, value: value}, socket) do
{:ok, handle_change(socket, value)}
end
def update(assigns, socket) do
value = assigns.input_values[assigns.attrs.id]
socket =
socket
|> assign(assigns)
|> assign(value: value)
{:ok, socket}
end
@impl true
def render(%{attrs: %{type: :image}} = assigns) do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<div class="input-label">
<%= @attrs.label %>
</div>
<.live_component
module={LivebookWeb.Output.ImageInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
height={@attrs.size && elem(@attrs.size, 0)}
width={@attrs.size && elem(@attrs.size, 1)}
format={@attrs.format}
fit={@attrs.fit}
/>
</div>
"""
end
def render(%{attrs: %{type: :audio}} = assigns) do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<div class="input-label">
<%= @attrs.label %>
</div>
<.live_component
module={LivebookWeb.Output.AudioInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
format={@attrs.format}
sampling_rate={@attrs.sampling_rate}
/>
</div>
"""
end
def render(%{attrs: %{type: :file}} = assigns) do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<div class="input-label">
<%= @attrs.label %>
</div>
<.live_component
module={LivebookWeb.Output.FileInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
accept={@attrs.accept}
input_id={@attrs.id}
session_id={@session_id}
/>
</div>
"""
end
def render(assigns) do
~H"""
<form id={"#{@id}-form-#{@counter}"} phx-change="change" phx-submit="submit" phx-target={@myself}>
<div class="input-label">
<%= @attrs.label %>
</div>
<.input id={"#{@id}-input"} attrs={@attrs} value={@value} myself={@myself} />
</form>
"""
end
defp input(%{attrs: %{type: :select}} = assigns) do
~H"""
<select data-el-input class="input input-select" name="value">
<%= for {{key, label}, idx} <- Enum.with_index(@attrs.options) do %>
<option value={idx} selected={@value == key}>
<%= label %>
</option>
<% end %>
</select>
"""
end
defp input(%{attrs: %{type: :checkbox}} = assigns) do
~H"""
<div class="mt-1">
<.switch_checkbox data-el-input name="value" checked={@value} />
</div>
"""
end
defp input(%{attrs: %{type: :range}} = assigns) do
~H"""
<div class="flex items-center space-x-2">
<div><%= @attrs.min %></div>
<input
type="range"
data-el-input
class="input-range"
name="value"
value={@value}
phx-debounce="blur"
phx-target={@myself}
spellcheck="false"
autocomplete="off"
min={@attrs.min}
max={@attrs.max}
step={@attrs.step}
/>
<div><%= @attrs.max %></div>
</div>
"""
end
defp input(%{attrs: %{type: :textarea}} = assigns) do
~H"""
<textarea
id={@id}
data-el-input
class={"input min-h-[38px] max-h-[300px] tiny-scrollbar #{if(@attrs[:monospace], do: "font-mono")}"}
name="value"
phx-hook="TextareaAutosize"
phx-debounce="blur"
phx-target={@myself}
spellcheck="false"
><%= [?\n, @value] %></textarea>
"""
end
defp input(%{attrs: %{type: :password}} = assigns) do
~H"""
<.with_password_toggle id={"#{@id}-password-toggle"}>
<input
type="password"
data-el-input
class="input w-auto bg-gray-50"
name="value"
value={@value}
phx-debounce="blur"
phx-target={@myself}
spellcheck="false"
autocomplete="off"
/>
</.with_password_toggle>
"""
end
defp input(%{attrs: %{type: type}} = assigns) when type in [:number, :color, :url, :text] do
~H"""
<input
type={html_input_type(@attrs.type)}
data-el-input
class="input w-auto invalid:input--error"
name="value"
value={to_string(@value)}
phx-debounce="blur"
phx-target={@myself}
spellcheck="false"
autocomplete="off"
/>
"""
end
defp input(assigns) do
~H"""
<div class="text-red-600">
Unknown input type <%= @attrs.type %>
</div>
"""
end
defp html_input_type(:number), do: "number"
defp html_input_type(:color), do: "color"
defp html_input_type(:url), do: "url"
defp html_input_type(:text), do: "text"
@impl true
def handle_event("change", %{"value" => html_value}, socket) do
case parse(html_value, socket.assigns.attrs) do
{:ok, value} ->
{:noreply, handle_change(socket, value)}
:error ->
# Force the current value
{:noreply, update(socket, :counter, &(&1 + 1))}
end
end
def handle_event("submit", %{"value" => html_value}, socket) do
case parse(html_value, socket.assigns.attrs) do
{:ok, value} ->
socket = handle_change(socket, value)
send(self(), {:queue_bound_cells_evaluation, socket.assigns.attrs.id})
{:noreply, socket}
:error ->
{:noreply, socket}
end
end
defp handle_change(socket, value) do
prev_value = socket.assigns.value
socket = assign(socket, value: value)
if value != prev_value do
report_change(socket)
end
socket
end
defp report_change(%{assigns: assigns} = socket) do
send(self(), {:set_input_values, [{assigns.attrs.id, assigns.value}], assigns.local})
unless assigns.local do
report_event(socket, assigns.value)
end
end
defp parse(html_value, %{type: :text}) do
{:ok, html_value}
end
defp parse(html_value, %{type: :textarea}) do
# The browser may normalize newlines to \r\n, but we prefer just \n
value = String.replace(html_value, "\r\n", "\n")
{:ok, value}
end
defp parse(html_value, %{type: :password}) do
{:ok, html_value}
end
defp parse(html_value, %{type: :number}) do
if html_value == "" do
{:ok, nil}
else
case Integer.parse(html_value) do
{number, ""} ->
{:ok, number}
_ ->
{number, ""} = Float.parse(html_value)
{:ok, number}
end
end
end
defp parse(html_value, %{type: :url}) do
cond do
html_value == "" -> {:ok, nil}
Livebook.Utils.valid_url?(html_value) -> {:ok, html_value}
true -> {:error, "not a valid URL"}
end
end
defp parse(html_value, %{type: :select, options: options}) do
selected_idx = String.to_integer(html_value)
options
|> Enum.with_index()
|> Enum.find_value(fn {{key, _label}, idx} ->
idx == selected_idx && {:ok, key}
end)
end
defp parse(html_value, %{type: :checkbox}) do
{:ok, html_value == "true"}
end
defp parse(html_value, %{type: :range}) do
{number, ""} = Float.parse(html_value)
{:ok, number}
end
defp parse(html_value, %{type: :color}) do
{:ok, html_value}
end
defp report_event(socket, value) do
topic = socket.assigns.attrs.ref
event = %{value: value, origin: socket.assigns.client_id, type: :change}
send(socket.assigns.attrs.destination, {:event, topic, event})
end
end