Dual-mode content editor LiveComponent with visual (WYSIWYG) and markdown modes.
Visual mode uses a contenteditable div with vanilla JS (no npm dependencies). Markdown mode uses a plain textarea with toolbar support. Content syncs between modes using Earmark (markdown→HTML) and client-side HTML→markdown conversion.
Usage
import Leaf, only: [leaf_editor: 1]
<.leaf_editor
id="my-editor"
content={@content}
mode={:visual}
preset={:advanced}
toolbar={[:image, :video]}
placeholder="Write something..."
readonly={false}
height="480px"
debounce={400}
/>Presets
:advanced(default) — Full toolbar with all formatting options:simple— Compact toolbar for comments/lightweight editing: undo/redo, bold, italic, strikethrough, inline code, lists, link, emoji, clear formatting
Messages Sent to Parent
{:leaf_changed, %{editor_id, markdown, html}}— Content updated{:leaf_insert_request, %{editor_id, type: :image | :video}}— Insert requested{:leaf_mode_changed, %{editor_id, mode: :visual | :markdown}}— Mode switched
Commands from Parent
Use send_update/2:
send_update(Leaf, id: "my-editor", action: :insert_image, url: "https://...", alt: "description")
send_update(Leaf, id: "my-editor", action: :set_content, content: "# Hello")
send_update(Leaf, id: "my-editor", action: :set_mode, mode: :visual)JS Setup
Add to your app.js:
import "../../../deps/leaf/priv/static/assets/leaf.js"
let Hooks = {
Leaf: window.LeafHooks.Leaf,
// ... your other hooks
}Gettext (optional)
To enable translations, configure a gettext backend:
config :leaf, :gettext_backend, MyApp.GettextOtherwise, English strings are used as-is.
Summary
Functions
Renders a Leaf editor as a function component.
Functions
Renders a Leaf editor as a function component.
This is a convenience wrapper around the Leaf LiveComponent.
Import it in your view helpers:
import Leaf, only: [leaf_editor: 1]Then use it in your templates:
<.leaf_editor id="my-editor" content={@content} />All attributes are passed through to the underlying LiveComponent.
Attributes
id(:string) (required)content(:string) - Defaults to"".mode(:atom) - Defaults to:hybrid. Must be one of:visual,:hybrid,:markdown, or:html.preset(:atom) - Defaults to:advanced. Must be one of:advanced, or:simple.toolbar(:list) - Defaults to[].placeholder(:string) - Defaults to"Write something...".readonly(:boolean) - Defaults tofalse.height(:string) - Defaults to"480px".debounce(:integer) - Defaults to400.upload_handler(:any) - Defaults tonil.sync_input_name(:string) - Defaults tonil.class(:string) - Defaults tonil.script_nonce(:string) - Defaults to"".loading_preset(:atom) - Defaults to:random. Must be one of:default,:random,:unpuzzling,:brewing,:polishing,:composing,:crafting, or:tidying.loading_text(:string) - Defaults tonil.- Global attributes are accepted.