Recent blog posts

This blog keeps a record of ideas, notes, and documentation, in chronological order. The writings are often not polished, and may contain errors here and there, but I like to keep them here for future reference.
This page publishes the most recent blog posts. To view all posts in a single page, go to the archive.

Confirm delete a record

Deleting a record from a list (for example in the index view) can be performed using the button_to method.

This method will create an html form, with a few elements inside of it: a button and two hidden input elements.

One hidden element is the authenticity token that makes sure the form is safe.

<input type="hidden" name="authenticity_token" value="SIIEsaU4c..." autocomplete="off">

The second hidden element is a field specifying the method to be used by the routing system to figure out which action to run (the destroy action).

<input type="hidden" name="_method" value="delete" autocomplete="off">

The form itself will make a POST request to /posts/:id with the id of the record to delete.

To prevent accidental removal of records, we can add a confirmation modal dialog with the data-turbo-confirm attribute on the form element.
If the user cancels the dialog, the form will not be submitted.

Here’s how to add the data-turbo-confirm dialog to the form, from inside the button_to method:

  <%= button_to "Delete", post_path(post),
    method: :delete,
    form: {data: {turbo_confirm: "This post will be deleted immediately. Do you want to continue?"}},
    class: "inline-block underline text-indigo-600" %>

Rails default sorting

The default_scope method in a model lets us sort records in a default sorting order, like so:

class Post < ApplicationRecord

  default_scope order(created_at: :desc)
end

default_scope is used to set any default on operations on a model. Here’s another example:

default_scope where(deleted_on: nil)

Even though default_scope is useful in certain circumstances, it has its drawbacks, and it’s probably better to avoid using it in favor of setting explicit scopes instead.

Here’s an example:

class PostsController < ApplicationController
  def index
    @posts = Post.order(created_at: :desc)
  end
end

Here’s an article on Why using default_scope is a bad idea

Add TinyMCE text editor to a Rails 8 application

Documentation for TinyMCE Rails gem is here

Add tinymce-rails to Gemfile, and run bundle install.

# Gemfile

gem 'tinymce-rails'

Create a config/tinymce.yml file with global configurations.

# config/tinymce.yml

toolbar:
  - styleselect | bold italic | undo redo
  - image | link
plugins:
  - image
  - link

Include TinyMCE assets by adding the script tag to the layout using the tinymce_assets helper.

<!-- app/views/layouts/application.html.erb -->

<%= tinymce_assets data: { turbo_track: "reload" } %>

For each textarea that needs to be set up with TinyMCE, add the tinymce class and ensure it has a unique ID.

<%= f.text_area :content, class: "tinymce", rows: 40, cols: 120 %>

Invoke the tinymce helper at the end of the form page to initialize TinyMCE.

<%= tinymce %>

Show the content by calling the sanitize method on the attribute.

<!-- app/views/pages/show.html.erb -->

<div class="text-content">
  <%= sanitize @page.content %>
</div>

Apply custom styles to the content by adding a class to a content container (in this example a div with class text-content) and by adding TailwindCSS styles under that class.

/* app/assets/stylesheets/application.tailwind.css */

@layer components {
  .text-content h2 { @apply font-bold text-2xl mt-6 mb-4; }

  .text-content p { @apply mb-8; }

  .text-content .p-with-image {
    @apply flex flex-row gap-12 justify-between;
  }
}

Stimulus components nested form

Using nested forms in Rails we may have a need to create additional nested fields on the fly inside the main form.

To achieve this result, we can use Rails Nested Form, a Stimulus controller from the Stimulus Components collection.

1. Installing the library

Install the library using importmap with this command:

$ bin/importmap pin @stimulus-components/rails-nested-form

> Pinning "@stimulus-components/rails-nested-form" to 
> vendor/javascript/@stimulus-components/rails-nested-form.js
> via download from https://ga.jspm.io/npm:@stimulus-components/[email protected]/dist/stimulus-rails-nested-form.mjs

> Pinning "@hotwired/stimulus" to vendor/javascript/@hotwired/stimulus.js
> via download from https://ga.jspm.io/npm:@hotwired/[email protected]/dist/stimulus.js

This action changes config/importmap.rb like so:

diff --git a/config/importmap.rb b/config/importmap.rb

-pin "@hotwired/stimulus", to: "stimulus.min.js"
+pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2

+pin "@stimulus-components/rails-nested-form", to: "@stimulus-components--rails-nested-form.js" # @5.0.0

It also adds the following files to vendor/javascript:

@hotwired--stimulus.js
@stimulus-components--rails-nested-form.js

2. Generate the Stimulus controller

$ bin/rails generate stimulus nested_form
This will generate `app/javascripts/controller/nested_form_controller.js`

Remove all the generated code in the file and add the following code, copied from the documentation under Extending Controller.

// app/javascripts/controller/nested_form_controller.js

import NestedForm from "@stimulus-components/rails-nested-form"

export default class extends NestedForm {
  connect() {
    super.connect()
    console.log("Do what you want here.")
  }
}

3. Update the form

In the form, add some data attributes required by the nested form controller:

<!-- app/views/pathways/_form.html.erb -->

<%= form_with model: pathway,
  data: {
    controller: "nested-form",
    nested_form_wrapper_selector_value: '.nested-form-wrapper'
  } do |form| %>

If we load the page with the form at this point http://localhost:3000/pathways/new we should see in the browser console the output of the console.log command that we put in the nested form controller. This confirms the controller is accessed correctly.

// app/javascripts/controller/nested_form_controller.js

  ...

  console.log("Do what you want here.")

  ...

Inside the form, add a <template> element that wraps the nested fields. Adjust the template to add the new object to add Step.new, and a child_index attribute with the value of NEW_RECORD.

Inside the nested fields, render a Rails partial with the actual fields needed, shown below.

    <template data-nested-form-target="template">
      <%= form.fields_for :steps, Step.new, child_index: 'NEW_RECORD' do |step_fields| %>
        <%= render "step_form", step_fields: step_fields, employer: employer %>
      <% end %>
    </template>

Immediately below the template element, add the regular nested fields, rendering the same Rails partial, like so:

    <%= form.fields_for :steps do |step_fields| %>
      <%= render "step_form", step_fields: step_fields, employer: employer %>
    <% end %>

Below this code, add a div that will be used by Nested Forms to inject the steps:

    <!-- Inserted elements will be injected before that target. -->
    <div data-nested-form-target="target"></div>

Finally, add a button to add the steps

     <button type="button" data-action="nested-form#add">Add step</button>

The Rails partial that’s rendered when we add a step looks like this:

    <!-- app/views/pathways/_step_form.html.erb -->

<div class="nested-form-wrapper mb-8 border border-stone-300 p-4 flex flex-row gap-4"
  data-new-record="<%= step_fields.object.new_record? %>">

  <p class="my-4">
    <%= step_fields.label :place, "Step", class: "block font-bold mb-2 uppercase text-xs" %>
    <%= step_fields.select :place, options_for_select(1..10, step_fields.object.place), prompt: "Select" %>
  </p>

  <p class="my-4">
    <%= step_fields.label :position_id, "Position", class: "block font-bold mb-2 uppercase text-xs" %>
    <%= step_fields.select :position_id,
      options_from_collection_for_select(employer.positions.all, :id, :title, step_fields.object.position_id),
      prompt: "Choose Position",
      class: "bg-stone-200 rounded-lg border border-stone-500" %>
  </p>


   <button type="button" data-action="nested-form#remove">Remove step</button>

   <%= step_fields.hidden_field :_destroy %>
</div>

It includes a button to remove the step. It also includes a hidden field that passes the _destroy param, to remove this step.

Remember to allow the _destroy param in the controller:

# app/controllers/pathways_controller.rb

  def pathway_params
    params.require(:pathway).permit(:name, steps_attributes: [:id, :place, :position_id, :_destroy])
  end

Rails 8 User registration

This post describes how to create a User registration functionality for a Rails 8 application that uses the default Rails 8 authentication framework.

In the routes file, add routes for the registration: new and create. Note: the route is singular.


# config/routes.rb

resource :registration, only: [:new, :create]
Add a new `RegistrationsController` in `app/controllers/registrations_controller.rb` with `new` and `create` actions.
# app/controllers/registrations_controller.rb

class RegistrationsController < ApplicationController
  allow_unauthenticated_access only: [ :new, :create ]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      start_new_session_for @user
      redirect_to root_path, notice: "Successfully signed up!"
    else
      flash.now[:alert] = "Something went wrong."
      render :new, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.expect(user: [:first_name, :last_name, :email_address, :password, :password_confirmation])
  end
end

Create a new view in registrations/new.html.erb with this content:

<%= form_with model: @user, url: registration_url do |form| %>

  <%= render "shared/errors", object: @user %>

  <div>
    <%= form.label :first_name %>
    <%= form.text_field :first_name,
      required: true,
      autofocus: true,
      placeholder: "your first name" %>
  </div>

  <div>
    <%= form.label :last_name%>
    <%= form.text_field :last_name,
      required: true,
      placeholder: "your last name" %>
  </div>

  <div>
    <%= form.label :email_address%>
    <%= form.email_field :email_address,
      required: true,
      placeholder: "your email address" %>
  </div>

  <div>
    <%= form.label :password, "New password" %>
    <%= form.password_field :password,
      required: true,
      placeholder: "your new password",
      maxlength: 72 %>
  </div>

  <div>
    <%= form.label :password_confirmation %>
    <%= form.password_field :password_confirmation,
      required: true,
      placeholder: "your password again",
      maxlength: 72 %>
  </div>

    <%= form.submit "Sign up" %>
<% end %>

User Model

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :email_address, presence: true, uniqueness: true
  validates :password, presence: true
  validates :password_confirmation, presence: true
  validates_confirmation_of :password

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end

Errors partial app/views/shared/_errors.html.erb


<% if object.errors.any? %>
  <section class="bg-orange-200 p-8 my-8 mx-auto">
    <h2>
      Your form could not be saved.
    </h2>
    <h3>
      Please correct the following
      <%= pluralize(object.errors.size, "error") %>:
    </h3>
    <ul>
      <% object.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </section>
<% end %>

Styling the .field_with_errors class in the main application.css file:

/* app/assets/stylesheets/application.css */

div.field_with_errors > label {
  color: red;
}

div.field_with_errors > :is(input, textarea, select) {
  border: 2px solid red;
  margin-bottom: 1rem;
}

Upgrade Rails version from 7 to 8

Useful links:

List versions of Ruby installed

$ mise list

Tool  Version           Config Source                          Requested
node  20.3.0 (missing)  ~/Sites/.tool-versions                 20.3.0
ruby  3.3.6             ~/Sites/career-pathways/.tool-versions 3.3.6

Set the Ruby version to use with new Rails (3.6.6)

mise use [email protected]

Check current version

$ ruby -v
ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]

Change the version of Ruby and Rails in Gemfile

# Gemfile

ruby "3.3.6"

gem "rails", "~> 8.0"

Run bundle update to update the gems:

$ bundle update

... lots of output ...

View All Posts →