defmodule LiveViewContinuity.Switch do @moduledoc """ An unstyled native switch with server-authoritative checked state and patch-safe optimistic interaction. See `SWITCH.md` for the observable interaction contract. """ use Phoenix.Component alias Phoenix.LiveView.JS attr(:id, :string, required: true) attr(:name, :string, required: true) attr(:checked, :boolean, required: true) attr(:on_change, :string, required: true) attr(:label, :string, default: nil) attr(:value, :string, default: "true") attr(:required, :boolean, default: false) attr(:disabled, :boolean, default: false) attr(:read_only, :boolean, default: false) attr(:invalid, :boolean, default: false) attr(:form, :string, default: nil) attr(:class, :any, default: nil) attr(:input_class, :any, default: nil) attr(:label_class, :any, default: nil) attr(:description_class, :any, default: nil) attr(:error_class, :any, default: nil) attr(:rest, :global) slot(:inner_block) slot(:description) slot(:error) def switch(assigns) do validate!(assigns) assigns = assign(assigns, :describedby, describedby(assigns)) ~H"""
"-input"} type="checkbox" role="switch" name={@name} value={@value} form={@form} class={@input_class} checked={@checked} required={@required} disabled={@disabled} aria-describedby={@describedby} aria-invalid={to_string(@invalid)} aria-readonly={to_string(@read_only)} data-lvc-switch-input data-lvc-checked={to_string(@checked)} phx-mounted={JS.ignore_attributes(["checked", "data-lvc-checked"])} />
"-description"} class={@description_class} data-lvc-switch-description > {render_slot(@description)}
"-error"} class={@error_class} hidden={!@invalid} data-lvc-switch-error > {render_slot(@error)}
""" end defp validate!(assigns) do validate_dom_value!(assigns.id, "switch id") validate_name!(assigns.name) validate_dom_value!(assigns.value, "switch value") if assigns.label != nil, do: validate_label!(assigns.label) if assigns.label == nil and assigns.inner_block == [], do: raise(ArgumentError, "switch requires a non-blank label or inner content") if length(assigns.inner_block) > 1, do: raise(ArgumentError, "switch accepts at most one inner content slot") if length(assigns.description) > 1, do: raise(ArgumentError, "switch accepts at most one description") if length(assigns.error) > 1, do: raise(ArgumentError, "switch accepts at most one error") end defp validate_dom_value!(value, name) when is_binary(value) do if value == "" or String.contains?(value, <<0>>) or Regex.match?(~r/[\t\n\f\r ]/, value), do: raise(ArgumentError, "#{name} must be a non-empty string without ASCII whitespace or NUL") end defp validate_dom_value!(_, name), do: raise(ArgumentError, "#{name} must be a string") defp validate_name!(name) when is_binary(name) do if String.trim(name) == "" or String.contains?(name, <<0>>), do: raise(ArgumentError, "switch name must be non-blank and contain no NUL") end defp validate_name!(_), do: raise(ArgumentError, "switch name must be a string") defp validate_label!(label) when is_binary(label) do if String.trim(label) == "", do: raise(ArgumentError, "switch label must be non-blank") end defp validate_label!(_), do: raise(ArgumentError, "switch label must be a non-blank string") defp describedby(assigns), do: Enum.join( Enum.reject( [ assigns.description != [] && assigns.id <> "-description", assigns.invalid && assigns.error != [] && assigns.id <> "-error" ], &(&1 == false) ), " " ) |> blank_to_nil() defp blank_to_nil(""), do: nil defp blank_to_nil(value), do: value end