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
SelectionAPI for active-state detection (highlighting toolbar buttons) - A
MutationObserverto 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
| Group | Buttons |
|---|---|
| Marks | Bold, Italic, Underline, Strikethrough |
| Blocks | Heading 1, Heading 2, Heading 3, Paragraph |
| Lists | Bullet List, Ordered List |
| Quotes | Blockquote, Inline Code, Code Block |
| Links | Add 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)
endPlaceholder
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
Renders a rich text editor integrated with Phoenix.HTML.FormField.
The editor consists of:
- An optional
<label>linked tofield.id - A bordered container with a formatting toolbar and the editable area
- A
<input type="hidden">bound tofield.name(synced by the hook) - 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
inputevents to sync changes to the hidden input - Attaches
clickevents to toolbar buttons and dispatchesexecCommand - Tracks active formatting via
queryCommandStateto toggleis-active
Attributes
field(Phoenix.HTML.FormField) (required) - APhoenix.HTML.FormFieldfrom@form[:field_name]. Providesid,name,value, anderrorsfor full form integration. The field value is loaded into the editor as initial HTML content.label(:string) - Label text rendered above the editor. Passnilto omit the label. Defaults tonil.placeholder(:string) - Placeholder text shown when the editor is empty. Rendered via a CSS::beforepseudo-element usingdata-placeholderand theis-emptyclass — not via the nativeplaceholderattribute.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 tonil.