Rails 7 with bootstrap 5 by cssbundling-rails with dart-sass and jsbundling-rails with esbuild. No webpack and webpacker and a salt of Stimulus

Today I put on my “new to Rails hat” and I decided to start a new Rails 7 project using the latest two new sharp knives that we have with Rails 7 to get a job done.

The Job To Be Done is: Demonstrate Bootstrap 5 examples of a Toast in a Rails 7 project.

This is a simple example of a popular Bootstrap 5 component that has both css and js involved. My goal is to build a website that will show a button and when the button is clicked a Toast will be visualised as it is in https://getbootstrap.com/docs/5.0/components/toasts/#live

Provide sharp knives and the menu is omakase

These are two points of Rails doctrine. The menu is omakase, which means that we have a good default for the tools that we are using. We are also given sharp knives to use – cssbundling-rails, jsbundling-rails, Stimulus, dart-sass, esbuild.

The process

I would like to bundle bootstrap 5 in the application. One of the options is to take bootstrap from npm and I would like to demonstrate how we bundle bootstrap from npm. Note that this might not be the right choice for you.

NPM version

Make sure you have npm 7.1+. I was trying for more than an hour to identify an error because my npm version was 6.X

$ npm --version

Rails 7 project

# We pass in the option --css bootstrap that will configure most things for us
$ rails _7.0.1_ new bProject --css bootstrap
# enter the project folder
$ cd bProject

Everything is installed. You have all the dependencies to bootstrap, there is a node_modules folder containing node modules like bootstrap.

Bundling the CSS

cssbundling-rails gem is installed by default in the gem file.

There is an app/assets/stylesheet/application.bootstrap.scss that imports the bootstrap css.

It’s content is:

@import 'bootstrap/scss/bootstrap';

We would be using sass (again from npm) to build this .scss file. There is a script in the package.json

{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@hotwired/stimulus": "^3.0.1",
    "@hotwired/turbo-rails": "^7.1.0",
    "@popperjs/core": "^2.11.2",
    "bootstrap": "^5.1.3",
    "esbuild": "^0.14.11",
    "sass": "^1.48.0"
  },
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

The script build:css will take the app/assets/stylesheets/application.bootstrap.scss and will produce an app/assets/builds/application.css

The file application.css is the one that our application will be using. It is referred in application.html.erb as
<%= stylesheet_link_tag “application”, “data-turbo-track”: “reload” %>

<!DOCTYPE html>
<html>
  <head>
    <title>BProject</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

The live reload of CSS

In order to change the css and live reload it we must start sass with –watch option as

$ sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch

but don’t do this as there is a helper file that we should execute at the end of the article that is  – calling ./bin/dev

Bundling the JavaScript

jsbundling-rails is installed in the Gemfile.

There is an app/javascript/application.js that imports bootstrap

// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"

The application.js is bundled with esbuild as a tool  and the command is in the package json

# part of package.json
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",

The result is produced in app/assets/builds

The result is referred by the Rails Assets Pipeline and included in the html by calling javascript_inlcude_tag ‘application’ as in

<!DOCTYPE html>
<html>
  <head>
    <title>BProject</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Create a new controller

Initial controller that will be the home screen containing the button.

# app/controllers/home_controller.rb
$ echo '
class HomeController < ApplicationController

  def index
  end
end
' > app/controllers/home_controller.rb

Create a views folder

# views folder
$ mkdir app/views/home

Create the view

It contains a button and a toast.

$ echo '
<!-- app/views/home/index.html.erb -->
<h1>Title</h1>
<div data-controller="toast">
  <button type="button" class="btn btn-primary" data-action="click->toast#show">Show live toast</button>
</div>

<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
  <div id="liveToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
    <div class="toast-header">
      <strong class="me-auto">Bootstrap</strong>
      <small>11 mins ago</small>
      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
    <div class="toast-body">
      Hello, world! This is a toast message.
    </div>
  </div>
</div>
' > app/views/home/index.html.erb

Note the code:

<div data-controller="toast">
  <button type="button" class="btn btn-primary" data-action="click->toast#show">Show live toast</button>
</div>

Here we have a ‘data-controller’ attribute in the div element and a ‘data-action’ attribute in the button element.
This is how we will connect Stimulus JS as a javascript framework to handle the logic for clicking on this button.

Configure the routes

When we open “/” we want Rails to call the home#index method that will return the index.html.erb page.

$ echo '
# config/routes.rb
Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
 root "home#index"
end' > config/routes.rb

Create a new Stimulus controller

We will use Stimulus as a small prefered framework in Rails for doing JavaScript. When the button in the above HTML fragment is selected the method show() in the controller will be called.

# Use the helper method to generate a stimulus controller
$ bin/rails generate stimulus toastController

# create the content of the controller
$ echo '
// app/javascript/controllers/toast_controller.js
import { Controller } from "@hotwired/stimulus"
import { Toast } from "bootstrap"

// Connects to data-controller="toast"
export default class extends Controller {

  connect() {
  }

  show() {
    const toastElement = document.getElementById("liveToast")
    const toast = new Toast(toastElement)
    toast.show();
  }
}
' > app/javascript/controllers/toast_controller.js

Start the dev servers for rails, jsbundling-rails and cssbundling-rails

$ ./bin/dev

Visit localhost:3000

There is a button that could be selected and a toast will appear.