Ruby On Rails 2026

From bibbleWiki
Jump to navigation Jump to search

Introduction

Got new job which wanted this skill to poking around to have another go.

Installation

No robot back in 2020 when last I gave this the initial look. It is telling me to use asdf with a specific version. Clearly I need to check this out before doing. For Ubuntu its self it say

sudo apt install -y build-essential libssl-dev libreadline-dev zlib1g-dev libsqlite3-dev

So went with the version of asdf it said because the second instructions fail because there is no completions directory with the latest release so for asdf

sudo apt install curl git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc
source ~/.bashrc

Now for rails went with 3.3.11

asdf plugin add ruby
asdf install ruby 3.3.11
asdf global ruby 3.3.11

And for Bundler and Rails

gem install bundler -v 2.4.19
gem install rails
rails -v Rails 8.1.3

Gemfile Stuff

Gemfile Basics

The Gemfile is Ruby’s equivalent of npm’s package.json. It defines which gems your application depends on and in which environments they should be installed.

Bundler supports groups, which work similarly to npm’s dependencies and devDependencies.

Grouping Gems

You can group gems so they are only installed or loaded in specific environments.

A common example is the development group:

group :development do
gem "web-console"
gem "rubocop", require: false
gem "rubocop-rails", require: false
gem "htmlbeautifier"
end

You can also combine multiple groups:

group :development, :test do
gem "debug", platforms: %i[mri windows], require: "debug/prelude"
gem "bundler-audit", require: false
end

Gems inside these blocks are only installed when Bundler is run in those environments, and Rails only loads them when running in that environment.

Inline Group Syntax

Bundler also supports an inline form, which is functionally identical to the block syntax:

gem "sorbet", group: :development

This line is equivalent to:

group :development do
gem "sorbet"
end

Use whichever style reads better for your Gemfile.

Gems Loaded in All Environments

Any gem not placed inside a group is installed and loaded in every environment (development, test, production).

For example:

gem "sorbet-runtime"

This gem will be available everywhere because it is not wrapped in a group.

Making my Website Page

Getting Started

So set up and ready to go. A few things to note

  • routes in /config/routes
  • views in /app/views/<page-name>
  • layouts in /app/views/layouts
  • components in app and a PageHeader component component with

So I generated an app, page and component

rails new bibble_web_ror --css=tailwind
cd bibble_web_ror 
rails generate controller Bill index
rails generate component PageHeader

Told the robot what I did for view and it kindly gave me the rest. Here is the amended layout

  <body class="min-h-screen bg-(--color-bg-page) text-(--color-text-primary)">
    <a href="#main-content" class="sr-only focus:not-sr-only">Skip to content</a>
    <main id="main-content"
        class="max-w-300 bg-(--color-bg-sections) px-2 pt-2 shadow-(--shadow-page) not-only:mx-auto">
      <%= render PageHeaderComponent.new(title: @title, subtitle: @subtitle) %>
      <div class="u-container">
        <%= yield %>
      </div>
    </main>
    <footer class="p-4 text-center text-sm text-(--color-text-primary)">
      &copy; 2025 Iain Wiseman
    </footer>
  </body>

And for the component

<header class="mb-6">
  <h1 class="text-3xl font-bold text-(--color-text-primary)">
    <%= @title %>
  </h1>

  <% if @subtitle.present? %>
    <p class="mt-1 text-(--color-text-secondary)">
      <%= @subtitle %>
    </p>
  <% end %>
</header>

The generate component failed because I am on rails 8. Suspect there a billion of these generators but we chose view_component. You need to add it to the gemfile and do a bundle install.

rails generate view_component:erb PageHeader

Next it failed again because ruby expects there to be a .rb file which is not automatically generated. Not sure why but suspect my new employer will let me know. Anyway here it is.

class PageHeaderComponent < ViewComponent::Base
  def initialize(title:, subtitle: nil)
    @title = title
    @subtitle = subtitle
  end
end

The robot says you can generate this with

rails generate component PageHeader --ruby

So this did not work so I did something I hate doing, read the docs which show

 rails generate view_component:component PageHeader

And of course this did work.

Tailwind

This did not work either. Ended up following the excellent instructions on their site for tailwind which mainly involved.

bundle add tailwindcss-rails
rails tailwindcss:install

I found I had to run with bin/dev as the generation of color did not happen

bin/dev

The config tailwind.config.js needs to be empty like in vue. And the theme stuff is in app/assets/stylesheets/tailwind/application.css

Debugger

This seemed harder than it needed to be. I think there have been several approaches at it which has left some stuff behind. Anyway I used https://mickzijdel.com/blog/2025-05-19-setting-up-a-debugger-for-ruby-on-rails-8/ for my setup. First I installed the vs code extension VSCode rdbg ruby debugger by Koichi Sasada. Next I added these to my gemfile

gem "rdbg" # Ruby debug integration
gem "foreman" # Necessary for the procfile to work

So next was the Procfile.dev in the root of the project. Replace the original web: line with this

web: RUBY_DEBUG_OPEN=TRUE RUBY_DEBUG_NONSTOP=TRUE rdbg --command --open --stop-at-load -- bundle exec bin/rails server -p 3000
css: bin/rails tailwindcss:watch

So here are the files for reference.
First Tasks

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "runBinDev",
      "type": "shell",
      "command": "bin/dev",
      "isBackground": true,
      "problemMatcher": [
        {
          "owner": "custom-rdbg-task",
          "pattern": [
            {
              "regexp": "^.*DEBUGGER: Debugger can attach.*$",
              "kind": "info",
              "file": 1,
              "location": 1,
              "message": 0
            }
          ],
          "background": {
            "activeOnStart": true,
            "beginsPattern": "^.*DEBUGGER: Debugger can attach.*$",
            "endsPattern": "^.*DEBUGGER: Debugger can attach.*$"
          }
        }
      ],
      "presentation": {
        "reveal": "always",
        "panel": "dedicated",
        "clear": true
      },
      "group": {
        "kind": "build",
        "isDefault": false
      }
    }
  ]
}

Now launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "rdbg",
      "name": "Run bin/dev & Attach with rdbg",
      "request": "attach",
      "preLaunchTask": "runBinDev",
      "localfs": true,
      "localfsMap": "${workspaceFolder}:${workspaceFolder}"
    },
    {
      "type": "rdbg",
      "name": "Attach with rdbg",
      "request": "attach",
      "localfs": true,
      "localfsMap": "${workspaceFolder}:${workspaceFolder}"
    }
  ]
}

Writing a Test Case

So wanted to get a test case going because of the lack of type checking. When you generate a new component you get an empty test case. So for

rails generate component PageHeader

IWe get a app/component/page_header_component.html.erb and app/component/page_header_component.rb which I edited to be

<header class="relative mb-1 bg-(--color-nav-background) pt-4 pb-4 pl-12 text-center sm:pl-4">
  <%= render PageHeaderButtonComponent.new %>
  <h1 class="mb-2 text-center text-3xl font-bold text-(--color-title)">
    <%= @title %>
  </h1>
  <% if @subtitle.present? %>
    <p class="mb-4 text-center text-[1.2rem] font-normal text-(--color-subtitle)">
      <%= @subtitle %>
    </p>
  <% end %>
  <%= render PageHeaderNavComponent.new %>
</header>

And

class PageHeaderComponent < ViewComponent::Base
  def initialize(title:, subtitle: nil)
    @title = title
    @subtitle = subtitle
  end
end

But we also get a

# frozen_string_literal: true

require "test_helper"

class PageHeaderHeaderComponentTest < ViewComponent::TestCase
  def test_component_renders_something_useful
    # assert_equal(
    #   %(<span>Hello, components!</span>),
    #   render_inline(PageHeaderHeaderComponent.new(message: "Hello, components!")).css("span").to_html
    # )
  end
end

This I adapted to be

# frozen_string_literal: true

require "test_helper"

class PageHeaderComponentTest < ViewComponent::TestCase
  def test_component_renders_something_useful
    rendered = render_inline(PageHeaderComponent.new(title: "my title", subtitle: "my subtitle"))

    assert_equal "my title", rendered.css("h1").text.strip
    assert_equal "my subtitle", rendered.css("p").text.strip
    assert rendered.at_css("button[data-controller='theme-toggle']").present?
  end
end

I found I had not done the following

  • Defined view_components correctly in the gemfile as it needs test
  • Not modified test_helper.rb

So for the gemfile we needed

group :development, :test do
  # view_component for building reusable view components [https://viewcomponent.org/]
  gem "view_component"
end

And for the test_helper.rb, I am assuming this is autoloading, I need to have

ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "view_component/test_case"

module ActiveSupport
  class TestCase
    # Run tests in parallel with specified workers
    parallelize(workers: :number_of_processors)

    # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
    fixtures :all

    # Add more helper methods to be used by all tests here...
  end
end

Sorbet

There is no type safety in ruby but if you use sorbet it maybe prove to be useful. I set this up using mostly using the youtube Type Checking with the Sorbet Gem in Rails 7 | Ruby on Rails 7 Gem Tutorial by deanin. First of all make sure you have the vs code sorbet extension. Next the gemfile

# Runtime for sorbet type checking [https://sorbet.org/]
gem "sorbet", group: :development
gem "sorbet-runtime"
gem "tapioca", require: false, group: [ :development, :test ]

Then run this - said it took a while but not for me.

bundle exec tapioca init

Now run

bin/tapioca dsl

Now we are free to put our types in. So I started with something small. Basically you need the magic line # typed: true and to add extend T::Sig. From there you put in your sig line appropriately

# typed: true

class PageHeaderComponent < ViewComponent::Base
  extend T::Sig

  sig { params(title: String, subtitle: T.nilable(String)).void }
  def initialize(title:, subtitle: nil)
    @title = title
    @subtitle = subtitle
  end
end

Nothing has been easy with this but probably because I am new to this again. A lot of setup I feel. I needed to restart the workspace to get the test cases to appear in test explorer and found the error "The super class `ViewComponent::TestCase` of `PageHeaderComponentTest` does not derive from `Class`" in all of the the tests. The robot fixed it by changing todo.rbi which appears to be an export file for sorbet. It changed

module ViewComponent::TestCase

to

class ViewComponent::TestCase

And the error went away. However I noticed at the top of the file it said

# DO NOT EDIT MANUALLY
# This is an autogenerated file for unresolved constants.
# Please instead update this file by running `bin/tapioca todo`.

# typed: false

So wonder whether this was a plan stand and of course the answer was no. It then fixed it, hopefully properly, by adding the file sorbet/rbi/manual

# typed: true
# Manual stub for ViewComponent test support that isn't auto-discovered by Tapioca

class ViewComponent::TestCase
end