Packages
livebook
0.5.1
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-element="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-element="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-element="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-element="input"
class="input h-[200px] resize-none 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-element="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-element="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
{:noreply, handle_html_value(socket, html_value)}
end
def handle_event("blur", %{"value" => html_value}, socket) do
socket = handle_html_value(socket, html_value)
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 = handle_html_value(socket, html_value)
send(self(), {:queue_bound_cells_evaluation, socket.assigns.attrs.id})
{:noreply, socket}
end
defp handle_html_value(socket, html_value) do
current_value = socket.assigns.value
case parse(html_value, socket.assigns.attrs) do
{:ok, ^current_value} ->
socket
{:ok, value} ->
send(
self(),
{:set_input_values, [{socket.assigns.attrs.id, value}], socket.assigns.local}
)
unless socket.assigns.local do
report_event(socket, value)
end
assign(socket, value: value, error: nil)
{:error, error, value} ->
assign(socket, value: value, error: error)
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", 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