Packages
livebook
0.6.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, error: nil, local: false)}
end
@impl true
def update(assigns, socket) do
value = assigns.input_values[assigns.attrs.id]
socket =
socket
|> assign(assigns)
|> assign(value: value, initial_value: value)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<form phx-change="change" phx-submit="submit" phx-target={@myself}>
<div class="input-label">
<%= @attrs.label %>
</div>
<.input
id={"#{@id}-input"}
attrs={@attrs}
value={@value}
error={@error}
myself={@myself} />
<%= if @error do %>
<div class="input-error">
<%= @error %>
</div>
<% end %>
</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="300"
phx-blur="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
data-el-input
class="input min-h-[200px] tiny-scrollbar"
name="value"
phx-debounce="300"
phx-blur="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="300"
phx-blur="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 #{if(@error, do: "input--error")}"}
name="value"
value={to_string(@value)}
phx-debounce="300"
phx-blur="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: "text"
defp html_input_type(:text), do: "text"
@impl true
def handle_event("change", %{"value" => html_value}, socket) do
socket = handle_html_value(socket, html_value)
socket =
if report_immediately?(socket.assigns.attrs.type) do
maybe_report_change(socket)
else
socket
end
{:noreply, socket}
end
def handle_event("blur", %{"value" => html_value}, socket) do
socket = socket |> handle_html_value(html_value) |> maybe_report_change()
if socket.assigns.error do
{:noreply, assign(socket, value: socket.assigns.initial_value, error: nil)}
else
{:noreply, socket}
end
end
def handle_event("submit", %{"value" => html_value}, socket) do
socket = socket |> handle_html_value(html_value) |> maybe_report_change()
send(self(), {:queue_bound_cells_evaluation, socket.assigns.attrs.id})
{:noreply, socket}
end
defp handle_html_value(socket, html_value) do
case parse(html_value, socket.assigns.attrs) do
{:ok, value} ->
assign(socket, value: value, error: nil)
{:error, error, value} ->
assign(socket, value: value, error: error)
end
end
defp report_immediately?(type), do: type in [:select, :checkbox]
defp maybe_report_change(socket) when socket.assigns.error != nil, do: socket
defp maybe_report_change(%{assigns: %{value: value, initial_value: value}} = socket), do: socket
defp maybe_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
socket
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", html_value}
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: self(), type: :change}
send(socket.assigns.attrs.destination, {:event, topic, event})
end
end