Packages
livebook
0.16.4
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: value, changed: changed} = assigns.input_views[assigns.input.id]
socket =
socket
|> assign(assigns)
|> assign(value: value, changed: changed)
{:ok, socket}
end
@impl true
def render(assigns) when assigns.input.attrs.type == :image do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.ImageInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
height={@input.attrs.size && elem(@input.attrs.size, 0)}
width={@input.attrs.size && elem(@input.attrs.size, 1)}
format={@input.attrs.format}
fit={@input.attrs.fit}
input_id={@input.id}
session_pid={@session_pid}
client_id={@client_id}
local={@local}
/>
</div>
"""
end
def render(assigns) when assigns.input.attrs.type == :audio do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.AudioInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
format={@input.attrs.format}
sampling_rate={@input.attrs.sampling_rate}
input_id={@input.id}
session_pid={@session_pid}
client_id={@client_id}
local={@local}
/>
</div>
"""
end
def render(assigns) when assigns.input.attrs.type == :file do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.FileInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
accept={@input.attrs.accept}
input_id={@input.id}
session_pid={@session_pid}
client_id={@client_id}
local={@local}
/>
</div>
"""
end
def render(assigns) when assigns.input.attrs.type == :utc_datetime do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label
label={@input.attrs.label}
changed={@changed}
help="Choose the time in your local time zone"
/>
<div class="inline-flex">
<.text_field
class="w-auto"
id={@id}
type="datetime-local"
data-el-input
name="html_value"
step="60"
autocomplete="off"
phx-hook="UtcDateTimeInput"
value={nil}
data-p-utc-value={hook_prop(@value && NaiveDateTime.to_iso8601(@value))}
data-p-utc-min={hook_prop(@input.attrs.min && NaiveDateTime.to_iso8601(@input.attrs.min))}
data-p-utc-max={hook_prop(@input.attrs.max && NaiveDateTime.to_iso8601(@input.attrs.max))}
data-p-phx-target={hook_prop(@myself)}
/>
</div>
</div>
"""
end
def render(assigns) when assigns.input.attrs.type == :utc_time do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label
label={@input.attrs.label}
changed={@changed}
help="Choose the time in your local time zone"
/>
<div class="inline-flex">
<.text_field
id={@id}
type="time"
data-el-input
name="html_value"
step="60"
autocomplete="off"
phx-hook="UtcTimeInput"
value={nil}
data-p-utc-value={hook_prop(@value && Time.to_iso8601(@value))}
data-p-utc-min={hook_prop(@input.attrs.min && Time.to_iso8601(@input.attrs.min))}
data-p-utc-max={hook_prop(@input.attrs.max && Time.to_iso8601(@input.attrs.max))}
data-p-phx-target={hook_prop(@myself)}
/>
</div>
</div>
"""
end
def render(assigns) do
~H"""
<form id={"#{@id}-form-#{@counter}"} phx-change="change" phx-submit="submit" phx-target={@myself}>
<.input_label label={@input.attrs.label} changed={@changed} />
<.input_output id={"#{@id}-input"} attrs={@input.attrs} value={@value} myself={@myself} />
</form>
"""
end
defp input_output(%{attrs: %{type: :select}} = assigns) do
~H"""
<div class="w-60">
<.select_field
id={@id}
name="html_value"
value={Enum.find_index(@attrs.options, fn {key, _label} -> key == @value end)}
options={Enum.with_index(@attrs.options, fn {_key, label}, idx -> {label, idx} end)}
data-el-input
/>
</div>
"""
end
defp input_output(%{attrs: %{type: :checkbox}} = assigns) do
~H"""
<div class="mt-1">
<.switch_field data-el-input name="html_value" value={@value} id={@id} />
</div>
"""
end
defp input_output(%{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="range-input"
id={@id}
name="html_value"
value={@value}
phx-debounce={@attrs.debounce}
phx-target={@myself}
spellcheck="false"
autocomplete="off"
min={@attrs.min}
max={@attrs.max}
step={@attrs.step}
/>
<div>{@attrs.max}</div>
</div>
"""
end
defp input_output(%{attrs: %{type: :textarea}} = assigns) do
~H"""
<.textarea_field
id={@id}
class="min-h-[38px] max-h-[300px]"
monospace={@attrs.monospace}
data-el-input
name="html_value"
value={@value}
phx-hook="TextareaAutosize"
phx-debounce={@attrs.debounce}
phx-target={@myself}
spellcheck="false"
/>
"""
end
defp input_output(%{attrs: %{type: :password}} = assigns) do
~H"""
<div class="inline-flex">
<.password_field
id={@id}
data-el-input
name="html_value"
value={@value}
phx-debounce={@attrs.debounce}
phx-target={@myself}
spellcheck="false"
autocomplete="off"
/>
</div>
"""
end
defp input_output(%{attrs: %{type: :date}} = assigns) do
~H"""
<div class="inline-flex">
<.text_field
type="date"
data-el-input
id={@id}
name="html_value"
value={@value}
phx-debounce="blur"
phx-target={@myself}
min={@attrs.min}
max={@attrs.max}
autocomplete="off"
/>
</div>
"""
end
defp input_output(%{attrs: %{type: :color}} = assigns) do
~H"""
<div class="w-16">
<.text_field
type="color"
class="h-12"
data-el-input
id={@id}
name="html_value"
value={to_string(@value)}
phx-debounce={@attrs.debounce}
phx-target={@myself}
spellcheck="false"
autocomplete="off"
/>
</div>
"""
end
defp input_output(%{attrs: %{type: type}} = assigns)
when type in [:number, :url, :text] do
~H"""
<div class="inline-flex">
<.text_field
type={html_input_type(@attrs.type)}
data-el-input
id={@id}
name="html_value"
value={to_string(@value)}
phx-debounce={@attrs.debounce}
phx-target={@myself}
spellcheck="false"
autocomplete="off"
/>
</div>
"""
end
defp input_output(assigns) do
~H"""
<div class="text-red-600">
Unknown input type {@input.attrs.type}
</div>
"""
end
attr :label, :string, required: true
attr :changed, :boolean, required: true
attr :help, :string, default: nil
defp input_label(assigns) do
~H"""
<.label help={@help}>
<div class="flex items-center justify-between gap-1">
<span>{@label}</span>
<span :if={@changed} class="cursor-pointer tooltip top" data-tooltip="This input has changed.">
<.remix_icon icon="error-warning-line text-gray-500" />
</span>
</div>
</.label>
"""
end
defp html_input_type(:number), do: "number"
defp html_input_type(:url), do: "url"
defp html_input_type(:text), do: "text"
@impl true
def handle_event("change", %{"html_value" => html_value}, socket) do
case parse(html_value, socket.assigns.input.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", %{"html_value" => html_value}, socket) do
case parse(html_value, socket.assigns.input.attrs) do
{:ok, value} ->
socket = handle_change(socket, value)
send(self(), {:queue_bound_cells_evaluation, socket.assigns.input.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.input.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
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 parse(html_value, %{type: :utc_datetime} = attrs) do
if html_value do
with {:ok, datetime} <- NaiveDateTime.from_iso8601(html_value),
datetime <- truncate_datetime(datetime),
true <- in_range?(datetime, attrs.min, attrs.max) do
{:ok, datetime}
else
_ -> :error
end
else
{:ok, nil}
end
end
defp parse(html_value, %{type: :utc_time} = attrs) do
if html_value do
with {:ok, time} <- Time.from_iso8601(html_value),
time <- truncate_time(time),
true <- in_range?(time, attrs.min, attrs.max) do
{:ok, time}
else
_ -> :error
end
else
{:ok, nil}
end
end
defp parse(html_value, %{type: :date} = attrs) do
if html_value == "" do
{:ok, nil}
else
with {:ok, date} <- Date.from_iso8601(html_value),
true <- in_range?(date, attrs.min, attrs.max) do
{:ok, date}
else
_ -> :error
end
end
end
defp truncate_datetime(datetime) do
datetime
|> NaiveDateTime.truncate(:second)
|> Map.replace!(:second, 0)
end
defp truncate_time(time) do
time
|> Time.truncate(:second)
|> Map.replace!(:second, 0)
end
defp in_range?(%struct{} = datetime, min, max)
when struct in [NaiveDateTime, Time, Date] do
(min == nil or struct.compare(datetime, min) != :lt) and
(max == nil or struct.compare(datetime, max) != :gt)
end
defp report_event(socket, value) do
topic = socket.assigns.input.ref
event = %{value: value, origin: socket.assigns.client_id, type: :change}
send(socket.assigns.input.destination, {:event, topic, event})
end
end