How to use Editor.js in Rails 7

James Garcia
2023-04-13 20:05:48 UTC

editor JS hero

In this article I will step through adding Editor.Js to a Rails 7 app. Lets start with creating the Rails app.

rails new editor-js-app --javascript=esbuild

So we have a resource to work with let’s scaffold Articles.

rails generate scaffold article title body:json

Now open the migration <timestamp>_create_articles.rb, and update the body to have a default value of “{}”.


class CreateArticles < ActiveRecord::Migration[7.0]
  def change
    create_table :articles do |t|
      t.string :title
      t.json :body, default: {} # <= update this line

      t.timestamps
    end
  end
end

Save and then run rails db:migrate to create the table.

Next we will add the Editor.js and some Block tools. If you only add the Editor.js it will come with a default Text (a.k.a. paragraph) tool. With the extra ones you will have the option for a Header and Text tool with alignment options. In the terminal run:

yarn add @editorjs/editorjs @editorjs/header editorjs-paragraph-with-alignment@3.x

To connect the editor to our articles we will use a Stimulus controller. Lets create the controller. In the terminal run:

rails generate stimulus editor

This create the editor_controller.js in the app/javascript/controllers folder and update the index.js in the same folder to register the controller. Open the editor_controller.js and overwrite the code with the following:

import { Controller } from "@hotwired/stimulus"
import EditorJS from '@editorjs/editorjs';
import Paragraph from 'editorjs-paragraph-with-alignment';
import Header from '@editorjs/header';

export default class extends Controller {
  static targets = [ "body" ]
  static values = { 
    readonly: Boolean,
    editordata: String 
  }

  connect() {
    let body = this.editordataValue ? JSON.parse(this.editordataValue) : {}
    this.editor = new EditorJS({
      holder: 'editorjs',
      tools: {
        paragraph: { 
          class: Paragraph,
          config: {
            placeholder: 'Enter text'
          }
        },
        header: {
          class: Header,
          shortcut: 'CMD+SHIFT+H',
          config: {
            placeholder: 'Enter a header',
            levels: [1,2,3,4],
            default: 3
          }
        },
      },
      readOnly: this.readonlyValue,
      data: body,
      onChange: async () => {
        let content = await this.editor.saver.save();
        this.bodyTarget.value = JSON.stringify(content)
      },
    });
  }

  disconnect() {
    if (this.editor) {
      this.editor = null
    }
  }
}

The following will import the packages we added through yarn

import EditorJS from '@editorjs/editorjs';
import Paragraph from 'editorjs-paragraph-with-alignment';
import Header from '@editorjs/header';

The following will are Stimulus data attributes that you can reference. In are case we will be targeting a data attribute called “body” to put information and getting values from data attributes called “readonly” and “editordata”. We will be using these in the views and the Connect function in this same controller.

  static targets = [ "body" ]
  static values = { 
    readonly: Boolean,
    editordata: String 
  }

The connect function is main part. Here we instantiate and configure the editor.js. First we are setting variable body to the value of editordata or and empty object {} this is based on the condition if this is a new article or an existing article. With a new article the editordata will not have any value. This body variable will set the data attribute in the editor configuration.

The next item is the configuration of the editor. Holder is the id of the HTML attributes where the editorjs will be added. Tools are were we configure the Text, Header or whatever Block tool you bring in. In this case it is just those two.

With the latest version of editor.js you can set the editor to be ReadOnly. I am using this in the show.html.erb page, which we will see later.

Lastly we see the onChange function this is called everytime a value is update in the editor. What is happening is here is that the save function is called on the values of the editor on each change. This will this insert the content into a hidden field in the form that will later be submitted and saved to the database. I could have made the submit to do a fetch POST call, but there was some unexpected behavior when I attempted this, which was basically it would cause an error of too many redirects. So came up with this implementation and stayed closer to what is already built into the rails forms. Also, if I recall correctly other WYSYWIG editors do the same with a hidden field.

  connect() {
    let body = this.editordataValue ? JSON.parse(this.editordataValue) : {}
    this.editor = new EditorJS({
      holder: 'editorjs',
      tools: {
        paragraph: { 
          class: Paragraph,
          config: {
            placeholder: 'Enter text'
          }
        },
        header: {
          class: Header,
          shortcut: 'CMD+SHIFT+H',
          config: {
            placeholder: 'Enter a header',
            levels: [1,2,3,4],
            default: 3
          }
        },
      },
      readOnly: this.readonlyValue,
      data: body,
      onChange: async () => {
        let content = await this.editor.saver.save();
        this.bodyTarget.value = JSON.stringify(content)
      },
    });
  }

The disconnect function is just to do some clean up of the editor.

  disconnect() {
    if (this.editor) {
      this.editor = null
    }
  }

Next we can update the views. Open the Articles new.html.erb file and update the following. This will connect the editor_controller.js, and will provide the editordata value.

<div data-controller="editor"
      data-editor-editordata-value={}>
  <h1>New article</h1>
  <%= render 'form', article: @article %>

  <div>
    <%= link_to "Back to articles", articles_path %>
  </div>
</div>

The edit.html.erb is very similar except we pass in the existing body value into the editordata.

<div data-controller="editor" 
      data-editor-editordata-value="<%= @article.body %>"
      data-editor-id-value="<%= @article.id %>">
  <h1>Edit article</h1>
  <%= render 'form', article: @article %>

  <div>
    <%= link_to "Back to articles", articles_path %>
  </div>
</div>

We need to update the form.html.erb partial. Here we are adding a div with an id with editorjs. Then adding the hidden field of body with the body data attribute. It will be updated when the editorjs changes.

<%= form_with(model: article) do |form| %>
  ...

  <div id="editorjs">
  </div>

  <div>
    <%= form.hidden_field :body, data: { editor_target: "body" } %>
  </div>

  ...
<% end %>

Lastly we will update the show.html.erb. The key item here is the data attribute of readonly set to true. This will have the editor change to read only mode and so no updates can be made. There are some ruby gems that can be used for this, but now that the attribute has been added to the latest version of editorjs we can just use this to render the body.

<div data-controller="editor" 
      data-editor-readonly-value=true
      data-editor-editordata-value="<%= @article.body %>">
  <%= render @article %>
</div>
...