Editor Example
A dummy-only host Stimulus controller coordinates shadcn-rails components without adding a new registry component.
Live demo
Toggle marks or alignment, type in the textarea, then preview live or save to POST the form back to this page.
Host editor controller
The wrapper uses data-controller="editor" while shadcn ToggleGroup events drive the editor state.
Example source
app/views/docs/examples/_editor_demo.html.erb
<%= form_with scope: :editor, url: docs_editor_example_path, method: :post, data: { controller: "editor" }, class: "space-y-6" do %>
<div class="space-y-4">
<div class="flex flex-wrap items-center gap-3">
<div class="space-y-2">
<p class="text-sm font-medium">Marks</p>
<%= render Shadcn::ToggleGroupComponent.new(
type: :multiple,
variant: :outline,
name: "editor[marks]",
value: @editor_params[:marks].to_s.split(","),
data: { action: "shadcn--toggle-group:change->editor#marksChanged" }
) do |group| %>
<% group.with_item(value: "bold", aria_label: "Bold") { "B" } %>
<% group.with_item(value: "italic", aria_label: "Italic") { "I" } %>
<% group.with_item(value: "underline", aria_label: "Underline") { "U" } %>
<% end %>
</div>
<div class="space-y-2">
<p class="text-sm font-medium">Alignment</p>
<%= render Shadcn::ToggleGroupComponent.new(
type: :single,
variant: :outline,
name: "editor[alignment]",
value: @editor_params[:alignment],
data: { action: "shadcn--toggle-group:change->editor#alignmentChanged" }
) do |group| %>
<% group.with_item(value: "left", aria_label: "Align left") { "Left" } %>
<% group.with_item(value: "center", aria_label: "Align center") { "Center" } %>
<% group.with_item(value: "right", aria_label: "Align right") { "Right" } %>
<% end %>
</div>
</div>
<%= render Shadcn::TextareaComponent.new(
name: "editor[body]",
id: "editor_body",
rows: 6,
value: @editor_params[:body],
data: {
"editor-target": "canvas",
action: "input->editor#canvasChanged"
}
) %>
<%= render Shadcn::ButtonGroupComponent.new do |group| %>
<% group.with_button(variant: :outline, type: "button", data: { action: "click->editor#preview" }) { "Preview" } %>
<% group.with_button(type: "submit") { "Save" } %>
<% end %>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border bg-card p-4">
<h3 class="mb-2 text-sm font-medium">Preview</h3>
<div data-editor-target="preview" class="min-h-24 rounded-md bg-muted p-3 text-sm"></div>
</div>
<div class="rounded-lg border bg-card p-4">
<h3 class="mb-2 text-sm font-medium">Params</h3>
<pre data-editor-target="params" class="min-h-24 whitespace-pre-wrap rounded-md bg-muted p-3 text-xs"></pre>
</div>
</div>
<% end %>
Host Stimulus controller
app/javascript/controllers/editor_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["canvas", "params", "preview"]
connect() {
this.applyCurrentState()
this.updatePreview()
this.updateParams()
}
marksChanged(event) {
this.applyMarks(this.normalizeValues(event.detail.value))
this.updatePreview()
this.updateParams()
}
alignmentChanged(event) {
this.applyAlignment(this.normalizeValues(event.detail.value)[0] || "left")
this.updatePreview()
this.updateParams()
}
canvasChanged() {
this.updatePreview()
this.updateParams()
}
preview(event) {
event.preventDefault()
this.applyCurrentState()
this.updatePreview()
}
applyCurrentState() {
this.applyMarks(this.paramsForForm().editor.marks)
this.applyAlignment(this.paramsForForm().editor.alignment)
}
applyMarks(marks) {
this.canvasTarget.style.fontWeight = marks.includes("bold") ? "700" : "400"
this.canvasTarget.style.fontStyle = marks.includes("italic") ? "italic" : "normal"
this.canvasTarget.style.textDecoration = marks.includes("underline") ? "underline" : "none"
}
applyAlignment(alignment) {
this.canvasTarget.style.textAlign = alignment
}
updatePreview() {
this.previewTarget.textContent = this.canvasTarget.value || "Nothing to preview yet."
this.previewTarget.style.fontWeight = this.canvasTarget.style.fontWeight
this.previewTarget.style.fontStyle = this.canvasTarget.style.fontStyle
this.previewTarget.style.textDecoration = this.canvasTarget.style.textDecoration
this.previewTarget.style.textAlign = this.canvasTarget.style.textAlign
}
updateParams() {
this.paramsTarget.textContent = JSON.stringify(this.paramsForForm(), null, 2)
}
paramsForForm() {
const formData = new FormData(this.element)
return {
editor: {
marks: this.normalizeValues(formData.get("editor[marks]")),
alignment: formData.get("editor[alignment]") || "left",
body: formData.get("editor[body]") || ""
}
}
}
normalizeValues(value) {
if (Array.isArray(value)) return value
if (!value) return []
return value.toString().split(",").filter(Boolean)
}
}