mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-16 12:57:32 +08:00
6f53e3db6a
* Add support for form control * Handle report_changes map * Assert on input/control events
261 lines
6.2 KiB
Elixir
261 lines
6.2 KiB
Elixir
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
|