defmodule Corex.Editable do @moduledoc ~S''' Phoenix implementation of [Zag.js Editable](https://zagjs.com/components/react/editable). ## Anatomy ### Basic ```heex <.editable value="Click to edit" class="editable"> <:label>Name <:edit_trigger><.heroicon name="hero-pencil-square" class="icon" /> <:submit_trigger><.heroicon name="hero-check" class="icon" /> <:cancel_trigger><.heroicon name="hero-x-mark" class="icon" /> ``` Required slots: `:label`, `:edit_trigger`, `:submit_trigger`, `:cancel_trigger`. Preview value is managed by the component and the Editable TS hook. ## API Requires a stable `id` on `<.editable>`. | Function | Action | Returns | | -------- | ------ | ------- | | [`set_value/2`](#set_value/2) | Set preview value (client) | `%Phoenix.LiveView.JS{}` | | [`set_value/3`](#set_value/3) | Set preview value (server) | `socket` | ## Events Pick an event name and pass it to `on_*` on `<.editable>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_value_change="editable_changed"` | Value committed or cancelled | `%{"id" => id, "value" => string}` | ### on_value_change ```heex <.editable value="Click to edit" class="editable" on_value_change="editable_changed" > <:label>Name <:edit_trigger><.heroicon name="hero-pencil-square" class="icon" /> <:submit_trigger><.heroicon name="hero-check" class="icon" /> <:cancel_trigger><.heroicon name="hero-x-mark" class="icon" /> ``` ```elixir def handle_event("editable_changed", %{"id" => _id, "value" => value}, socket) do {:noreply, assign(socket, :name, value)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_value_change_client="editable-changed"` | Value changes | `id`, `value` | ## Form Use `field={f[:name]}` inside `<.form>` for changeset-backed forms. For cross-cutting invalid styling and error presentation, see the [Forms](forms.html) guide. Pass `invalid={Corex.FormField.invalid?(@form[:name])}` when you want alert borders after validation. ```heex <.form for={@form} phx-change="validate"> <.editable field={@form[:name]} class="editable"> <:label>Name <:error :let={msg}> <.heroicon name="hero-exclamation-circle" class="icon" /> {msg} <:edit_trigger><.heroicon name="hero-pencil-square" class="icon" /> <:submit_trigger><.heroicon name="hero-check" class="icon" /> <:cancel_trigger><.heroicon name="hero-x-mark" class="icon" /> ``` ## Style Use data attributes to target elements: ```css [data-scope="editable"][data-part="root"] {} [data-scope="editable"][data-part="area"] {} [data-scope="editable"][data-part="label"] {} [data-scope="editable"][data-part="input"] {} [data-scope="editable"][data-part="preview"] {} [data-scope="editable"][data-part="edit-trigger"] {} [data-scope="editable"][data-part="control"] {} [data-scope="editable"][data-part="submit-trigger"] {} [data-scope="editable"][data-part="cancel-trigger"] {} [data-scope="editable"][data-part="error"] {} ``` If you wish to use the default Corex styling, you can use the class `editable` on the component. This requires to install `Mix.Tasks.Corex.Design` first and import the component css file. ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/editable.css"; ``` You can then use modifiers ```heex <.editable class="editable editable--accent editable--lg" value=""> <:label>Label <:edit_trigger>Edit <:submit_trigger>Save <:cancel_trigger>Cancel ``` ''' @doc type: :component use Phoenix.Component import Corex.Api.Doc alias Phoenix.HTML.Form alias Phoenix.LiveView alias Phoenix.LiveView.JS alias Corex.Editable.Anatomy.{ Area, CancelTrigger, Control, EditTrigger, FormValue, Input, Label, Preview, Props, Root, SubmitTrigger, Triggers } alias Corex.Editable.Connect alias Corex.Editable.Translation attr(:id, :string, required: false, doc: "The id of the editable component") attr(:value, :string, default: "", doc: "Initial preview text (Zag defaultValue)") attr(:disabled, :boolean, default: false, doc: "Whether the editable is disabled") attr(:read_only, :boolean, default: false, doc: "Whether the editable is read-only") attr(:required, :boolean, default: false, doc: "Whether the input is required") attr(:invalid, :boolean, default: false, doc: "Whether the editable is in invalid state") attr(:name, :string, default: nil, doc: "The name attribute for form submission") attr(:form, :string, default: nil, doc: "The id of the form this input belongs to") attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "Text direction") attr(:orientation, :string, default: "vertical", values: ["horizontal", "vertical"]) attr(:default_edit, :boolean, default: false, doc: "Initial edit state") attr(:placeholder, :string, default: nil, doc: "Placeholder text when value is empty") attr(:activation_mode, :string, default: nil, values: [nil, "dblclick", "focus"], doc: "How to activate edit mode" ) attr(:select_on_focus, :boolean, default: true, doc: "Whether to select all text on focus") attr(:on_value_change, :string, default: nil, doc: "Server event name when value changes") attr(:on_value_change_client, :string, default: nil, doc: "Client event name when value changes" ) attr(:translation, Corex.Editable.Translation, default: nil, doc: "Override translatable strings" ) attr(:field, Phoenix.HTML.FormField, default: nil, doc: "A form field struct, e.g. f[:text] or @form[:text]" ) attr(:errors, :list, default: [], doc: "List of error messages to display") attr(:rest, :global) slot :label, required: true do attr(:class, :string, required: false) end slot :edit_trigger, required: true do attr(:class, :string, required: false) end slot :submit_trigger, required: true do attr(:class, :string, required: false) end slot :cancel_trigger, required: true do attr(:class, :string, required: false) end slot :error, required: false do attr(:class, :string, required: false) end def editable(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do assigns |> Corex.FormField.assign_form_field(field) |> assign(:value, value_to_string(Form.normalize_value("text", field.value))) |> editable() end def editable(assigns) do translation = Translation.resolve(assigns.translation) value_s = value_to_string(Form.normalize_value("text", assigns[:value])) content_value = value_s || "" empty = String.trim(content_value) == "" editing = assigns[:default_edit] || false value_text = if(empty, do: assigns[:placeholder] || "", else: content_value) assigns = assigns |> assign_new(:id, fn -> "editable-#{System.unique_integer([:positive])}" end) |> assign_new(:form_field, fn -> false end) |> assign_new(:dir, fn -> "ltr" end) |> assign_new(:orientation, fn -> "horizontal" end) |> assign(:translation, translation) |> assign(:value, value_s) |> assign(:content_value, content_value) |> assign(:empty, empty) |> assign(:editing, editing) |> assign(:value_text, value_text) ~H"""