Note: Set readonly to see files and images.<h1 class="text-primary-lg-regular mb-6">Using name + value: </h1><turbo-frame id="some_unique_id"> <form action="#" accept-charset="UTF-8" method="get"> <input type="hidden" name="name" id="name" value="rich_text" /><input type="hidden" name="value" id="value" /><input type="hidden" name="placeholder" id="placeholder" value="Digite aqui" /><input type="hidden" name="readonly" id="readonly" value="false" /><input type="hidden" name="help" id="help" /><input type="hidden" name="error" id="error" /><input type="hidden" name="mode" id="mode" value="inline" /> <div class="lui-form_entry lui-form_entry--horizontal"> <div class="lui-form_entry__input"> <div data-controller="input" data-input-open-actions-value="false" class="lui-inner-input relative flex gap-2" data-input-original-input-value="{"time":1576142051557,"blocks":[{"type":"paragraph","data":{"text":"hello\u003cscript\u003ehacker code\u003c/script\u003e \u003cb\u003eWorld2\u003c/b\u003e"}},{"type":"header","data":{"text":"testing\u003ci\u003ehello\u003c/i\u003e\u003csmall\u003esmall texty\u003c/small\u003e","level":2}},{"type":"embed","data":{"service":"coub","source":"https://coub.com/view/1czcdf","embed":"https://coub.com/embed/1czcdf","width":580,"height":320,"caption":"My Life"}}],"version":"2.16.1"}" data-input-mode-value="inline" data-input-form-value=""> <div class="w-full flex flex-col"> <div class="lui-rich_text"> <div data-react-class="RichTextEditor" data-react-props="{"value":"{\"time\":1576142051557,\"blocks\":[{\"type\":\"paragraph\",\"data\":{\"text\":\"hello\\u003cscript\\u003ehacker code\\u003c/script\\u003e \\u003cb\\u003eWorld2\\u003c/b\\u003e\"}},{\"type\":\"header\",\"data\":{\"text\":\"testing\\u003ci\\u003ehello\\u003c/i\\u003e\\u003csmall\\u003esmall texty\\u003c/small\\u003e\",\"level\":2}},{\"type\":\"embed\",\"data\":{\"service\":\"coub\",\"source\":\"https://coub.com/view/1czcdf\",\"embed\":\"https://coub.com/embed/1czcdf\",\"width\":580,\"height\":320,\"caption\":\"My Life\"}}],\"version\":\"2.16.1\"}","placeholder":"Digite aqui","name":"rich_text","language":"en","readOnly":false,"uploadEndpoint":null}" data-react-cache-id="RichTextEditor-0"></div> </div> </div> <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="cancel" data-action="click->input#handleClose" type="button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-xmark" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="submit" data-action="click->input#setLoading" type="submit" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-check" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> </span> </div> </div> <div class="lui-form_entry__label-wrapper pt-2" style="width: 140px; min-width: 140px; word-break: break-word;"> <span class="lui-form_entry__label">Rich Text</span> <span class="lui-form_entry__required"></span> <div class="lui-form_entry__icon"> </div> </div> </div> <div class="lui-form_entry lui-form_entry--horizontal"> <div class="lui-form_entry__input"> <div class="lui-text relative"> <div data-controller="input" data-input-open-actions-value="false" class="lui-inner-input relative flex gap-2" data-input-original-input-value="Hello World" data-input-mode-value="inline" data-input-form-value=""> <div class="w-full flex flex-col"> <span class="lui-input "> <input name="text" type="text" value="Hello World" class="lui-input__input" mode="inline" contentEditable="true" data-input-target="input" data-action="input->input#onChange change->input#onChange"> <span class="lui-input__spinner"> <i class="fa-regular fa-spinner"></i> </span> </span> </div> <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="cancel" data-action="click->input#handleClose" type="button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-xmark" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="submit" data-action="click->input#setLoading" type="submit" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-check" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> </span> </div> </div> </div> <div class="lui-form_entry__label-wrapper pt-2" style="width: 140px; min-width: 140px; word-break: break-word;"> <span class="lui-form_entry__label">Text</span> <span class="lui-form_entry__required"></span> <div class="lui-form_entry__icon"> </div> </div> </div> </form></turbo-frame>RichTextEditor
Description
Related components
| Used Components | Components where is Used |
|---|---|
| Label |
Usage rules
- ✅ Do
- ❌ Don't
Note: Set readonly to see files and images.<% data = preview_params[:data].presence || { "time": 1576142051557, "blocks": [ { "type": "paragraph", "data": { "text": "hello<script>hacker code</script> <b>World2</b>" } }, { "type": "header", "data": { "text": "testing<i>hello</i><small>small texty</small>", "level": 2 } }, preview_params[:readonly] && { "type":"attaches", "data": { "file": { "title": "iframe-integration-guide-1.pdf", "size":60226, "url":"https://loopos-development.ams3.digitaloceanspaces.com/fqeg9m91iirzlw7c4vl4q3a68w14" }, "title": "iframe-integration-guide-1.pdf" } }, preview_params[:readonly] && { "id":"-INNQ1ensV", "type":"image", "data": { "caption":"jjj", "withBorder":true, "withBackground":false, "stretched":false, "file": { "title":"akogpz5kxx21erwnat22g7t6hz9y.png", "size":2249841, "url":"https://loopos-development.ams3.digitaloceanspaces.com/gfjj7yd40pzyiue6vm5fq8e0gr40" } } }, { "type": "embed", "data": { "service": "coub", "source": "https://coub.com/view/1czcdf", "embed": "https://coub.com/embed/1czcdf", "width": 580, "height": 320, "caption": "My Life" } } ].compact_blank, "version": "2.16.1" }.to_json%><h1 class="text-primary-lg-regular mb-6">Using name + value: </h1><%= turbo_frame_tag "some_unique_id" do %> <%= form_with url: "#", method: :get do %> <%= lookbook_fields(preview_params) %> <%= render LooposUi::FormEntry.new(label: "Rich Text", orientation: "horizontal", label_width: 140) do |entry| %> <% entry.with_input do %> <%= render LooposUi::Inputs::RichText.new( **preview_params, value: params[preview_params[:name]].presence || preview_params[:value].presence || data ) %> <% end %> <% end %> <%= render LooposUi::FormEntry.new(label: "Text", orientation: "horizontal", label_width: 140) do |entry| %> <% entry.with_input do %> <%= render LooposUi::Inputs::Text.new(name: "text", value: "Hello World", mode: :inline) %> <% end %> <% end %> <% end %><% end %>Base options
| Param | Description | Input |
|---|---|---|
|
Input name |
|
|
|
Input value |
|
|
|
Input placeholder |
|
|
|
Input readonly |
|
|
|
Input help text |
|
|
|
Input error text |
|
|
|
— |
|
Description
Inputs::RichText component is used to render a rich text editor powered by EditorJS. It provides a WYSIWYG editor with support for various formatting options including headers, lists, quotes, code blocks, tables, embeds, and more. The component supports file uploads for images and attachments when an upload endpoint is provided.
Notes
- If you want to be able to enable image and attachments, you will have to define the upload endpoint property. If you don't, you are only able to see images and files on read only mode. Check the itemscontroller uploadattachment endpoint to see the format of the request and response.
Arguments
| Property | Default | Description |
|---|---|---|
model |
nil |
ActiveRecord instance. |
attribute |
nil |
Attribute name of the model. |
name |
nil |
Name of the input field. |
value |
nil |
Value of the input field (can be a JSON string or plain text). |
placeholder |
nil |
Placeholder text displayed in the editor. |
error |
nil |
Error message. If model is present, the errors will be extracted for the current attribute |
help |
nil |
Help message. |
mode |
:inline |
Mode of the input. Can be :inline, :autosubmit, or :form. |
form |
nil |
Form object to associate the input with. |
readonly |
false |
Whether the editor is in read-only mode. |
open_actions |
false |
Open field in edit mode by default. |
upload_endpoint |
nil |
URL endpoint for uploading files (images and attachments). When provided, enables image and attachment upload functionality. |
Supported Editor Features
The RichText editor supports the following EditorJS tools:
- Paragraph - Basic text editing with inline toolbar
- Header - Headers of various sizes
- List - Ordered and unordered lists
- Quote - Block quotes
- Code - Code blocks
- Table - Tables
- Marker - Text highlighting
- Embed - Embedded content (YouTube, Twitter, etc.)
- Image - Image insertion (only available when
upload_endpointis provided) - Attaches - File attachments (only available when
upload_endpointis provided)
Value Format
The value argument accepts:
- JSON string - EditorJS output format (preferred)
- Plain text - Legacy format, will be automatically converted to a paragraph block
The component automatically saves changes as a JSON string in EditorJS format, which includes structured block data with types and content.
Slots
No slots available for this component.
Usage
You have two usages for this component:
- With a model and an attribute
- With name and value
Example using a model and an attribute:
<%= render LooposUi::Inputs::RichText.new( model: Item.new(notes: '{"blocks":[{"type":"paragraph","data":{"text":"Initial notes"}}]}'), attribute: :notes, placeholder: "Add your notes here...", mode: :autosubmit) %>Example using name and value:
<%= render LooposUi::Inputs::RichText.new( name: "content", value: "Default content", placeholder: "Enter your content", mode: :inline) %>Example with file uploads:
<%= form_with url: upload_path, method: :post do |form| %> <%= render LooposUi::Inputs::RichText.new( name: "content", value: notes, placeholder: "Add notes...", mode: :autosubmit, upload_endpoint: helpers.upload_attachment_path( token: item.token, authenticity_token: form_authenticity_token ) ) %><% end %>Example in read-only mode:
<%= render LooposUi::Inputs::RichText.new( name: "internal_notes", value: item.notes, readonly: true) %>⚠️ Important
Form Handling: It's the responsibility of the developer to handle the generation of the form, and handling any turbo frame updates or form submissions. The recommendation is to use the
form_withhelper from Rails, along with aturbo_frame_tag, so only the inline component is updated.Auto-save: The component automatically saves changes with a 1-second debounce delay to prevent excessive save events while typing.
File Uploads: When
upload_endpointis provided, the component expects the endpoint to:- Accept POST requests with a
fileparameter - Return JSON in the format:
{ "data": { "filename": "...", "byte_size": ..., "url": "..." } }
- Accept POST requests with a
Language: The editor automatically uses the current
I18n.localefor translations.Custom Readonly: The component uses
custom_readonly: trueinternally, which means it renders the content instead of just the value when in readonly mode.