Corex.ColorPicker (Corex v0.1.1)

View Source

Phoenix implementation of Zag.js Color Picker.

Anatomy

Basic

<.color_picker
  value="rgb(25, 9, 192, 0.9)"
  label="Select Color (RGBA)"
  presets={["#ff0000", "#00ff00", "#0000ff", "rgb(25, 9, 192, 0.9)"]}
  class="color-picker"
/>

Style

Target elements via data attributes:

[data-scope="color-picker"][data-part="root"] {}
[data-scope="color-picker"][data-part="label"] {}
[data-scope="color-picker"][data-part="control"] {}
[data-scope="color-picker"][data-part="trigger"] {}
[data-scope="color-picker"][data-part="content"] {}
[data-scope="color-picker"][data-part="area"] {}
[data-scope="color-picker"][data-part="channel-slider"] {}
[data-scope="color-picker"][data-part="swatch-trigger"] {}
[data-scope="color-picker"][data-part="error"] {}

Import the Corex design:

@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/color-picker.css";

API

Requires a stable id on <.color_picker>.

FunctionActionReturns
set_value/2Set color (client)%Phoenix.LiveView.JS{}
set_value/3Set color (server)socket

set_value

<.action phx-click={Corex.ColorPicker.set_value("color-picker-api", "#ff0000")} class="button button--sm">
  Set red
</.action>
<.color_picker id="color-picker-api" value="#000000" label="Color" class="color-picker" />
def handle_event("set_color", %{"color" => hex}, socket) do
  {:noreply, Corex.ColorPicker.set_value(socket, "color-picker-api", hex)}
end

Events

Server events

EventWhenPayload
on_value_change="color_value_changed"Color changes%{"id" => id, "valueAsString" => value}
on_open_change="color_open_changed"Open state changes%{"id" => id, "open" => open}

on_value_change

<.color_picker
  class="color-picker"
  value="#3b82f6"
  label="Color"
  on_value_change="color_value_changed"
/>
def handle_event("color_value_changed", %{"valueAsString" => value}, socket) do
  {:noreply, assign(socket, :color, value)}
end

on_open_change

<.color_picker
  class="color-picker"
  value="#3b82f6"
  label="Color"
  on_open_change="color_open_changed"
/>
def handle_event("color_open_changed", %{"open" => open}, socket) do
  {:noreply, assign(socket, :color_picker_open, open)}
end

Client events

EventWhenevent.detail
on_value_change_client="color-value-changed"Color changesid, valueAsString
on_open_change_client="color-open-changed"Open state changesid, open
on_format_change_client="color-format-changed"Format changesid, format
on_pointer_down_outside_client="color-pointer-down-outside"Pointer down outsideid, ...
on_focus_outside_client="color-focus-outside"Focus outsideid, ...
on_interact_outside_client="color-interact-outside"Interact outsideid, ...

on_value_change_client

<.color_picker
  id="color-picker-events-client"
  class="color-picker"
  value="#3b82f6"
  label="Color"
  on_value_change_client="color-value-changed"
/>
document.getElementById("color-picker-events-client")?.addEventListener("color-value-changed", (e) => {
  console.log(e.detail);
});

on_open_change_client

<.color_picker
  id="color-picker-open-events-client"
  class="color-picker"
  value="#3b82f6"
  label="Color"
  on_open_change_client="color-open-changed"
/>
document.getElementById("color-picker-open-events-client")?.addEventListener("color-open-changed", (e) => {
  console.log(e.detail);
});

Form

Set the form id in to_form/2 and use <.form for={@form}>. Use field={@form[:color]} so the picker name matches the form. For Ecto validation in LiveView, add phx-change on the form so params stay in sync.

For cross-cutting invalid styling and error presentation, see the Forms guide. Pass invalid={Corex.FormField.invalid?(@form[:color])} when you want alert borders after validation.

Phoenix form (changeset)

Heex

    <.form
      :let={f}
      for={@form}
      action="/color-picker/form"
      method="post"
    >
      <.color_picker
        field={f[:color]}
        label="Color"
        class="color-picker"
        presets={["#ff0000", "#00ff00", "#0000ff"]}
      >
        <:error :let={msg}>
          <.heroicon name="hero-exclamation-circle" class="icon" />
          {msg}
        </:error>
      </.color_picker>

      <.action type="submit" class="button button--accent">
        Submit
      </.action>
    </.form>

Elixir

    def color_picker_form_page(conn, _params) do
      phoenix_form =
        Phoenix.Component.to_form(%{"color" => "#3b82f6"},
          as: :color_picker_phoenix,
          id: "color-picker-form-phoenix"
        )

      render(conn, :color_picker_form_page, phoenix_form: phoenix_form)
    end

    def color_picker_form_submit(conn, params) do
      if is_map(params["color_picker_phoenix"]) do
        color = params["color_picker_phoenix"]["color"] || ""

        conn
        |> put_flash(:info, "Submitted: color=#{inspect(color)}")
        |> redirect(to: "/color-picker/form")
      end
    end

Ecto changeset (validation)

Heex

    <.form
      :let={f}
      for={@form}
      action="/color-picker/form"
      method="post"
    >
      <.color_picker
        field={f[:color]}
        label="Color"
        class="color-picker"
        presets={["#ff0000", "#00ff00", "#0000ff"]}
      >
        <:error :let={msg}>
          <.heroicon name="hero-exclamation-circle" class="icon" />
          {msg}
        </:error>
      </.color_picker>

      <.action type="submit" class="button button--accent">
        Submit
      </.action>
    </.form>

Elixir

    def color_picker_form_validate_page(conn, _params) do
      changeset =
        MyApp.Form.ColorPickerForm.changeset_validate(%MyApp.Form.ColorPickerForm{}, %{})

      form =
        Phoenix.Component.to_form(changeset,
          as: :color_picker_validate,
          id: "color-picker-validate-form"
        )

      render(conn, :color_picker_form_page, form: form)
    end

    def color_picker_form_validate_create(conn, %{"color_picker_validate" => params}) do
      case MyApp.Form.ColorPickerForm.changeset_validate(%MyApp.Form.ColorPickerForm{}, params) do
        %Ecto.Changeset{valid?: true} = changeset ->
          data = Ecto.Changeset.apply_changes(changeset)

          conn
          |> put_flash(:info, "Saved: color=#{data.color}")
          |> redirect(to: "/settings")

        changeset ->
          changeset = Map.put(changeset, :action, :insert)

          form =
            Phoenix.Component.to_form(changeset,
              as: :color_picker_validate,
              id: "color-picker-validate-form"
            )

          render(conn, :color_picker_form_page, form: form)
      end
    end

Ecto

    defmodule MyApp.Form.ColorPickerForm do
      use Ecto.Schema
      import Ecto.Changeset

      embedded_schema do
        field :color, :string, default: "#3b82f6"
      end

      def changeset(form, attrs \\ %{}) do
        form
        |> cast(attrs, [:color])
        |> validate_required([:color])
      end

      def changeset_validate(form, attrs \\ %{}) do
        form
        |> cast(attrs, [:color])
        |> validate_required([:color])
        |> validate_alpha_max_50()
      end

      defp validate_alpha_max_50(changeset) do
        with value when is_binary(value) <- get_field(changeset, :color),
             [_, alpha] <-
               Regex.run(~r/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/, value),
             {float_val, _} <- Float.parse(alpha),
             true <- float_val > 0.5 do
          add_error(changeset, :color, "maximum alpha allowed is 50%")
        else
          _ -> changeset
        end
      end
    end

Native form (plain HTML)

    <form
      action="/color-picker/form"
      method="post"
    >
      <input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
      <.color_picker
        name="color_picker_form[color]"
        value="#3b82f6"
        label="Color"
        class="color-picker"
      />
      <.action type="submit" class="button button--accent">
        Submit
      </.action>
    </form>

Elixir

    def color_picker_form_submit(conn, %{"color_picker_form" => %{"color" => color}}) do
      conn
      |> put_flash(:info, "Submitted: color=#{color}")
      |> redirect(to: "/color-picker/form")
    end

Summary

API

Set the picker value from a control (phx-click). value is a color string (e.g. hex).

Set the value from handle_event.

Components

color_picker(assigns)

Attributes

  • id (:string) - The id of the color picker.
  • value (:string) - Initial color string sent as data-default-value for the hook. Defaults to "#000000".
  • name (:string) - The name attribute for form submission. Defaults to nil.
  • label (:string) - Label for the color picker trigger. Defaults to "Select Color".
  • close_on_select (:boolean) - Defaults to true.
  • open_auto_focus (:boolean) - Defaults to true.
  • disabled (:boolean) - Defaults to false.
  • invalid (:boolean) - Defaults to false.
  • read_only (:boolean) - Defaults to false.
  • required (:boolean) - Defaults to false.
  • dir (:string) - Defaults to nil.Must be one of nil, "ltr", or "rtl".
  • positioning (:map) - Defaults to %Corex.Positioning{hide_when_detached: true, strategy: "fixed", placement: "bottom", gutter: 8, shift: 0, overflow_padding: 0, arrow_padding: 4, flip: true, slide: true, overlap: false, same_width: false, fit_viewport: false, offset: nil}.
  • presets (:list) - Defaults to [].
  • class (:string) - Defaults to nil.
  • on_value_change (:string) - Defaults to nil.
  • on_value_change_client (:string) - Defaults to nil.
  • on_value_change_end (:string) - Defaults to nil.
  • on_value_change_end_client (:string) - Defaults to nil.
  • on_open_change (:string) - Defaults to nil.
  • on_open_change_client (:string) - Defaults to nil.
  • on_format_change (:string) - Defaults to nil.
  • on_format_change_client (:string) - Defaults to nil.
  • on_pointer_down_outside (:string) - Defaults to nil.
  • on_pointer_down_outside_client (:string) - Defaults to nil.
  • on_focus_outside (:string) - Defaults to nil.
  • on_focus_outside_client (:string) - Defaults to nil.
  • on_interact_outside (:string) - Defaults to nil.
  • on_interact_outside_client (:string) - Defaults to nil.
  • translation (Corex.ColorPicker.Translation) - Override translatable strings. Defaults to nil.
  • errors (:list) - Error messages to display (non-field API). Defaults to [].
  • field (Phoenix.HTML.FormField) - A form field, e.g. f[:color] or @form[:color].
  • Global attributes are accepted.

Slots

  • error - Accepts attributes:
    • class (:string)

API

set_value(color_picker_id, value)

Set the picker value from a control (phx-click). value is a color string (e.g. hex).

<.action phx-click={Corex.ColorPicker.set_value("my-color-picker", "#226677")}>Set</.action>
<.color_picker id="my-color-picker" class="color-picker" label="Color" value="#000000" />
document.getElementById("my-color-picker")?.dispatchEvent(
  new CustomEvent("corex:color-picker:set-value", {
    bubbles: false,
    detail: { value: "#226677" },
  })
);

set_value(socket, color_picker_id, value)

Set the value from handle_event.

<.action phx-click="pick_color" phx-value-value="#226677">Set</.action>
<.color_picker id="my-color-picker" class="color-picker" label="Color" value="#000000" />
def handle_event("pick_color", %{"value" => v}, socket) do
  {:noreply, Corex.ColorPicker.set_value(socket, "my-color-picker", v)}
end