Skip to main content
37 Common Features:

Using the Trix Editor plus File Upload Attachments

Episode 209 · October 3, 2017

Learn how to use the Trix editor for editing wysiwyg content and upload attachments like images using Javascript and Shrine

Gems Javascript


What's up guys? This episode we're going to be talking about the basecamp Trix editor, which is a WYSIWYG editor. It isn't markdown or anything like that, it generates html and we're going to talk about implementing this and adding file uploads to it in your rails app. This is really pretty simple to implement, and it's especially simple if you're not looking to do file uploads, all of this is really really easy to just drop in, it is one of the easiest editors, and it has a really good API, so if you want to add more buttons to do different features, that are custom to your applications, you can write all of that with some really easy to use JavaScript, and they have good examples of how you implement things like using blockquotes and code and all that stuff, you can see source of all of these and it has undo and redo functionality, which is really handy.

So we're going to be implementing this in a very simple application that I have right here, we have posts, they have a title and a body that we want to be able to format, and so we're going to be implementing Trix, and I'm going to leave this text area visible for you. Normally, this becomes a hidden field instead, and Trix will become the UI, but in this case we're going to use this text area, so you can actually watch Trix at work, and you can see what it's generating as the html gets edited. So it's pretty cool, and then we're going to go add photo uploading to it, but we're going to do that in a little bit because we want to just get Trix up and running first.

Now of course, this comes with a gem, called Trix. All you have to do is install that, run bundle and start up your rails server again, and then we want to go into our


//*= require trix 


//= require trix

Then, if we go to our post form, we can go to the area where we have that body, and then it's as simple as adding:


<div class="field">
    <%= form.label :body %>
    <%= form.text_area :body, id: post_body %> 
    <trix-editor input>

it will attach to that and then use that content that's in there, so let's take a look at that and we'll close off that tab, and refresh our page now, and that is as simple as it is to install trix. Now, it's going to take a little bit to compile that JavaScript and everything and scss again, but now we have trix here, and if we type something. "Hello world", you'll see it visibly right here, so that's really cool, so let's expand this a little bit, so you can see that, if we go in bold "Hello world", you'll see that it inserts a **strong** tag around hello world, you can go do the same thing for that, and you can go edit a bunch of stuff, and you'll see that this will try to generate the best version of the html based upon what the user has done, and then you can hit the undo and redo buttons to go back and forth and see your history, that's pretty cool and it works really simply, that's all you have to do to get it up and running, and normally, of course you're going to want to change this from a text area to just a hidden field, and you'll be good to go, so we know that it's updating the content of that hidden field now, so if we were to type "Hello World" here, and let's make "world" bold and just have a title of "Test", we then need to go into our **show.html.erb**, now that we have content here, that's html, we're going to want to sanitize this, don't ever use html safe or raw here because if someone put JavaScript in there maliciously or any other tags that you don't want, then they will be able to attack other users on your site, so always use sanitize and make sure that you sanitize that content. Now if we create this post, you'll see that "world" is bold and all of that is being rendered out exactly like we saw in the text editor, so that's really cool, and it's generating the divs and the strong tags and all of that just like we saw in that text area, everything works, and you want to just make sure that you always use sanitize here, just for safety. Now, this works great so far, but we don't have file uploads and it's often the case where you want to be able to upload images as part of your content, and so we're going to be working on that next. Now, we have to first set up file uploading in our application before we can even use that for trix just so that we have file uploading working, and then we can focus on the JavaScript the trix is going to require for this. So what I'm going to do, is I'm going to generate a new model, let's also generate a scaffold with this so we have the create action, we're going to have photos and photos are going to have image data on them, which will be a text column, we're going to use shrine for uploading, so this is going to generate that model, we'll be able to access all of that, and then we'll run `rails db:migrate`, and then if we go into our photos controller

def photo_params 


class Photo < ApplicationRecord 
    include ImageUploader[ :image ]


class ImageUploader < Shrine 

Last but not least we need to make sure we also install Shrine, so we'll run bundle to install the gem, and then before we restart our rails server, let's create a config/initializers/shrine.rb, and I'm going to paste in some configuration here, basically we need to tell Shrine where to store our files, and I'm going to have them just stored on the file system here, and turn on the Active Record plug in so that we have access to that, so once we do that, we can restart the rails server, it will load all that config, and then we can go to our photos form, and if we modify this instead of image data for these things, we can have it as a file field so that we can test this out and just make sure that our file uploads are working before we go and create those with trix.

So because we've created the scaffold for this, we now have photos/new, and we have the image field that we can upload from here, we can create a photo, and we're going to get that image data and the photo should be saved, and if we go to the photos/show.html.erb, we can then put in a little tag

<%= image_tag @photo.image_url %> 

And there we go. So our photos are currently uploading correctly, so we know all of that works, and it's good to confirm this before we go into trix just to make sure that we're not dealing with photo uploading problems at the same time as you're trying to mess with the JavaScript for trix. So now we want to go back to this, and make this form so that you can drag and drop a photo onto it, and then it will upload that photo asynchronously and then show the preview inside of your content here.

So as a quick side note I want to mention here that when we edit a post, trix is automatically loaded that content from before and put it into the rich text editor and we can edit that stuff automatically like we would normally would, so if we update posts, it's going to edit just like you would expect, and that all comes for free because we've set the input attribute on the trix editor to match the id of the hidden field, so it's able to look at that for the content, it looks at the value, rails is automatically going to populate the value because we're using the hidden field tag and we've set the model in our form, so all of that is the standard rails stuff, but trix is smart enough to say: Well, if you tell me what form field to read and write from, I will go ahead and do that and preload all the content, and then any updates you make we will send it to that value on the hidden field as well, so that all works out of the box, and is really really easy to use.

Now, one thing I do find useful is to change this back to a text area when we're working on the trix upload JavaScript, because if anything goes wrong, you will see that the text area value has not changed and that is important to see in case anything went wrong with the file uploading, so let's take a look at how we go implement all of that jazz.

Now it's time to start building the trix upload javascript, and the first thing that we want to do is create a file called


document.addEventListener("trix-attachment-add", function(event) {
  var attachment = event.attachment;
  if (attachment.file) {
    return uploadAttachment(attachment);

This, we don't have to put inside the tubolinks load because as soon as the page loads we'll set up this listener, which will listen to any trix attachment add events, so you could have trix add itself to other pages and whatever, this only needs to be added one time. We don't need to re add this listener every single page that happens, so we're going to have that, and then we're going to have a function up here called Function.attachment(attachment) and we're going to have this do the processing and submission to the server, and then take the result of that, the url of the image and then tell trix what the actual url is. This is really all we have to fill out, but we have to then figure out how do we get the attachment here. We can get the attachment by saying var attachment = event.attachment and then if the attachment.file exists, then we want to upload the attachment, and that's really it, and then we're going to call that and then it will have this function, which we will set up with some form data, so we need to basically submit that file, and we're going to do that using the form data object, and then take an XHR request and submit that over. Now, normally, what I would recommend is using the rails ujs library so you would want to have the rails.ajax method here and do all that normal stuff, but, in this situation, rails.ajax doesn't provide us right now at least with an easy way of adding listeners for the upload progress, which we want to use in order to tell trix how the progress is going for the file upload, so if you have a big file upload, it will display the image that you're uploading or video or whatever it is, that attachment will be displayed there in a progress bar and we want to, ideally be sending that progress information over to trix. So the rails ajax method isn't really going to do a great job for us here, and so we're going to set up an xhr request manually instead.


function uploadAttachment(attachment) {
  // Create our form data to submit
  var file = attachment.file;
  var form = new FormData;
  form.append("Content-Type", file.type);
  form.append("photo[image]", file);

Now we'll have the form data object set up so that we can create a new xhr request:

// Create our XHR request
  var xhr = new XMLHttpRequest;"POST", "/photos.json", true);
  xhr.setRequestHeader("X-CSRF-Token", Rails.csrfToken());

// Report file uploads back to Trix
  xhr.upload.onprogress = function(event) {
    var progress = event.loaded / * 100;

We can finish off our xhr upload now, but we need to define this ahead of tie so that that is handled once we fire off the request. We need to add one more function to handle progress here

  // Tell Trix what url and href to use on successful upload
  xhr.onload = function() {
    if (xhr.status === 201) {
      var data = JSON.parse(xhr.responseText);
      return attachment.setAttributes({
        url: data.image_url,
        href: data.url

If all of this is set up correctly and I didn't make any mistakes, then we should be good to go. This xhr stuff is really hard to remember, so don't feel bad about copying and pasting this stuff because I never remember it and have to reference this all the time, and that's really why rails and jQuery have those wrappers around this stuff because it's really tricky to remember the exact names or the upload on progress or whatever. All this stuff can be a little confusing to remember. Now, one last thing is that we should look at the photos controller and take a look at that create action. This is going to render json if it's successful, and so it's going to have a render show, and this is going to render the app/views/photos/show.json.jbuilder file, which in turn renders a partial app/views/photos/_photo.json.jbuilder, and this is where you're going to define the attributes and stuff that you send back, and anytime that you serialize one of these photos to json. Now, this photo url is actually set to the url of the photo object, so this would point to /photos/1/json or whatever, but not the image itself, and so we have a couple options, we can either use the url like I wrote it in here, or for better sake it's probably better for us to say json.image_url photo.image_url which will access the shrine image url, so let's change this to image url for both of those, and if we save that, and we go back to our browser, if everything works correctly, we can pull up an image and drag and drop this on to our form, and we should see that the image shows up. Now this is going to be shown as a preview just because you can do that in JavaScript without having to actually upload the file first, so don't trust the visual image that you see immediately, you want to actually trust that the href was added to your content here. So I have this set up so that we can see this a little bit more visibly, and here you can see that there's an big href that wraps a lot of content and this is actually what we want to see. We want to see /uploads/store and the jpeg name, and that means that we are posting that image url, it's been saved in the database and all of that, and you should see in your rails side of things as well, if you want to double check that, you should see that you got a post to photos.json, you should see that it inserted a photo, and then rendered out that show.json.jbuilder file, and if all of that was correct then you have successfully loaded a file and it has been added to your content, so then you can update posts and you can see your image here as well, and it automatically generates and shows the filename and the size, and so you will have that caption by default, but you can also edit that out with a little bit of JavaScript.

So to actually remove that trix preview with the caption, you can have the name and size attribute set to false, with this Trix.config.attachments.preview.caption and you are going to have all of that set up like so, and so that is going to be run, and if we do this again, we should see that now if we drag and drop a new photo on here, let's grab one, so we'll drag and drop this one, it uploads and there is no caption here, and you can write your caption if you would like and click update, and this is a giant photo and it saves our caption as we would expect. So in this situation, we have that and you have the two options for those attributes that you passed in, so for example with this url, this is the image url, and the href is actually the link that wraps that image, so when you click on the image, it will actually open up a link automatically and so if you set this href o a different url than just the image, so we've set it to the exact same thing, if you click on the image, it will load the image on it's own page, so if you had a special page that you wanted to link to for those, you can change that as well and do that however you would like if we change this photo url to format as html and you set url there, we could change this href to url, and that would point us to the photos show page for htlm, so if we try this now, we can go edit, let's delete this photo, let's drop another one in, let's put that same one we did before, this time, if we update posts, we have no caption if we didn't enter one, and if we click this, it goes to photos/6.html, which is our photo show page, so if you ever have pages like we built where we have a photo section, you can click these and then you can go see the bigger picture of that or whatever that's not inside of your content. That is how you add file uploads to trix. It's not terribly complicated, but it is more confusing than you might expect when you're diving into something like that. All of those WYSIWYG editors generally work the same way, when you do a file upload, it will trigger a callback like what we saw with the trix attachment add, and that's going to then tell you to run whatever code you want and then you report back with the successful upload and then all of that is good.

That's it for this episode, I hope you enjoyed it and I will talk to you in the next one. Peace

Transcript written by Miguel



Great vid, can this be done with the carrierwave gem?


Yep, just swap out the Shrine code on the Photos model and you're set.


how about ActiveStorage?


Should work for that too, since the JS is agnostic to any backend storage you might want to use. Direct uploads to S3 etc will work as well.


Hi Chris, I am trying to get the Direct uploads to S3 to work with trix. Can you offer some pointers/resources that may help me?




How hard is it to add trix to a selected input-field in administrate. What should be the best approach?


Really really easy. You need to add the JS and CSS to administrate's versions. Then you can create a custom field. I made one called FormattedText and just added the editor to the form.html.erb view for the field.

<div class="field-unitlabel">
<%= f.label field.attribute %>
<div class="field-unit
<%= f.hidden_field field.attribute, id: field.attribute %>
<trix-editor input="&lt;%= field.attribute %&gt;"></trix-editor>

wow that easy! Great. Thanks!


Great. Got it working after understanding that i had to add the js and scss to the actual gem. I have a hard time understanding javascript and the pipeline so I do have a question:

1) Why cant I add the code somewhere in the normal asset pipeline

2) How do I add the files in a production environment. Do I have to ssh into production to add the requires in both .js and sccs files inside the gem?


1. The admin compiles separate css and JS files. Your regular users will never have access to the admin, so it doesn't make sense to give them all your admin css and js.

2. They will be compiled automatically. Administrate already configures your app to have separate admin js and css files that are compiled via the normal asset pipeline. The asset pipeline can compile more than one file as output, but we don't generally do that so the user only has to download a minimal set of files. We only do here because sending those regular users the admin style and functionality doesn't make sense.


my local install of the administrate gem (/var/lib/gems/2.3.0/gems/administrate-0.8.1/lib/) will not move to production, and that is were I added the requires for trix.js and trix.scss. Wasn't this the right place to edit the files?


Yeah you should never edit a gem's source code. Install the administrate assets into your app using the generator and edit those to include trix.


Thanks a lot Chris!

learned something new today.

My last question:)
Should I create a new folder 'assets/javascript/admin' (because this is the route to the dashboard) and place/edit the administrate asset files there?


I don't think so, just use the administrate generator to create the administrate files. You don't need to create any folders yourself.


From what I've read trix doesn't allow any html tags in input. This is good for security but we use some custom css classes. How hard is it to add these to trix locally?


Hey Chris! That was a great episode. Special thanks for the part where you explain the details about image uploading. I've been implementing images processing for few Rails apps, nevertheless some aspects in this area felt like a black magic to me. For example, progress tracking for XHR requests was new.


Also now I know that axios has onUploadProgress callback. Neat. When you know what features to expect, it is much easier to find them.


How add post_id with Photo ?


How add association has_many :photos with post.rb after save ?


Hi! How to use jquery_ujs instead of rails-ujs to use trix gem?

xhr.setRequestHeader("X-CSRF-Token", Rails.csrfToken()); Rails. not work


You use



Hmm. So in this example the Post object has no association with Photo. Since Trix isn't handling any sort of deletion, we only add photos and never remove them. We also can't remove a Photo (or the attached image) when a Post is deleted since there's no relationship between them. And because Trix is handling the HTML for the image, I don't have an opportunity to add markup to the image to use, say, Imgix.js to handle responsive adaptations.

If I add a post-processor on the HTML (e.g., parse w/ Nokogiri and manipulate the images as needed), Trix is going to be disagreeable on edit. I could handle that on show and use fragment caching, I suppose. If I use JS to modify what I'm handing Trix, my final output is tied to the JS on a totally different view in an unrelated area of my website, and it's difficult to update down the road. Neither of these options fixes the lack of relationships, either, unfortunately.

How are others handling this in, say, a CMS for a media organization?


How did you end up handling it @coreyward:disqus ? I used a form of post-processing in the past with Refile, curious to know if you came up with a better solution?


I went for a half-baked solution. Photos belong_to Post, and I pass along the post ID when uploading to make that work. When a post is removed, so are the photos. For new posts, there’s no ID. I didn’t have time to store these separately or add a unique pre-persisting token to Post, but the logic there should work. For now a Post adopts all orphan images on save. This is fine since this isn’t end-User facing, so volume is low.

Removing photos without removing a post is more work. You need to handle the JS event Trix triggers when the photo is removed, but just mark it for removal on save instead of actually deleting it. Otherwise someone can cancel and the photo will break.

As for manipulations/transformations, I’m using Imgix to handle that. I built a the particular params I’m using into the controller that returns the image url for Trix. I’d rather that happen on render, but I didn’t have time/budget. If I need to change them in the future they’re easy enough to grep for, and if I want to be more dynamic I’ll use nokigiri to parse and render a srcset (and probably fragment cache for performance).

Images man…too much work.


Chris! Next video: Trix + ActiveStorage. :)

Stephane Le Boisselier


Thanks for this great job, i decide to replace Refile with Shrine, but got a problem. In my js xhr.setRequestHeader("X-CSRF-Token", Rails.csrfToken()); generate an error in JS Console, Rails is not define. I tried to update my rails version (previously on 5.0.5) to 5.1.4 but problem is still there :( Any idea ? Thanks

Stephane Le Boisselier

I solve this doing :

var token = $('meta[name="csrf-token"]').attr('content');
xhr.setRequestHeader("X-CSRF-Token", token);

But I don't understand why xhr.setRequestHeader("X-CSRF-Token", Rails.csrfToken()); is not working.

Other question, How do you do to have the full url image in src ? I only have /uploads/blablabla, and I have to send html with my api to a mobile app so, I need the full url.

Thanks for your help

Stephane Le Boisselier

Done with :

shrine_options = {
host: ""

Shrine.storages = {

cache:"public", prefix: "uploads/cache", **shrine_options),
store:"public", prefix: "uploads/store", **shrine_options),


In development on cloud9 my trix editor work well, but in production on Heroku, i cannot write on the text area. Anyone know how i can fix this issue?


I tried to combine this with the direct S3 uploads and keep the URL's private. It's working well for me so far so here are the changes i made in a gist
I am a relative novice in Javascript so if anyone has corrections or improvements I would love to know :)


Thanks for sharing Neil!


I really need help with trix, i dont know what iam doing wrong, but Trix isnt working for me in Rails 5.2.
I did everything like in the setup explained but i had my postmodel before.
so as you can see i have a field called t.text "content" - and everywhere trix is referring to a body. Is that the issue? How can i change the migration so that my content is a body? And! If that even needed? I mean if i switch in my code body for content it is supposed to work aswell right?

My Schema looks like this:
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
t.index ["slug"], name: "index_posts_on_slug", unique: true


Login or create an account to join the conversation.