PhiaUi.Components.RichTextEditor (phia_ui v0.1.17)

Copy Markdown View Source

Rich text editor component for PhiaUI.

Provides rich_text_editor/1 — a contenteditable-based WYSIWYG editor with a full formatting toolbar, Phoenix.HTML.FormField integration, and changeset error display. Zero npm dependencies.

The editor is powered by the PhiaRichTextEditor JS hook, which uses:

  • document.execCommand() for formatting commands (bold, italic, lists, etc.)
  • The Selection API for active-state detection (highlighting toolbar buttons)
  • A MutationObserver to sync the HTML content to a hidden <input> on change

When to use

Use rich_text_editor/1 for fields that need multi-line formatted content:

  • Blog post / article body
  • Email template editor
  • Product description with emphasis and lists
  • Knowledge base article
  • Comment with basic formatting support

For plain text without formatting, use a regular textarea or input.

Registration

Register the hook in app.js:

import PhiaRichTextEditor from "./hooks/rich_text_editor"
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaRichTextEditor }
})

Basic example

<.form for={@form} phx-submit="publish">
  <.rich_text_editor
    field={@form[:body]}
    label="Article body"
    placeholder="Write your article here..."
    min_height="400px"
  />
  <.button type="submit">Publish</.button>
</.form>

Multiple editors on one page

Each editor is independent. Giving each a unique field is sufficient:

<.rich_text_editor field={@form[:summary]} label="Summary" min_height="120px" />
<.rich_text_editor field={@form[:body]}    label="Body"    min_height="400px" />

Toolbar groups

GroupButtons
MarksBold, Italic, Underline, Strikethrough
BlocksHeading 1, Heading 2, Heading 3, Paragraph
ListsBullet List, Ordered List
QuotesBlockquote, Inline Code, Code Block
LinksAdd Link, Remove Link

Each button has data-action which the hook reads to dispatch the correct execCommand. Active state is toggled via the is-active CSS class, which the hook sets based on document.queryCommandState().

Changeset integration

The hook syncs the contenteditable HTML to a <input type="hidden"> bound to field.name. On phx-submit, the hidden input carries the HTML string which the changeset receives:

def changeset(post, attrs) do
  post
  |> cast(attrs, [:body])
  |> validate_required([:body])
  # Optionally sanitise HTML server-side with HtmlSanitizeEx:
  # |> update_change(:body, &HtmlSanitizeEx.basic_html/1)
end

Placeholder

The placeholder is implemented via CSS ::before pseudo-element using data-placeholder and the is-empty class toggled by the hook. This avoids native placeholder which is not supported on contenteditable elements.

Security note

The editor produces raw HTML. Always sanitise the stored HTML server-side before rendering it back to other users. Consider HtmlSanitizeEx or Earmark for sanitisation.

Summary

Functions

Renders a rich text editor integrated with Phoenix.HTML.FormField.

Functions

rich_text_editor(assigns)

Renders a rich text editor integrated with Phoenix.HTML.FormField.

The editor consists of:

  1. An optional <label> linked to field.id
  2. A bordered container with a formatting toolbar and the editable area
  3. A <input type="hidden"> bound to field.name (synced by the hook)
  4. Changeset validation errors from field.errors

The PhiaRichTextEditor hook:

  • Sets contenteditable="true" on the editor div on mount
  • Loads data-content (the field's current value) as initial HTML
  • Attaches input events to sync changes to the hidden input
  • Attaches click events to toolbar buttons and dispatches execCommand
  • Tracks active formatting via queryCommandState to toggle is-active

Attributes

  • field (Phoenix.HTML.FormField) (required) - A Phoenix.HTML.FormField from @form[:field_name]. Provides id, name, value, and errors for full form integration. The field value is loaded into the editor as initial HTML content.

  • label (:string) - Label text rendered above the editor. Pass nil to omit the label. Defaults to nil.

  • placeholder (:string) - Placeholder text shown when the editor is empty. Rendered via a CSS ::before pseudo-element using data-placeholder and the is-empty class — not via the native placeholder attribute.

    Defaults to nil.

  • min_height (:string) - Minimum height of the editable content area as a CSS value. The editor grows vertically beyond this minimum as content is added. Example values: "200px", "400px", "50vh".

    Defaults to "200px".

  • class (:string) - Additional CSS classes for the outer wrapper div. Defaults to nil.