Rails Setup

This is a setup guide for a Rails application. It assumes you generated the application with the --minimal option. To save time, you can instead use the template:

rails app:template\
  LOCATION=https://railsway.dohmen.io/modern_frontend.rb

Gems

To make things easier, let's adjust the Gemfile in one go:

source "https://rubygems.org"

gem "rails", "~> 8.0.2"
# The modern asset pipeline for Rails
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3"
# Use the Puma web server
gem "puma"
# Use Haml for templating
gem "haml-rails"
# Reusable, testable & encapsulated view components
gem "view_component"

group :development, :test do
  # Debugger
  gem "debug", platforms: %i[mri windows], require: "debug/prelude"
end

Run bundle now. We will set up all these things over the next sections.

The benefit of using haml-rails over the haml gem and rspec-rails over the rspec gem is that they configure Rails's generators to generate Haml instead of ERB and RSpec instead of Minitest.

JavaScript & CSS Setup

The Rails core team offers the jsbundling-rails and cssbundling-rails gems to build a setup similar to ours. It offers little flexibility, which is why we will instead do those steps by hand. To work with propshaft, our JavaScript and CSS bundling will put the processed files into app/assets/builds. Create that directory, and put a .keep file in it. We also add it to the .gitignore file together with node_modules:

# Ignore the build files from CSS and JS
/app/assets/builds/*
!/app/assets/builds/.keep

# Ignore npm dependencies
/node_modules

Following the new Rails convention, we will add our empty JS file as app/javascript/application.js:

// JavaScript goes here

We create the package.json with our two build scripts:

{
  "private": true,
  "type": "module",
  "scripts": {
    "build:js": "esbuild app/javascript/application.js --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
    "build:css": "postcss ./app/assets/stylesheets/application.css -o ./app/assets/builds/application.css"
  }
}

We add the configuration for PostCSS postcss.config.js:

import postcssImport from "postcss-import";
import autoprefixer from "autoprefixer";

export default {
  plugins: [postcssImport, autoprefixer],
};

Then we install the required dependencies:

npm i --save-dev autoprefixer esbuild postcss postcss-cli postcss-import

To make sure our teammates install the Node dependencies as well, we will run npm install after bundle. So we add this line to our bin/setup right after the bundle check line:

system("npm install")

If you are using a node version manager like nvm, don't forget to add a .nvmrc file to your project. Rails has already created a .ruby-version file for your Ruby version manager.

We will use Foreman to run the three processes in development. We add a Procfile.dev file:

web: env RUBY_DEBUG_OPEN=true bin/rails server
js: node --run build:js -- --watch
css: node --run build:css -- --watch

We also replace bin/dev to run Foreman:

#!/usr/bin/env sh

# Default to port 3000 if not specified
export PORT="${PORT:-3000}"

exec foreman start -f Procfile.dev --env /dev/null "$@"

Finally, we add Rake tasks in lib/tasks/assets.rake that integrate with propshaft:

namespace :css do
  desc "Build your CSS bundle"
  task :build do
    system("node --run build:css")
  end

  desc "Remove CSS builds"
  task :clobber do
    rm_rf Dir["app/assets/builds/**/[^.]*.{css,css.map}"], verbose: false
  end
end

namespace :javascript do
  desc "Build your JavaScript bundle"
  task :build do
    system("node --run build:js")
  end

  desc "Remove JavaScript builds"
  task :clobber do
    rm_rf Dir["app/assets/builds/**/[^.]*.{js,js.map}"], verbose: false
  end
end

Rake::Task["assets:clobber"].enhance(%w[css:clobber javascript:clobber])
Rake::Task["assets:precompile"].enhance(%w[css:build javascript:build])

Haml

You need to replace the layout file with a Haml version. Delete app/views/layouts/application.html.erb and create a new file app/views/layouts/application.html.haml:

!!!
%html{ lang: I18n.locale }
  %head
    %title= content_for(:title) || "Time And Expenses"
    %meta{ name: "viewport", content: "width=device-width,initial-scale=1" }
    = csrf_meta_tags
    = csp_meta_tag

    %link{ rel: "icon", href: "/icon.png", type: "image/png" }
    %link{ rel: "icon", href: "/icon.svg", type: "image/svg+xml" }
    %link{ rel: "apple-touch-icon", href: "/icon.png" }

    = stylesheet_link_tag :application
    = javascript_include_tag :application, type: "module"

  %body
    = yield

Note that we made some changes compared to the ERB version:

ViewComponent

We create an initializer for ViewComponent in config/initializers/view_component.rb:

Rails.application.configure do
  # Generate a folder for each component for
  # Template, CSS, JS...
  config.view_component.generate.sidecar = true

  # Don't generate helpers
  config.generators.helper = nil
end

As we are using ViewComponent, we do not need the generated helpers. So we will not generate them. We will talk about sidecars later. We will also add one helper to app/helpers/component_helper.rb that we will explain in the next chapter:

module ComponentHelper
  def component(name, **args, &)
    component = "#{name}_component".camelize.constantize
    render(component.new(**args), &)
  end
end

By default, all components inherit from ApplicationComponent. Let's create it including the helper above in app/components/application_component.rb:

class ApplicationComponent < ViewComponent::Base
  include ComponentHelper
end

Lift Browser Restrictions

Because the default asset pipeline in Rails no longer supports old browsers, Rails now blocks older browsers by default. As we don't use that pipeline, and we want everyone to be able to use our website, we will remove the blocker. In app/controllers/application_controller.rb remove the following line and the comment above it:

allow_browser versions: :modern