Using QuillJS with Rails 6

Published 3rd May 2020 at 04:45pm UTC (Last Updated 19th June 2020 at 06:30pm UTC)

As alluded to in my previous post, I've run into a few difficulties since writing my last technical piece. I recently upgraded the version of Ruby on Rails on the site to version 6. Obviously, it's good practice to keep your application dependencies up-to-date so upgrading provided me with an excuse to do this but in all honesty, Rails 6's major selling point for me was its support for ActionText. At last, I could add a proper WYSIWYG to my app instead of relying on my old Markdown editor!


Now of course, it's always been possible to add these to rails projects, they just weren't supported natively until now. On top of that, my own hangups about the security and accessibility issues involved with making javascript a hard requirement had made me reluctant to add a WYSIWYG editor in the past. I've since been dragged kicking and screaming over to the dark side when it comes to javascript so I figure, what the hell?


So I set up the ActionText editor...which is essentially the Trix editor with a dependency on the new ActionText::RichText type.


Above: the Trix logo

It seemed like a good idea at first...


....that is, until I tried to use inline <code> tags.


You see, these don't really exist in Trix. You can add <pre> tags for code blocks but good luck writing...well writing sentences like this one in a technical article with just those!


The Trix editor also only seems to support one header type (<h1>) so all text must either be this size ("normal") or


this size ("H1")


which, when you take into account the effect header tags can have on SEO, is....I really can't think of a nice thing to say about this decision so I won't.


Cant Even GIF - Cant Even GIFsI think Annalise Keating would agree with me on this if she were real


It also doesn't have an underline button!


Its support for image uploads was kind of nice. There didn't seem to be a way to resize images of course and the captions were a little awkward to style with CSS. But I mean, they technically worked so that was something.


I tried to raise a PR to add a button for the <code> tag to the editor and the bloody thing worked locally. Unfortunately, it was a bit of a hack, a) because my coffeescript knowledge is a bit iffy to say the least and b) because it fundamentally changed the way in which the existing code button behaved and broke tests that relied on the text editor being able to delete inline code in a very specific way.


After weeks of turning the problem over in my head, I was forced to abandon the Trix editor altogether.


But now that I'd seen the light, I knew that there would be no going back to the old Markdown editor. I'd have to look for a more suitable replacement.


I tried to weigh my options. I figure that for my purposes, something simple and easy to customise was best. I'd had some experience with CKEditor in the past but didn't exactly have fond memories of it. I recall its toolbar being slow to load and a little unwieldy. Never really worked out how to persist images in that thing (though I'm sure it's possible).


I also briefly considered ProseMirror. I'd run into this text editor before whilst working at the Royal Pharmaceutical Society, home to the most dysfunctional engineering team I've ever seen. As I'm currently suing RPS (who, to be clear simply use ProseMirror in production as oppose to developing or maintaining the project itself), I'll admit that my thoughts regarding that editor could be a little biased. That being said, I wasn't a fan of the writing style used in the official documentation and the tool struck me as unnecessarily complicated for my needs.


Eventually, I stumbled upon Quill and it just seemed to do exactly what I needed.



Why Quill?


Screenshot of the Quill rich text editor


Now by default, Quill doesn't necessarily give you the buttons that I was after (the inline <code> tag being the most obvious of these). However these are fairly trivial to add and more generally it's actually quite easy to customise the toolbar beyond that.


As well as this, support for resizable images can be added with the use of existing javascript packages (like this one which is essentially a fork of the quill-image-resize module). There's a reasonably active community around the product which helps when it comes to more complicated features.



How do you use it?


So that's all very well and good but how does one even go about adding Quill to a Rails 6 project?


#1 - Define your model


First, you'll need to create a model with at least one string or text field. For example, you could generate an Article model with:

rails g model Article title:string body:text


In this scenario, the body field will be used by the Quill editor.


Once you're happy with the new migration file, run rails db:migrate and move on to the next step.


#2 - Add Quill to your views


Next, create the controller and views for the model:

rails g controller articles


Setting up the controller should be straightforward if you've built a rails application before. If you're unfamiliar, this post on the Ruby on Rails website should walk you through it. If the concept of CRUD in an application is also new to you, you may want to read through Nancy Do's article about this as well.


Typically when you're creating a CRUD app in rails, you'll want to create a partial view for a form so that it can be reused by both the new.html.erb and edit.html.erb views generated.

touch app/views/articles/_form.html.erb


In this file, add the following code:

<%= form_for @article do |f| %>
  <fieldset class="row">
    <div>
      <%= f.label :title, "Title" %>
    </div>
    <div>
      <%= f.text_field :title %>
    </div>
  </fieldset>
  <fieldset class="row">
    <div>
      <%= f.label :body, "Body" %>
    </div>
    <div>
      <%= f.hidden_field :body, class: "article-content" %>
      <div id="editor-container" style="height: 30rem; width: 100%;">
        <%= raw(@article.body) %>
      </div>
    </div>
  </fieldset>

  <fieldset class="row">
    <div>
      <%= f.button "Submit", class: "button" %>
    </div>
  </fieldset>
<% end %>

Most of the work is done in this section:

<div>
  <%= f.hidden_field :body, class: "article-content" %>
  <div id="editor-container" style="height: 30rem; width: 100%;">
    <%= raw(@article.body) %>
  </div>
</div>

The hidden field will add the formatted text input into the editor as a HTML string under the article's body attribute. This value will form part of the POST request sent by the form even though it isn't visible in the browser. The class name used here doesn't matter too much but it will be used in a later step so make a note of the name given.


The text editor itself will appear in the #editor-container div tag. Be sure to make a note of the ID used here as well.


As this partial will appear in the edit view, the editor container will need to display the existing contents of the article when the page is loaded. This is why a HTML escaped version of the @article.body string is nested within the #editor-container div tag.

To refer to the partial in the new and edit views, add the following tag:

<%= render partial: "articles/form" %>


Before we move on to the next step, it's also important to ensure that you've added a CSRF meta tag to your views if this hasn't been done already. This will protect your application against cross-site request forgery by adding a digital signature to the page that can be used to verify requests sent to the server have come from a user rather than a malicious application.

These are typically added to the <head> tag within the app/views/layouts/application.html.erb file (or in the corresponding partial file if these are being used):

<%= csrf_meta_tags %>


#3 - Setup ActiveStorage


Now if you want to be able to write articles with embedded images in your text editor you'll need to use ActiveStorage, which has been available since Rails 5.2. To set this up, run:

rails active_storage:install
rails db:migrate

This will create the active_storage_blobs and active_storage_attachments tables in your database which is where images will be stored as values in their BLOB columns.


Once you've done this, you should also create a config/storage.yml file to store the credentials for the different storage services that can be used by your application. An example setup for an app that uses the Google Cloud Platform in production would be:

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

google:
  service: GCS
  credentials:
    type: <%= Rails.application.credentials.dig(:gcs, :type) %>
    project_id: <%= Rails.application.credentials.dig(:gcs, :project_id) %>
    private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
    private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
    client_email: <%= Rails.application.credentials.dig(:gcs, :client_email) %>
    client_id: <%= Rails.application.credentials.dig(:gcs, :client_id) %>
    auth_uri: <%= Rails.application.credentials.dig(:gcs, :auth_uri) %>
    token_uri: <%= Rails.application.credentials.dig(:gcs, :token_uri) %>
    auth_provider_x509_cert_url: <%= Rails.application.credentials.dig(:gcs, :auth_provider_x509_cert_url) %>
    client_x509_cert_url: <%= Rails.application.credentials.dig(:gcs, :client_x509_cert_url) %>
  project: "project_name"
  bucket: <%= Rails.application.credentials.dig(:gcs, :bucket) %>


Note the use of the variables defined in the config/credentials.yml.enc file. More detail about the credentials file's usage can be found in the official Rails guide. For now it's important to know that it can be modified with the command below:

EDITOR=vim bin/rails credentials:edit

If you're using GCP, the images uploaded via the text editor in production will be stored in a storage bucket which can be associated with a service account. This Stack Overflow answer goes into detail about how to obtain the credentials for a given service account in GCP but in short, you will need to generate a key for the account and download its credentials as a JSON file. These credentials must be downloaded immediately and stored securely as there isn't an option to download service account credentials a second time. Of course once you've downloaded this file, its contents can then be assigned to environment variables in config/credentials.yml.enc.

e.g.,

gcs:
  type: service_account
  project_id: project_name
  private_key_id: 2f6d5255889009da25ba6bf2486d6b7e2bb5aaef
  private_key: "-----BEGIN PRIVATE KEY-----\nexampleKey\n-----END PRIVATE KEY-----\n"
  client_email: 00000001-compute@developer.gserviceaccount.com
  client_id: 1111111
  auth_uri: https://accounts.google.com/o/oauth2/auth
  token_uri: https://oauth2.googleapis.com/token
  auth_provider_x509_cert_url: https://www.googleapis.com/oauth2/v1/certs
  client_x509_cert_url: https://www.googleapis.com/robot/v1/metadata/x509/00000001-compute%40developer.gserviceaccount.com
  bucket: project_name_bucket

More information on how to add other service providers (e.g., AWS and Azure) can also be found in the official ActiveStorage documentation.


You will then need to refer to the ActiveStorage service you intend to use in your environment configuration. In config/environment/test.rb and config/environment/development.rb you should store images locally:

Rails.application.configure do
  ...
  config.active_storage.service = :local
  ...
end

But in production (config/environment/production.rb), you should use your cloud provider of choice. Here I've used GCP:

Rails.application.configure do
  ...
  config.active_storage.service = :google
  ...
end


#4 - Add Quill to your javascript code


Hopefully if you're using Rails 6, you'll already have Webpacker set up in your app. If not, this blog post by Gaurav Tiwari goes into it a little and may be worth a read.


To use QuillJS in your app, it will need to be added as a dependency in your package.json file:

...
  "dependencies": {
    "@babel/preset-react": "^7.7.4",
    "@rails/activestorage": "^6.0.2",
    "@rails/webpacker": "4.2.2",
    "@taoqf/quill-image-resize-module": "^3.0.1",
    "quill": "^1.3.7",
    "quill-image-drop-module": "^1.0.3",
    "svg-inline-loader": "0.8.2"
  },
...


Note, I use npm instead of yarn for package management because I'm not convinced it's compatible with Heroku (and even if it were, its use feels like duplication to me). If you're using yarn, these dependencies will need to go in the yarn.lock file via the yarn add command (described here) instead.


To ensure that you're able to import packages added to the node_modules folder, add the following to the config/initializers/assets.rb file:

Rails.application.config.assets.paths << Rails.root.join('node_modules')


I'd suggest adding a separate file for Quill configuration in the javascript/packs folder:

touch app/javascript/packs/quill-editor.js


In this file add the following imports...

import { DirectUpload } from "@rails/activestorage"
import ImageResize from "@taoqf/quill-image-resize-module/image-resize.min";
import Quill from 'quill/quill';
export default Quill;


...and register the ImageResize module from @taoqf's fork:

Quill.register('modules/imageResize', ImageResize);

Note, I suggest using this fork because in February 2020, the original ImageResize module was quite broken and the fix for this issue has yet to be merged three months later.


The basic configuration for a Quill toolbar, as described on the official site might be something like this:

document.addEventListener("DOMContentLoaded", function (event) {
  var quill = new Quill('#editor-container', {
    modules: {
      toolbar: [
        [{ header: [1, 2, false] }],
        ['bold', 'italic', 'underline'],
        ['image', 'code-block']
      ]
    },
    placeholder: 'Compose an epic...',
    theme: 'snow'
  });

  document.querySelector('form').onsubmit = function () {
    var body = document.querySelector('input[class=article-content]');
    body.value = quill.root.innerHTML
  };
});


Here the toolbar is a two-dimensional array of button groups. Drop-down buttons are defined as objects with values that are specific to the button type.

Note also, the use of the addEventListener method called on the document in the first line of the code block. This is needed to ensure that your text editor code is only run after the DOM has finished loading and not including it can produce a Quill is not defined error in the console like the one mentioned in this SO question. More info on the DOMContentLoaded event can be found in the MDN web docs.


Of course this'll only get you a pretty basic toolbar: bold, italic, underline, code blocks, image uploads and H1 and H2 tags.

Basic quill editor


Personally, I still wanted a few more buttons. My configuration wound up looking more like this:

document.addEventListener("DOMContentLoaded", function (event) {
  var quill = new Quill('#editor-container', {
    modules: {
      toolbar: [
        [{ header: [1, 2, 3, 4, 5, 6, false] }],
        [{ color: [] }],
        [{ size: [] }],
        [
          'bold', 'italic', 'underline', 'strike',
          { 'script': 'super'},
          { 'script': 'sub' },
          'code', 'link'
        ],
        ['blockquote', 'code-block', 'image'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        [{ align: ['center', 'right', 'justify', false] }],
        [{ indent: '-1'}, { indent: '+1' }]


      ],
      imageResize: {
        displaySize: true,
        displayStyles: {
          backgroundColor: 'black',
          border: 'none',
          color: 'white'
        },
        modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
      }
    },
    value: document.querySelector('input[class=rich-text-content]').value,
    theme: 'snow'
  });

  document.querySelector('form').onsubmit = function () {
    var body = document.querySelector('input[class=article-content]');
    body.value = quill.root.innerHTML
  };

  // More on this in a bit!
  quill.getModule('toolbar').addHandler('image', () => {
    importImage(quill);
  });
});

...


Now I have more header sizes, font colours and sizes, text alignment, inline code, subscript and superscript, hyperlinks and lists. As mentioned earlier, document.querySelector('form').onsubmit function towards the end sets the value of the hidden .article-content field based on the inner HTML in the quill editor

If you ignore the fact that the code block and inline code buttons share the same icon, it works pretty well (I was too lazy to change this in my own example but I'll explain how to fix it in step #7).


kohrVid editor


Now in the previous code block you may have spotted this code:

quill.getModule('toolbar').addHandler('image', () => {
  importImage(quill);
});


This code is used to persist images uploaded with the quill editor into a (usually external) file storage essentially by calling a new importImage() function defined below:

var importImage = function (textEditor) {
  const input = document.createElement('input');
  input.setAttribute('type', 'file');
  input.click();

  input.onchange = () => {
    const file = input.files[0];

    // Ensure only images are uploaded
    if (/^image\//.test(file.type)) {
      uploadImage(textEditor, file);
    } else {
      alert('Only images allowed');
    }
  };
};


This function essentially creates a temporary file upload field that can be used to import an image and check that it is of the correct file type before calling the uploadImage() function. When called, this function takes the quill variable in our earlier code as a parameter (textEditor) and later passes said variable to the uploadImage() function along with the new file.


You may also want to set a maximum file size for image uploads. If so, add a constant like the following just below the package imports closer to the top of the file:

const MAX_FILE_SIZE = 1000000


And add a nested if statement to check this in the importImage() function:

if (file.size > MAX_FILE_SIZE) {
 alert("Only support attachment files upto size 1MB!")
 return
}

Here, we're limiting images stored in our database to about 1MB which is great for photos but perhaps a little tricky for GIFs. YMMV.


The uploadImage() function looks like this:

var uploadImage = function (textEditor, file) {
  var fd = new FormData();
  fd.append('blob', file);

  var upload = new DirectUpload(file, '/rails/active_storage/direct_uploads')
  upload.create((error, blob) => {
    if (error) {
      console.log(error)
    } else {
      insertImage(
        textEditor,
        `/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`
      );
    }
  });
};


This method will take the file selected by the user and send it in the payload of a POST request to the /rails/active_storage_direct_uploads endpoint provided by ActiveStorage. Provided the request doesn't return an error, the image should be saved as a blob within the active_storage_blobs table of the database and the create method used should return the new row as a response (blob).


The filename and signed_id of this row can then be used to construct the new file's URL which is used (again, along with the quill textEditor) as a parameter in the insertImage() function:

var insertImage = function (textEditor, fileUrl) {
  const range = textEditor.getSelection();
  textEditor.insertEmbed(range.index, 'image', fileUrl);
};


This function is used to insert the image into the inner HTML of the text editor, rendering the image on the page.


Once you're happy with your script, it is important that you remember to import the file in app/javascript/packs/application.js:

require("@rails/activestorage").start()
import "./quill-editor.js"


(And obviously make sure that this is included in the <head> tag in the app/views/layouts/application.html.erb file:

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

)


At first, this won't look quite right because of the way in which Rails 6 handles SVG files. It actually requires the addition of a separate loader to package.json (or yarn.lock). In this case, I've used the svg-inline-loader package which was added as a dependency a little earlier:

  ...
  "dependencies": {
    ...
    "svg-inline-loader": "0.8.2"
  }
...


This, along with the file-loader package (which is already included as a Webpacker dependency), should then be referred to in the config/webpack/environment.js file like so:

const { environment } = require('@rails/webpacker')

const fileLoader = environment.loaders.get('file')
fileLoader.exclude = /node_modules[\\/]quill/
...

const svgLoader = {
  test: /\.svg$/,
  loader: 'svg-inline-loader'
}

environment.loaders.prepend('svg', svgLoader)

module.exports = environment


The file loader will allow you to resolve the SVG paths for the icons in the text editor into their respective files. Excluding the node_modules/quill folder as is done in the example above is also needed to get the icons to display correctly. As the order in which loaders are added is important, the SVG loader must then be prepended, ensuring that it gets appended before the file loader does.



So that was a lot of javascript for one day.

Here's an old photo of a cat!


QT the cat in a bag from etefy, a company with a truly ridiculous(!) name that went bust and fucked over its workers a few years ago. As you can see, this is QT's homage to the Maschinenmensch


#5 - Style the editor


Now you'll want to make sure that the Quill core and Snow styles are added either to the stylesheets in Sprockets or in Webpacker. Personally, I chose to add this to sprockets because I ran into issues with Heroku when these were added to Webpacker. I did this by adding the following to the app/assets/stylesheets/application.css file:

*= require_self
*= require 'quill/dist/quill.core.css'
*= require 'quill/dist/quill.snow.css'


This file is of course included in your application by referring to it in the head of the app/views/layouts/application.html.erb file:

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>


If you choose to go down the Webpacker route for CSS, some information on how to import the quill styles can be found in the Webpacker repo here.


I must admit though that my styling is something of a hot mess, no thanks in part to issues with asset versioning in Heroku. I'd suggest that you experiment with this in your own setup.


#6 - Add environment configuration


I've covered most of the configuration that you would need to set this up in previous sections already. However it's still important that you have a Content Security Policy with sensible settings. Something like the configuration below should ensure that your application is able to serve the right assets to your users without the risk of their being hijacked by browser extensions and the like:

Rails.application.config.content_security_policy do |p|
  p.default_src :self, :https, "http://localhost:3000"
  p.font_src    :self, :https, :data

  p.img_src     :self, :https, :data, :blob, "http://domain-name.com",
    "http://www.domain-name.com",
    "http://cloud-hostname.storage.googleapis.com"

  p.object_src  :none
	

  p.script_src  :self, :https, "http://domain-name.com", "http://www.domain-name.com",
    "http://localhost:3000"

  p.connect_src :self, :https, "http://localhost:3035",
    "ws://localhost:3035" if Rails.env.development?

  p.style_src   :self, :https, :unsafe_inline

  # Specify URI for violation reports
  p.report_uri  "/csp_reports"
end


At this point, you may start to see errors like the following:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://storage.googleapis.com/cloud-hostname/s%2F2vkzje. (Reason: CORS request did not succeed).

To remove this, you will need to configure your CORS settings which are specific to your cloud provider. if Google is used this, needs to be set up in the GCP console. Full instructions on how to do this can be found here but essentially, you will need to create a cors.json file like the following...

[
  {
    "origin": ["https://www.domain-name.com", "https://domain-name.herokuapp.com"],
    "method": ["PUT"],
    "responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
    "maxAgeSeconds": 3600
  }
]

...where the origin list should contain your domain name and any other public-facing URLs associated with your app (e.g., you Heroku URL or staging URL, &c.). You should then log into the console and whilst in the cloud shell you can upload the file...


kohrVid cloud shell


...and run the following command:

gsutil cors set cors.json gs://project_name_bucket


This will allow your site to host images stored in your project's bucket.


#7 - Optional Steps


Overriding the DirectUploadsController

If you plan to override any of the default behaviour in the ActiveStorage endpoint referred to in step 3 (the endpoint for which is obviously used in step 4), the easiest way is to create a new controller that inherits from the ActiveStorage::DirectUploadsController class. This Stack Overflow question and answer go into more detail but essentially, you would need to create a controller that looks like this:

class DirectUploadsController < ActiveStorage::DirectUploadsController
  protect_from_forgery with: :null_session,
    if: Proc.new { |c| c.request.format == 'application/json' }

  def create
    # Override the create method here
  end

  private

  def blob_args
    params.require(:blob).permit(
      :filename,
      :byte_size,
      :checksum,
      :content_type,
      :metadata
    ).to_h.symbolize_keys
  end
end


And add this controller as a resource to config/routes.rb:

Rails.application.routes.draw do
  ...
  resources :direct_uploads, only: [:create]
end


Overriding SVG icons

I mentioned earlier that one of the drawbacks of using Quill is that, by default, the inline code and code-block buttons use the same icon. If you're so inclined, you can overwrite this or any of the other buttons with the SVG code for a new icon design.


So for example, I could replace the default icon with an SVG from the Bootstrap library by adding the following code snippet to the javascript code:

var icons = Quill.import('ui/icons');

icons['code'] = `<svg class="bi bi-code" width="1.5em" height="1.5em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
  <path fill-rule="evenodd" d="M5.854 4.146a.5.5 0 010 .708L2.707 8l3.147 3.146a.5.5 0 01-.708.708l-3.5-3.5a.5.5 0 010-.708l3.5-3.5a.5.5 0 01.708 0zm4.292 0a.5.5 0 000 .708L13.293 8l-3.147 3.146a.5.5 0 00.708.708l3.5-3.5a.5.5 0 000-.708l-3.5-3.5a.5.5 0 00-.708 0z" clip-rule="evenodd"/>
</svg>`;

...

Essentially, assigning the inline code icon (icons['code']) to an inline SVG object. Note, this must be defined before the Quill editor is instantiated within your code in order to work.


With the correct styling, <i> tags can be used instead. This Github comment provides an example to that effect.



Summary


So I've told you why I chose to use the Quill editor on my site and how it compares with ActionText. I've also explained how I added this to my Rails application and gone over some of the steps needed in a production environment. Lastly I've taken a brief look at optional steps that can be taken to further customise the editor in your own applications.


Should you choose to use QuillJS in Rails 6, I hope that this article proves helpful and at the very least saves some of the time that might have otherwise been spent googling the process.



Special thanks


Special thanks goes to Stas and the anonymous poster in the comments below who both pointed out a mistake I'd overlooked in the original version of this post.



External Resources


Other than the links I've already mentioned in this post, the following links could prove helpful to readers:


Post a comment

Posted by Anonymous on 20th May 2020 at 09:43:49 UTC

Thanks a lot! It was a great help.

Chris

Posted by Jess on 26th May 2020 at 12:42:38 UTC

Glad you liked the post, Chris! =]

Posted by Anonymous on 3rd June 2020 at 12:44:01 UTC

Hi Jess,


Thx for an amazing article! This is very nice of you to show how to make webpack properly pack quill js editor to Rails app!


However, for some reason, I still have this:



Apparently, instead of proper rendering svg, there's just a load of paths to icons, like this:


<span class="ql-picker-label" tabindex="0" role="button" aria-expanded="false" aria-controls="ql-picker-options-0">module.exports = __webpack_public_path__ + "media/icons/dropdown-8bd433f1.svg";</span>


In my package.json, I do have "svg-inline-loader": "^0.8.2", while my environment.js is:


const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide', new webpack.ProvidePlugin({
 $: 'jquery/src/jquery',
 jQuery: 'jquery/src/jquery'
}))
const svgLoader = {
 test: /\.svg$/,
 loader: 'svg-inline-loader'
}
environment.loaders.prepend('svg', svgLoader)
module.exports = environment

Did I probably missed something from your article?


This would be so great if you could help me with this.

Posted by Stas on 3rd June 2020 at 12:48:10 UTC(last modified on 3rd June 2020 at 12:48:51 UTC)

Oops, sry for a crazy markup, as well as for not signing up before posting!

Best,

Stas.

Posted by Jess on 4th June 2020 at 10:28:18 UTC

Thank you for the kind words, Stas. I’m really sorry you ran into trouble following the post! I don’t mention it explicitly but you actually have to add the file loader to config/environment.js. Apologies for leaving that out - what you're describing is an issue I ran into myself so I'm not sure how I forgot. Though the file-loader package is included as a Webpacker dependency, you have to add it to your configuration like so in order to import inline SVGs:

const { environment } = require('@rails/webpacker')

const fileLoader = environment.loaders.get('file')
fileLoader.exclude = /node_modules[\\/]quill/
...
const svgLoader = {
 test: /\.svg$/,
 loader: 'svg-inline-loader'
}
...

The key line here is where the fileLoader constant is defined. The file loader will allow you to resolve the paths that you can see in your screenshot into URLs for the relevant SVG files. Excluding the node_modules/quill folder as is done in the following line is also needed to get the icons to display correctly.


I hope that helps and apologies for not mentioning this earlier. I'll try to update the post when I have a bit more time but do let me know if you run into any issues in the meantime.

Posted by Stas on 6th June 2020 at 11:40:53 UTC

Jess. You're simply the best. All worked out, big thanks for your kind efforts!

And keep going with your blog, it's so valuable and awesomely written!

Peace,

Stas.

Posted by Anonymous on 19th June 2020 at 09:32:53 UTC

Thanks for this! I don't think I would've been able to do it without your help...


Note: Like others, I was beating my head against a wall trying to make the SVG icons work, and spent half the day googling solutions. It wasn't until I decided to come back and look through these comments that I noticed someone else having the same problem, and that you mentioned that the environment.js file was missing the part about the file loader:


const fileLoader = environment.loaders.get('file')
fileLoader.exclude = /node_modules[\\/]quill/


That turned out to be what made the difference. If you could add that back into your example, I'm sure others would be very grateful.


Thanks again!



Posted by Jess on 19th June 2020 at 18:37:03 UTC

No problem. I'd been meaning to add a note about the file loader over the last few weekends but something always came up. I've updated the post now though so hopefully that won't trip up anyone else in the future.


Thank you for reading my blog and apologies for not making the changes sooner!

Posted by Anonymous on 19th June 2020 at 19:24:16 UTC

No worries! Like you, I was initially excited about Action Text, but it didn't cut it for a number of reasons, so this post is incredibly helpful and I'm very appreciative.

Posted by Anonymous on 9th March 2021 at 22:36:49 UTC

Thanks for this post! It's work locally but I'm having a problem with Heroku


quill.js:1336 Uncaught TypeError: r is not a function

  at Object.<anonymous> (quill.js:1336)

  at n (quill.js:66)

  at Object.<anonymous> (quill.js:7494)

  at n (quill.js:66)

  at Object.<anonymous> (quill.js:13242)

  at n (quill.js:66)

  at Object.<anonymous> (quill.js:15522)

  at n (quill.js:66)

  at quill.js:187

  at a (quill.js:14)




What could it be?

Posted by Jess on 18th April 2021 at 14:54:47 UTC

Apologies for the delayed response - March was a difficult month for personal reasons. I've never actually run into the problem that you describe but something like it seems common with the Uglifier gem. Do you know which version you're using by any chance? This Stack Overflow answer suggests that there might have been a problem with v4.1. I'm using v4.2 and haven't seen that error before

Posted by Anonymous on 6th August 2021 at 09:02:03 UTC

Can you provide the source code in this post? Thanks!

Posted by Aayush Shrestha on 17th February 2022 at 18:50:56 UTC

I don't get the text field for the body part. What did I do wrong?

Posted by Jess on 17th February 2022 at 19:25:46 UTC

Hey Aayush. Are you sure that the ID that you've given the text editor in your layout (i.e., in _form.html.haml) is the same as the one that you've used to create the new Quill instance in your javascript code?


(so in this section of your code:

var quill = new Quill('#editor-container', {
...

)


If they're different, that could explain the issue. If not then I'll need a little more info - perhaps if you share some of the code you're working on, I might be able to help

Posted by Aayush Shrestha on 18th February 2022 at 14:05:18 UTC(last modified on 18th February 2022 at 14:07:28 UTC)

I checked and I think they are the same.

This is my _form.html.erb file





<%= form_for @article do |f| %>
  <fieldset class="row">
    <div>
      <%= f.label :title, "Title" %>
    </div>
    <div>
      <%= f.text_field :title %>
    </div>
  </fieldset>
  <fieldset class="row">
    <div>
      <%= f.label :body, "Body" %>
    </div>
    <div>
      <%= f.hidden_field :body, class: "article-content" %>
      <div id="editor-container" style="height: 30rem; width: 100%;">
        <%= raw(@article.body) %>
      </div>
    </div>
  </fieldset>


  <fieldset class="row">
    <div>
      <%= f.button "Submit", class: "button" %>
    </div>
  </fieldset>
<% end %>


And this is my quill-editor.js file


import { DirectUpload } from "@rails/activestorage"
import ImageResize from "@taoqf/quill-image-resize-module/image-resize.min";
import Quill from 'quill/quill';
export default Quill;
Quill.register('modules/imageResize', ImageResize);


const MAX_FILE_SIZE = 1000000




document.addEventListener("DOMContentLoaded", function (event) {
  if(!document.querySelector('input[class=article-content]')){
    return;
  }
  var quill = new Quill('#editor-container', {
    modules: {
      toolbar: [
        [{ header: [1, 2, 3, 4, 5, 6, false] }],
        [{ color: [] }],
        [{ size: [] }],
        [
          'bold', 'italic', 'underline', 'strike',
          { 'script': 'super'},
          { 'script': 'sub' },
          'code', 'link'
        ],
        ['blockquote', 'code-block', 'image'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        [{ align: ['center', 'right', 'justify', false] }],
        [{ indent: '-1'}, { indent: '+1' }]




      ],
      imageResize: {
        displaySize: true,
        displayStyles: {
          backgroundColor: 'black',
          border: 'none',
          color: 'white'
        },
        modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
      }
    },
    value: document.querySelector('input[class=article-content]').value,
    theme: 'snow'
  });


  document.querySelector('form').onsubmit = function () {
    var body = document.querySelector('input[class=article-content]');
    body.value = quill.root.innerHTML
  };


  // More on this in a bit!
  quill.getModule('toolbar').addHandler('image', () => {
    importImage(quill);
  });
});


var importImage = function (textEditor) {
  const input = document.createElement('input');
  input.setAttribute('type', 'file');
  input.click();


  input.onchange = () => {
    const file = input.files[0];


    // Ensure only images are uploaded
    if (/^image\//.test(file.type)) {
      if (file.size > MAX_FILE_SIZE) {
        alert("Only support attachment files upto size 1MB!")
        return
      }
      uploadImage(textEditor, file);
    } else {
      alert('Only images allowed');
    }
  };
};


var uploadImage = function (textEditor, file) {
  var fd = new FormData();
  fd.append('blob', file);


  var upload = new DirectUpload(file, '/rails/active_storage/direct_uploads')
  upload.create((error, blob) => {
    if (error) {
      console.log(error)
    } else {
      insertImage(
        textEditor,
        `/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`
      );
    }
  });
};


var insertImage = function (textEditor, fileUrl) {
  const range = textEditor.getSelection();
  textEditor.insertEmbed(range.index, 'image', fileUrl);
};


I have put the project in github too https://github.com/MadhurShrestha/kohrvid_quilljs.

Just a demo project, thanks for helping!!

Posted by Jess on 18th February 2022 at 18:06:19 UTC

That's odd. I cloned your repo a little earlier and noticed that the editor does appear when I refresh page, suggesting an issue with turbolinks. I guess it's deprecated now which could explain it but it's been known to cause problems with older versions of Rails. What happens if you comment out this line in your code?

Posted by Aayush Shrestha on 19th February 2022 at 17:20:54 UTC

Yeah, it worked after I commented out the turbolinks code in application.js

Thanks a ton, I am going to remove the trix editor from my other project too now.

Posted by Jess on 19th February 2022 at 18:09:14 UTC

Glad that helped. Best of luck!

Posted by Aayush Shrestha on 23rd February 2022 at 18:54:55 UTC

Hi Again, Jess. I was trying to implement the editor into my project. So wanted to play around with your blog/portfolio. When I clone your project, I get this

To be clear, do I do this in the terminal ?

Posted by Jess on 23rd February 2022 at 21:37:26 UTC

Hm...that error actually looks more like it's related to the postrges database. Do you have postgres installed? If so, you may need to change the configuration slightly. This Stack Overflow answer might help but drop me an email if it doesn't:

https://stackoverflow.com/a/18664239