© Brady Somerville, Adam Gamble, Cloves Carneiro Jr and Rida Al Barazi 2020
B. Somerville et al.Beginning Rails 6https://doi.org/10.1007/978-1-4842-5716-6_9

9. JavaScript and CSS

Brady Somerville1 , Adam Gamble2, Cloves Carneiro Jr.3 and Rida Al Barazi4
(1)
Bowling Green, KY, USA
(2)
Gardendale, AL, USA
(3)
Hollywood, FL, USA
(4)
FONTHILL, ON, Canada
 

JavaScript and CSS (Cascading Style Sheets) have evolved over the years from being nice embellishments on a web page to critical aspects of a web application’s user interface. It should be no surprise that Rails, by convention over configuration, makes including modern JavaScript and CSS both easy to incorporate into your web application and flexible to modify for advanced use cases.

Rails 6 introduces the inclusion of the webpacker gem by default. The webpacker gem (with its default configuration) causes your javascript to be preprocessed and bundled with the popular JavaScript bundler webpack. Though webpack is capable of also handling CSS, images, fonts, and more, at this point Rails’ default configuration only uses webpack for JavaScript. To keep things simple, we’ll stick with only using webpack for JavaScript in this book.

For CSS, images, and fonts, Rails still uses the Asset Pipeline, a component of Rails which handles preprocessing and bundling.

Why do we need our JavaScript, CSS, and other assets to be preprocessed and bundled? What does that even mean? We’ll give a brief overview of some of the benefits before we apply our knowledge to the blog application we’re building.

Note

If you need to get the code at the exact point where you finished Chapter 8, download the source code zip file from the book’s page on www.apress.com and extract it onto your computer.

Benefits of Preprocessing Assets

Why bother preprocessing and bundling your assets? We’ve been serving JavaScript, CSS, images, and more on our websites for years just fine, right? In the last several years, JavaScript and CSS have exploded in new features and capabilities and have quickly become integral parts of our web applications, whereas previously they might have just been a nice enhancement.

As our web applications now include more JavaScript and CSS than they used to, we must be concerned with how quickly our users can download our assets. Traditional approaches to optimizing the sizes of our assets required tedious work, or custom scripts.

Also, in recent years, JavaScript and CSS have spawned new languages—such as TypeScript and SASS—which seek to add features that make authoring JavaScript and CSS easier and more featureful. But browsers need JavaScript and CSS, not TypeScript or SASS. Wouldn’t it be nice to choose to author our JavaScript and CSS in the language we desire and have it converted automatically to what the browser needs?

In the next few sections, we’ll discuss some of these benefits of preprocessing assets in more detail.

Asset Concatenation and Compression

Applications that have a lot of JavaScript and CSS can have hundreds of individual .js and .css files. If a browser has to download all of these files, it causes a lot of overhead just starting and stopping the transfer of files. The Asset Pipeline concatenates both your JavaScript and CSS into files so that a browser only has to download one or two files instead of hundreds. It can also minify and compress the files. This removes things like comments, whitespace, and long variable names from the final output. The final product is functionally equivalent, but usually much smaller. Both of these features combine to make web applications load much faster and are transparent to the user.

Secondary Languages

Browsers have very strong support for both JavaScript and CSS, but if you want to use another language on the frontend or even if you use newer JavaScript features that aren’t available in slightly older browsers, you’d be out of luck. The browser would at best ignore it and at worse throw errors all over the screen. webpack and the Asset Pipeline allow you to use other languages that compile down to code that browsers understand. For example, webpack (with babel) allows you to write modern JavaScript—ES6—which is then transpiled (converted) into older JavaScript which more browsers can understand. The Asset Pipeline allows you to create your app’s styles in the SASS language and converts it into standard CSS which browsers understand.

Detailed description of ES6 and SASS is out of the scope of this book, but you should know what they are if you encounter them. For more information on ES6, visit https://developer.mozilla.org/en-US/docs/Web/JavaScript, and for more information on SASS, visit https://sass-lang.com/.

Asset Locations

Rails allows you to place files in several different locations, depending on whether you want them to be processed by webpack or the Asset Pipeline. The following table describes these locations (Table 9-1).
Table 9-1

Locations for Assets

Preprocessor

File Location

Description

Asset Pipeline

app/assets

This is for assets that are owned by the application. You can include images, style sheets, and JavaScript.

Asset Pipeline

lib/assets

This location is for assets that are shared across applications but are owned by you. These assets don’t really fit into the scope of this specific application but are used by it.

Asset Pipeline

vendor/assets

This location is for assets that are from an outside vendor, like JavaScript or CSS frameworks.

webpack

app/javascript/packs/

This location is where you create packs—JavaScript files that import other Javascript files, meant to be served as a bundle. By default, application.js is installed. You can add to it or create a separate pack when you want a substantially different group of JavaScript files (e.g., admin.js).

webpack

app/javascript

This location is where you add smaller JavaScript files which will be imported by pack files, as described in the preceding text.

In general, webpack and the Asset Pipeline stay out of the way, but they empower you to do impressive things with your assets with the default configuration and can be configured to do even more. For more information on the Asset Pipeline, visit https://guides.rubyonrails.org/asset_pipeline.html. For more information on Webpacker, see https://github.com/rails/webpacker.

Turbolinks

Since version 4, Rails has included the Turbolinks gem by default. This gem (and the accompanying JavaScript) aims to speed up your application by using Ajax to request pages instead of the more traditional page requests. It tracks files that are commonly shared across requests, like JavaScript and style sheets, and only reloads the information that changes. It attaches itself to links on your page instead of making those requests the traditional way. It makes an Ajax request and replaces the body tag of your document. Turbolinks also keeps track of the URL and manages the back and forward buttons. It’s designed to be transparent to both users and developers.

Turbolinks is turned on by default since Rails 4. It is included in the default JavaScript pack. If you needed to remove Turbolinks for some reason, you could do so, but we’ll leave it on for our blog application we’re building.

By default, Turbolinks attaches itself to every link on the page, but you can disable it for specific links by attaching a data-turbolinks="false" attribute to the link, as shown in Listing 9-1. This causes the link to behave in a traditional fashion.
link_to "Some Link", "/some-location", data: { turbolinks: false }
Listing 9-1

Rails link_to Helper with a No-Turbolinks Attribute Attached

Note

Some JavaScript libraries aren’t compatible with Turbolinks. Listing these is out of the scope of this book, but you can find more information at https://github.com/turbolinks/turbolinks. If you continue to have problems, you can always disable Turbolinks.

Let’s Build Something!

We’ve talked about the features of Rails that support JavaScript and CSS, but let’s actually put JavaScript to work. We’ve added our style sheets in Chapter 8, but this chapter will focus on making our application use Ajax to load and submit forms.

Ajax and Rails

Ajax is a combination of technologies centered around the XMLHttpRequest object, a JavaScript API originally developed by Microsoft but now supported in all modern browsers. Of course, you could interface with the XMLHttpRequest API directly, but it wouldn’t be fun. A far better idea is to use one of several libraries that abstracts the low-level details and makes cross-browser support possible.

Rails makes Ajax easier for web developers to use. Toward that end, it implements a set of conventions that enable you to implement even the most advanced techniques with relative ease.

Most of the Ajax features you implement in Rails applications are coded using JavaScript; so familiarity with JavaScript code always helps and is pretty important for today’s web developers.

JavaScript and the DOM

The Document Object Model (DOM) provides a way to interact programmatically with a web page in your browser with JavaScript. Using the DOM, you can add, update, and remove elements from the web page without having to ask the server for a new page.

In the past, different browsers did not provide consistent APIs for interacting with the DOM. Developers were forced to write different JavaScript for different browsers. Eventually, JavaScript frameworks like jQuery emerged to simplify the process of writing code compatible with different browsers.

However, things have changed considerably. Different browsers now provide a more consistent interface (not perfectly consistent, but better!). So while tools like jQuery were considered essential in the not-so-distant past, developers no longer need such frameworks to achieve cross-browser compatibility.

Tip

Wikipedia defines DOM as follows: “The Document Object Model (DOM) is a cross-platform and language-independent convention for representing and interacting with objects in HTML, XHTML, and XML documents” (https://en.wikipedia.org/wiki/Document_Object_Model).

Working with the DOM is a deep subject; we’ll only scratch the surface in this book. But we’ll learn enough to add some nice touches to our application. See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model for more information.

First, we’ll show you a few different ways to select elements from the DOM in the following table (Table 9-2).
Table 9-2

Selecting Elements from the DOM

Function

Description

document.querySelector('#article_123')

Returns the element matching the given ID article_123.

document.querySelectorAll('.comment')

Returns a list of elements with the class name comment.

document.querySelectorAll('div.article')

Returns a list of div elements with the class name article.

Table 9-2 used some of the most commonly used CSS selectors. For a complete list, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors. Also, notice that when we expected a unique element (e.g., with a given id), we used querySelector, whereas when we expected any number of elements, we used querySelectorAll.

Moving to Practice

Now that you know what Ajax is, how it works, and how to select elements from the DOM, we can apply some of this knowledge to enhance the usability of our application. Mainly, one would use Ajax in their application when they think a snappier interaction is possible and recommended. Let’s begin Ajaxifying our blog application in the article page.

Not All Users Comment

If you look at the article page, you quickly notice that every time users read a post, they’re presented with a form for entering comments. Although reader participation is paramount, most users are only interested in reading the content. You can modify the article page to not load the comment form automatically; instead, it will load the form only after a user clicks the new comment link.

Loading a Template via Ajax
One of the rules of good interface design is to make things snappy. That is to say, the interface should be responsive and quick to load. A good way to achieve this is to load elements (like forms or content areas) onto the page whenever the user requests them. Modify the article’s show template, as shown in Listing 9-2.
<%= render @article %>
<h3>Comments</h3>
<div id="comments">
  <%= render @article.comments %>
</div>
<%= link_to "new comment", new_article_comment_path(@article), remote: true, id: 'new_comment_link' %>
Listing 9-2

The Article Partial in app/views/articles/show.html.erb: https://gist.github.com/nicedawg/5e13311132f323e425be2488c7b2f5d4

The template hasn’t changed a lot: you no longer directly render the comment form, and you add a link called new comment. The new link still uses the well-known link_to helper to generate a link; however, you pass in the remote: true option, which tells Rails that you want the request triggered by this link to hit the server using Ajax.

There are a couple of things to note in the use of link_to in Listing 9-2. First, you send the request to a URL that already exists; the new_article_comment_path route identifies a path to a new comment. Second, you use the id: 'new_comment_link' option to give the rendered HTML element an ID that you can refer to later.

On the server side, you don’t need to make any changes to the comments controller. As currently implemented, you don’t explicitly implement a new action; the default behavior in this case is to render the new partial template in app/views/comments/_new.html.erb. But that file doesn’t exist, and that isn’t really what we want. We want to execute some JavaScript in this case—not just receive some HTML. Instead, we want a separate JavaScript template to be used as a response for this action.

Responding to Requests with JavaScript
When a browser makes a request, it indicates what type of content it hopes to receive. When we add remote: true to the preceding link, Rails will now cause the browser to request a JavaScript response, instead of the typical HTML response. To make sure you send a response that includes JavaScript code, you must create a template with the .js.erb template extension. Create the app/views/comments/new.js.erb template as per Listing 9-3. The following text explains all the lines in the template to make sure you know what’s happening.
document.querySelector("#comments").insertAdjacentHTML("afterend", "<%= escape_javascript render partial: 'new' %>");
document.querySelector("#new_comment_link").style.display = 'none';
Listing 9-3

The .js.erb New Comment Template in app/views/comments/new.js.erb: https://gist.github.com/nicedawg/944c9b741caa5437101687103e292a94

The first line selects the element with id comments and inserts after it the rendered output of the app/views/comments/_new.html.erb partial. Table 9-3 lists similar DOM methods that you can use to add HTML content to a page with JavaScript.
Table 9-3

DOM Element Methods for Inserting HTML into a Page

Method

Description

insertAdjacentHTML(position, text)

Inserts the provided text adjacent to the current element, according to position, which can be ‘beforebegin’, ‘afterbegin’, ‘beforeend’, or ‘afterend’

insertAdjacentElement(position, element)

Inserts the provided element adjacent to the current element, according to the provided position, as in the preceding text

insertAdjacentText(position, text)

Inserts the provided text adjacent to the current element, according to the provided position, as in the preceding text. This is recommended when you expect the content to be plain text.

Going back to Listing 9-3, the last line hides the new_comment_element, which contains the link to add a new comment, by setting its style’s display attribute to “none.” Because you already have the comment form in your page, it makes little sense to keep that link around.

Note

In a similar fashion, you can display a hidden element by setting its style.display attribute to “block,” “inline,” or other values, depending on the element’s intended usage.

Let’s see what you built in practice. Open your browser to any existing article, such as http://localhost:3000/articles/2, and notice that the comment form is no longer there (Figure 9-1).
../images/314622_4_En_9_Chapter/314622_4_En_9_Fig1_HTML.jpg
Figure 9-1

The article page without the comment form

As soon as you click the new comment link, the comment form pops into place, and you can add comments (Figure 9-2). You achieved your goal of keeping the user interface cleaner while allowing users to quickly access functionality without having to move to a new page. That’s a good start.
../images/314622_4_En_9_Chapter/314622_4_En_9_Fig2_HTML.jpg
Figure 9-2

The article page with the comment form and without the new comment link

Making a Grand Entrance

In the previous section, you added an element to the screen via Ajax—the comment form. It’s a pretty big form. It’s a very obvious inclusion on the page and your users won’t miss it; however, sometimes you may want to add just an extra link or highlight some text on a page. To help draw attention to the new content, let’s have it fade in.

We’ll add some JavaScript to fade in the comment form. Modify your app/views/comments/new.js.erb so it looks like the code in Listing 9-4.
document.querySelector("#comments").insertAdjacentHTML("afterend", "<%= escape_javascript render partial: 'new' %>");
var comment_form = document.querySelector("#main form");
comment_form.style.opacity = 0;
setTimeout(function() {
  comment_form.style.transition = 'opacity 1s';
  comment_form.style.opacity = 1;
}, 10);
document.querySelector("#new_comment_link").style.display = 'none';
Listing 9-4

The Updated New Comment Template in app/views/comments/new.js.erb: https://gist.github.com/nicedawg/1a94805ef8141b48b16a0b2faf8d659b

We added a few lines that could use some explanation. First, we select the form we just added and store it in the variable comment_form. Then, we immediately set the form’s opacity to 0, to make it completely transparent. Then, we use the setTimeout method to delay the execution of the next steps by 10 milliseconds. (Apparently, newly added content needs a few milliseconds before they’re ready to consistently work with CSS transitions.) Lastly, we tell the element that any future changes to its opacity attribute should gradually take place over 1 second, and then we set its opacity to 1—full visibility—and the element begins to fade in.

Arguably, there are better ways of making an element fade in. Perhaps we should have written some CSS rules to handle the transition in combination with JavaScript and made a more reusable solution for fading the element in. We also should make the JavaScript code we added more robust—but for now, this is the simplest way to get what we want, and that’s okay!

Open your browser at any article page and look at the shiny effect that is being applied.

Note

You very likely want to learn more about all the various style properties you can change with CSS and JS. For more info, see https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style.

Using Ajax for Forms

Another user interaction improvement is to not refresh the page after a user adds a new record. In quite a few applications, users may be required to enter a considerable amount of data in forms; so this technique is important to grasp.

In the same way that you made a link submit data via Ajax, we can make forms submit data via Ajax by making sure the data-remote=“true” attribute is on the form’s HTML element. When using the form_with helper, as we did with the comment form, that happens automatically! However, we added local: true to keep that from happening earlier to help illustrate this point. We can simplify our comment form a bit by removing some parameters we no longer need (Listing 9-5).
<%= form_with(model: @article.comments.new, url: article_comments_path(@article)) do |form| %>
  <div class="field">
    <%= form.label :name %><br />
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :email %><br />
    <%= form.text_field :email %>
  </div>
  <div class="field">
    <%= form.label :body %><br />
    <%= form.text_area :body %>
  </div>
  <div class="actions">
    <%= form.submit 'Add' %>
  </div>
<% end %>
Listing 9-5

The Updated Comment Form in app/views/comments/_new.html.erb: https://gist.github.com/nicedawg/5dd35d1922270369b41f52815b57b224

Although the changes in the view are minimal, you have to make a few more changes in your controller layer. You want to respond to JavaScript and HTML requests in different ways. Change the create method in your comments controller to look like the code in Listing 9-6.
class CommentsController < ApplicationController
  before_action :load_article, except: :destroy
  before_action :authenticate, only: :destroy
  def create
    @comment = @article.comments.new(comment_params)
    if @comment.save
      respond_to do |format|
        format.html { redirect_to @article, notice: 'Thanks for your comment' }
        format.js
      end
    else
      respond_to do |format|
        format.html { redirect_to @article, notice: 'Unable to add comment' }
        format.js { render :fail_create }
      end
    end
  end
  def destroy
    @article = current_user.articles.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to @article, notice: 'Comment deleted'
  end
  private
  def load_article
    @article = Article.find(params[:article_id])
  end
  def comment_params
    params.require(:comment).permit(:name, :email, :body)
  end
end
Listing 9-6

The Updated Comments Controller in app/controllers/comments_controller.rb: https://gist.github.com/nicedawg/db9226972a7cbc652513d3e657b959b7

The main method in this code is the respond_to helper . By using respond_to, you can have some code in the format.html block that’s called when you receive a regular request and some code in the format.js block that’s called when a JavaScript request is received. Hang on! There is no code in format.js! When no code is added to a format block, Rails looks for a template named after the view, just like regular views, which means it looks for create.js.erb. When a submitted comment fails validation, you also want to warn the user by displaying error messages; for that, you use format.js { render :fail_create } to render a template named fail_create.js.erb.

The new apps/views/comments/create.js.erb and app/views/comments/fail_create.js.erb templates are shown in Listings 9-7 and 9-8, respectively.
document.querySelector("#comments").insertAdjacentHTML("beforeend", "<%= escape_javascript render @comment %>");
document.querySelector("#main form").reset();
Listing 9-7

The Template in app/views/comments/create.js.erb: https://gist.github.com/nicedawg/cae8f8be678155859b6730b144738599

alert("<%= @comment.errors.full_messages.to_sentence.html_safe %>");
Listing 9-8

The Template in app/views/comments/fail_create.js.erb: https://gist.github.com/nicedawg/0cb4848e5553d60b366d0ecd8f2a1d4d

In the create.js.erb template , you run a couple of JavaScript commands. First, you render the template for a new comment—using render @comment—and insert that HTML at the bottom of the comments div, similar to what we’ve done before. The document.querySelector("#main form").reset(); line is a simple call to reset all the elements of the new comment form, which is blank and ready to accept another comment from your user.

In the fail_create.js.erb template , you use the alert JavaScript function to display a dialog box with the validation error message, as shown in Figure 9-3.
../images/314622_4_En_9_Chapter/314622_4_En_9_Fig3_HTML.jpg
Figure 9-3

Displaying an error message

Give it a try: point your browser to an existing article, for example, http://localhost:3000/articles/2, and enter a few—or lots of—comments. As you can see, you can interact with the page in a much more efficient way: there’s no need to wait until a full page reload happens.

Deleting Records with Ajax

To complete the “making things snappy” section, you may want to delete some of the comments that are added by users. You can combine the techniques you’ve learned in this chapter to let users delete comments without delay.

You already have a link to delete comments in the comment template at app/views/comments/_comment.html.erb. To use Ajax with that link, you again need to add the remote: true option to the method call. We’re also going to add a unique id to each comment so that later, we know which comment to delete (Listing 9-9).
<div class="comment" id="comment-<%= comment.id %>">
  <h3>
    <%= comment.name %> <<%= comment.email %>> said:
    <% if @article.owned_by? current_user %>
      <span class="actions">
        <%= link_to 'Delete', article_comment_path(article_id: @article, id: comment), confirm: 'Are you sure?', method: :delete, remote: true %>
      </span>
    <% end %>
  </h3>
  <%= comment.body %>
</div>
Listing 9-9

The Template in  app/views/comments/_comment.html.erb: https://gist.github.com/nicedawg/3710992814a2d9328dd86dd8cd081df5

The changes in the controller are also minimal. Use the respond_to and format block to make sure you support both regular and JavaScript requests, as shown in Listing 9-10.
class CommentsController < ApplicationController
  before_action :load_article, except: :destroy
  before_action :authenticate, only: :destroy
  def create
    @comment = @article.comments.new(comment_params)
    if @comment.save
      respond_to do |format|
        format.html { redirect_to @article, notice: 'Thanks for your comment' }
        format.js
      end
    else
      respond_to do |format|
        format.html { redirect_to @article, notice: 'Unable to add comment' }
        format.js { render :fail_create }
      end
    end
  end
  def destroy
    @article = current_user.articles.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    respond_to do |format|
      format.html { redirect_to @article, notice: 'Comment deleted' }
      format.js
    end
  end
  private
  def load_article
    @article = Article.find(params[:article_id])
  end
  def comment_params
    params.require(:comment).permit(:name, :email, :body)
  end
end
Listing 9-10

The Comments Controller in app/controllers/comments_controller.rb: https://gist.github.com/nicedawg/0d6b6c3907175ed645cd6c1ebb2965c4

You wire up the delete link in the comment partial to send an Ajax request to the controller. The controller responds to those Ajax requests with the default action, which is to render the app/views/comments/destroy.js.erb file (Listing 9-11).
var comments = document.querySelector("#comments");
comments.removeChild(document.querySelector("#comment-<%= @comment.id %>"));
Listing 9-11

The app/views/comments/destroy.js.erb File: https://gist.github.com/nicedawg/b30f595ec078927ff93ab37f3bb94f14

In the preceding JavaScript, we select the comments container and then call removeChild, passing it the comment element we wish to delete. Removing an element seems a little complicated. Most modern browsers support simply calling .remove() on the element you want to remove, but IE doesn’t support that feature. We could have added a polyfill—a JavaScript library which adds specific features to browsers which don’t implement them—but that’s out of the scope of this book.

Open your browser to an article page—make sure you are logged in as the article owner—with some comments you want to delete or add lots of spam-like comments. See how quickly you can get rid of comments now? It’s a lot better than waiting for page reloads.

Summary

To be sure, JavaScript is a large topic. Entire books, conferences, and technology are devoted to the language, so it goes without saying that this chapter only scratches the surface. Still, in short order, you’ve learned the basics of implementing Ajax in Rails applications, and you know where to go when you need to dig deeper.

You learned how to make remote Ajax calls using the remote: true option for links and forms. You also used a simple visual effect to show new elements on the page, thanks to JavaScript’s ability to interact with the DOM.

Finally, you learned about using JavaScript templates—which have the .js.erb extension—to produce responses to Ajax requests using JavaScript code. At this stage, you have a solid grasp of the Action Pack side of web development with Rails.