When a website requires a lot of custom functionality, we often reach for RubyOnRails rather than something like WordPress or SilverStripe. Rails has a lot of capability that enables us to build CMS features comparable to the best CMS systems. So when you want something unique, you don't have to miss out on great content editing features. Let's take a look at an aspect at the center of content editing - the WYSIWYG.
ActionText is built into RubyOnRails and provides a robust and configurable trix text editor "for every day writing". For CMS use, some extra features are needed that are not included by default - but never fear! ActionText is built to be modified.
To follow along, get your Rails app up and running version 6+ with Stimulusjs ready to go. We will start with a way to embed things into the editor. What things? Anything embeddable! Videos, tweets, instagrams and the like. Today let's build a dialog that lets a user embed a tweet or a youtube or anything else embeddable.
In your form add a stimulus controller to the rich_text_area - we will use it to change the toolbar, add dialogs and listen for attachment insertions.
<%= f.rich_text_area :content, data: { controller: "bebop" } %>
In the bebop Controller add methods to add a toolbar button and a dialog for your new feature and call them in the connect method.
import { Controller } from "@hotwired/stimulus"
import Trix from 'trix'
addHeadingAttributes()
export default class extends Controller {
static get targets() { }
connect() {
this.addEmbedButton()
this.addEmbedDialog()
this.eventListenerForEmbedButton()
}
addEmbedButton() {
const buttonHTML = 'Embed'
this.buttonGroupFileTools.insertAdjacentHTML("beforeend", buttonHTML)
}
addEmbedDialog() {
const dialogHTML = ``
this.dialogsElement.insertAdjacentHTML("beforeend", dialogHTML)
}
showembed(e){
const dialog = this.toolbarElement.querySelector('[data-trix-dialog="embed"]')
const embedInput = this.dialogsElement.querySelector('[name="embed"]')
if (event.target.classList.contains("trix-active")) {
event.target.classList.remove("trix-active");
dialog.classList.remove("trix-active");
delete dialog.dataset.trixActive;
embedInput.setAttribute("disabled", "disabled");
} else {
event.target.classList.add("trix-active");
dialog.classList.add("trix-active");
dialog.dataset.trixActive = "";
embedInput.removeAttribute("disabled");
embedInput.focus();
}
}
eventListenerForEmbedButton() {
this.toolbarElement.querySelector('[data-trix-action="embed"]').addEventListener("click", e => {
this.showembed(e)
})
}
insertAttachment(content, sgid){
const attachment = new Trix.Attachment({content, sgid})
this.element.editor.insertAttachment(attachment)
}
}
Note that last method. That is what we will be sending the embed content and sgid (a global identifier) back to from our embed controller. It looks simple but it does a lot - creating a new attachment in the trix editor. So far, so good. We have a button and it shows a dialog.
Now you want to have a stimulus controller to manage this new button and dialog.
import { Controller } from "@hotwired/stimulus"
import Rails from "@rails/ujs"
export default class extends Controller {
connect() {
}
static get targets() {
return [ "input", "submit"]
}
embedit(e){
e.preventDefault
let formData = new FormData()
formData.append("content", this.inputTarget.value)
Rails.ajax({
type: 'PATCH',
url: `/admin/embed/`,
data: formData,
success: ({content, sgid}) => {
this.editorController().insertAttachment(content, sgid);
}
})
}
editorController(){
return this.application.getControllerForElementAndIdentifier(this.editorElement(), "bebop")
}
editorElement(){
return document.querySelector(this.editorElementName())
}
editorElementName(){
return `#${this.finderDiv().dataset.editorId}`
}
finderDiv(){
return this.element.closest('.embedder')
}
}
This couldn't be simpler. It sends the url you paste to your rails controller, which returns back the attachment content html and the sgid of the attachment - so that action text can rebuild the attachment when it displays the content. Now all you need is a nice simple embeds_controller.rb to deal with that:
class Admin::EmbedsController < AdminController
def update
puts params
@embed = Embed.find_or_create_by(url: params[:content])
content = ApplicationController.render(partial: 'embeds/thumbnail',
locals: { embed: @embed },
formats: :html)
render json: { content: content, sgid: @embed.attachable_sgid }
end
end
We need a model to generate the actual embed code. We can use the oembed gem to help with that and we should store the output so that we don't have to make repeated external requests. Oembed creates a thumbnail and html for embeds from a huge range of services including some paid agreggators that boost the services available. Note the Attachable include. That makes any model into an attachable resource that you can attach to the actiontext instance.
class Embed < ApplicationRecord
include ActionText::Attachable
require 'oembed'
after_create :setup
def setup
resource = oembed
self.video = resource.video?
if resource.video?
self.thumbnail_url = resource.thumbnail_url
end
self.html = resource.html
self.save
end
def oembed
OEmbed::Providers.register_all
return OEmbed::Providers.get(url, {width: '500px'})
end
def to_trix_content_attachment_partial_path
"embeds/thumbnail"
end
end
And some super simple views to show the embed on the front end and a thumbnail in the editor.
_embed.html.erb
<%= embed.html.html_safe %>
_thumbnail.html.erb
<%= image_tag embed.thumbnail_url %>
You can repeat these steps to create any kind of document attachment you can think of.
To summarise:
In your Trix Stimulus Controller:
- Add a button
- Add a dialog
In your Feature Stimulus Controller:
- Add functionality to your dialog
- Fetch a content & sgid pair from your rails controller.
In your Rails App:
- Respond to the Stimuls request for any lists of items like images or pages.
- Respond with a Content & SGID pair to the stimulus.js
- Make sure the relevent model is Attachable
- Add the editor and content partials
That's a wrap. A super flexible embed feature to add all those shiny web embeds to your content. That's not the end though. You can repeat the same steps and attach anything to your document. images, internal links etc. All the features of a modern CMS.
Leave a Reply
Comments
Chetan Dev Mittal:
Hi,
Thanks for this post.
However, it is not working with Rails 7.0.2.2.
I am getting error at "addHeadingAttributes()" not defined in chrom devtools console.
Do you have a working example or a gist which I can refer?
Regards.
comments powered by Disqus