Rails 7: Up & Running with JSBundling

Rails new has new options. They are --css and --javascript. If you don’t use either one, you will build a new Rails app using ImportMap. If you specify either –css or –javascript, your app will be created not using Importmap and instead using Bundling: both cssbundling-rails and jsbundling-rails.

If you are familiar with Webpacker under Rails 7, JSBundling is probably what you will transition to, because it lets you build javascript using the tool of your choice: Webpack, ESBuild, or Rollup.

Remember, if you want parity with what you were familiar with in Webpacker under Rails 6, using jsbundling-rails with the new faster esbuild is the way to go for you.

This article will help you if you’ve already decided you don’t want ImportMap. To help make that decision, check out Rails 7 : Do I need ImportMap-rails

Bundling itself is not compatible with eager loading your Javascript files, so for Stimulus, your application loader will look different.

Bundling also requires that you run a “watcher” in the background so that as you develop SCSS or Typescript your changes are immediately recompiled by the watcher and visible in your development environment.

IMPORTANT: Do not attempt to add jsbundling-rails on top of an app created with rails new (importmap). Do not attempt to create a new Rails app with cssbundling-rails and then add jsbundling-rails after. Do not attempt to switch between jsbundling-rails and Importmap on new Rails installations.

Section 1: Meet the CSS and JS Flags

If you use either the --css or the --js flag, you will get both cssbundling and jsbundling. However, if you use one or the other, the next one becomes more tedious to set up. Therefore, I recommend you always start with both --css and --javascript flags:

rails new HelloWorld --javascript=esbuild --database=postgresql

Your JS choices are: ESBuild, Rollup, and Webpack. For CSS, see Section 2 of this article.

Now make sure to use the new ./bin/dev instead of the old rails server or else your JS & CSS will not compile.

On the project folder you now start your Rails server and a background “watcher” with:

./bin/dev

Confirm ESBuild Loads the Rails 7 Features

1. Confirm that Stimulus works

Uncomment this line in routes.rb

Rails.application.routes.draw do
  root "articles#index"
end

Generate an articles controller

rails generate controller Articles

Now create a file at app/views/articles/index.erb

<div data-controller="hello"></div>

Open up app/javascript/controllers/hello_controller.js

Boot rails using

./bin/dev rails

DO NOT BOOT RAILS USING THE OLD rails server METHOD

Confirm that you see the “Hello world!” on the screen.

When creating a new Stimulus controller, it is essential to register this controller with the Stimulus loader found in app/javascript/controllers/index.js

You can do this one of two ways

Generating the controller using the Rails helper:

./bin/rails generate stimulus controllerName

When generated this way, you will automatically be modifying the manifest file.

If you create the Stimulus controller by hand, run this command to update your manifest file:

./bin/rails stimulus:manifest:update

2. Confirm that Turbo is Working

Go back to routes.rb and add this line. This is non-standard, do not do this in a real Rails app!

Rails.application.routes.draw do

  post "/", to: "articles#index"
  root "articles#index"
end

Now go back to app/views/articles/index.erb

You can leave the Stimulus test in place, and add this form:

<div data-controller="hello"></div>

<%= form_with(url: "/") do |form| %>
  this is a form for you to submit
  <%= form.text_field :xyz %>
  <%= form.submit "submit" %>
<% end %>

Now, load your browser window and you will see the new form.

If Turbo Rails is NOT installed correctly, you will see this:

WRONG: This is what happens when you first boot without the “watcher” running — Stimulus & Turbo are not enabled

Rails 7 boots but Turbo does not initialize

Notice that this form submission — which does absolutely nothing— should be sent to the Rails backend as a TURBO_STREAM, not as an HTML request.

CORRECT: This is what happens when you boot using the new ./bin/dev rails command, which includes running webpack in the background to compile your JS.

./bin/dev rails

Rails 7 boots correctly with ./bin/dev rails

You must see the request submitted as TURBO_STREAM or else Turbo will not be working correctly.

Please note that obviously, even if Turbo is working, this app doesn’t actually do anything. The only purpose of this exercise is to examine your Rails logs for the TURBO_STREAM requests from the frontend.

Section 2: Bundling app with With Bootstrap & ESBuild

If you started with section 1, you can archive that work and start again here.

rails new MyGreatApp --javascript=esbuild --css=bootstrap --database=postgresql

1. Hello sassc-rails Gem

Start by uncommenting the scssc-rails gem which is commented out by default in the Gemfile:

gem "sassc-rails"

2. Turn on Inline Source maps (so we can debug Rails 7 Bootstrap)

in config/environments/development.rb add this line:

 config.sass.inline_source_maps = true

3. Add a (temporary) Dummy Controller & Route

Create the articles controller with:

rails generate controller Articles

Add an empty def index to it:

class ArticlesController < ApplicationController
  def index
  end
end

in config/routes.rb, uncomment this line:

root "articles#index"





And finally, create a Hello World file at app/views/articles/index.erb

Hello World!

Now, load your website in your browser and click “View Source”

Sprockets has included what we told it to in our case just the @import statement

Notice here that Sprockets has applied a thumbprint to this specific build. When we change anything in our CSS, Sprockets will recompile the application file and give it a new thumbprint.

7. Now add Something for Rails 7 Bootstrap

If we look in our browser, our existing layout looks like so:

The shrewd eye might notice that Bootstrap is in fact applied (you can tell because the font is not the browser’s default), but we would hardly notice since all our app does is say “Hello World”‘

So let’s add a Bootstrap navbar:


    <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Navbar</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="#">Home</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">Link</a>
          </li>
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              Dropdown
            </a>
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
              <li><a class="dropdown-item" href="#">Action</a></li>
              <li><a class="dropdown-item" href="#">Another action</a></li>
              <li><hr class="dropdown-divider"></li>
              <li><a class="dropdown-item" href="#">Something else here</a></li>
            </ul>
          </li>
          <li class="nav-item">
            <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
          </li>
        </ul>
        <form class="d-flex">
          <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success" type="submit">Search</button>
        </form>
      </div>
    </div>
  </nav>

Ok now let’s take a look:

Our app now has what looks like a Bootstrap Navbar, you should find it now in a working state:

However, if you are used to old-style Rails development where jQuery and other library were loaded by Sprockets, you will note that jQuery is not globally available, even though it works here.

This is because we are now using the ESModules paradigm of modern Javascript, where we import the jQuery only where we need it.

Hence, if you open your console browser and type jQuery, you will see:

This is the expected result as jQuery is not globally available.