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.

embeds

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.

embed 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.

 editors