← All posts

Customizing Rails Generators for Views and Controllers

June 22, 2023 rails

Rails scaffold generators get you running fast, but the views and controllers they produce rarely match how your app actually looks. Instead of generating and then rewriting, you can customize the templates so the output matches your conventions from the start.

Configuring What Gets Generated

First, turn off the stuff you don’t use. In config/application.rb:

config.generators do |g|
    g.test_framework :minitest, spec: false, fixture: true
    g.helper false
    g.stylesheets false
    g.javascripts false
    g.jbuilder false
end

Now rails generate scaffold won’t create helper files, stylesheets, JS files, or jbuilder templates.

Where the Templates Live

Every built-in generator uses ERB templates. Override any of them by placing a file at the matching path inside lib/templates/. Rails checks there before falling back to its own defaults.

Key paths for view and controller customization:

lib/templates/
  erb/scaffold/
    index.html.erb.tt
    show.html.erb.tt
    new.html.erb.tt
    edit.html.erb.tt
    _form.html.erb.tt
    _partial.html.erb.tt
  rails/scaffold_controller/
    controller.rb.tt

The .tt extension means it’s a template that generates a template. <%% outputs a literal <% in the generated file. <%= runs at generation time.

To find the originals, look in the Rails source under railties/lib/rails/generators/erb/scaffold/templates/.

Example: Index View with a Table and Turbo

The default index view is pretty bare. Here’s a lib/templates/erb/scaffold/index.html.erb.tt that generates a table with Turbo Frame support:

<h1><%= plural_table_name.titleize %></h1>

<%%= turbo_frame_tag "<%= plural_table_name %>" do %>
  <table>
    <thead>
      <tr>
      <% attributes.reject(&:password_digest?).each do |attribute| -%>
        <th><%= attribute.human_name %></th>
      <% end -%>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
      <tr>
        <% attributes.reject(&:password_digest?).each do |attribute| -%>
        <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
        <% end -%>
        <td><%%= link_to "Show", <%= singular_table_name %> %></td>
      </tr>
      <%% end %>
    </tbody>
  </table>
<%% end %>

<%%= link_to "New <%= singular_table_name.titleize %>", new_<%= singular_table_name %>_path %>

Example: Controller with Strong Params and Flash Messages

lib/templates/rails/scaffold_controller/controller.rb.tt:

class <%= controller_class_name %>Controller < ApplicationController

    before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]

    def index
        @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
    end

    def show
    end

    def new
        @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
    end

    def edit
    end

    def create
        @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>

        if @<%= singular_table_name %>.save
            redirect_to @<%= singular_table_name %>,
                notice: "<%= human_name %> was created."
        else
            render :new, status: :unprocessable_entity
        end
    end

    def update
        if @<%= singular_table_name %>.update(<%= singular_table_name %>_params)
            redirect_to @<%= singular_table_name %>,
                notice: "<%= human_name %> was updated."
        else
            render :edit, status: :unprocessable_entity
        end
    end

    def destroy
        @<%= singular_table_name %>.destroy
        redirect_to <%= index_helper %>_url,
            notice: "<%= human_name %> was deleted."
    end

    private

    def set_<%= singular_table_name %>
        @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    end

    def <%= singular_table_name %>_params
        <%- if attributes_names.empty? -%>
        params.fetch(:<%= singular_table_name %>, {})
        <%- else -%>
        params.require(:<%= singular_table_name %>)
            .permit(<%= permitted_params %>)
        <%- end -%>
    end
end

Example: Form Partial with Stimulus

lib/templates/erb/scaffold/_form.html.erb.tt that wires up a Stimulus controller for form behavior:

<%%= form_with(model: <%= model_resource_name %>,
    data: { controller: "form", action: "turbo:submit-end->form#onSubmit" }) do |form| %>

  <%% if <%= singular_table_name %>.errors.any? %>
  <div class="alert alert-danger">
    <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prevented saving:</h2>
    <ul>
      <%% <%= singular_table_name %>.errors.each do |error| %>
      <li><%%= error.full_message %></li>
      <%% end %>
    </ul>
  </div>
  <%% end %>

<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
  <div class="mb-3">
    <%%= form.label :password %>
    <%%= form.password_field :password, class: "form-control" %>
  </div>

  <div class="mb-3">
    <%%= form.label :password_confirmation %>
    <%%= form.password_field :password_confirmation, class: "form-control" %>
  </div>
<% elsif attribute.attachments? -%>
  <div class="mb-3">
    <%%= form.label :<%= attribute.column_name %> %>
    <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "form-control" %>
  </div>
<% else -%>
  <div class="mb-3">
    <%%= form.label :<%= attribute.column_name %> %>
    <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "form-control" %>
  </div>
<% end -%>
<% end -%>

  <div>
    <%%= form.submit class: "btn btn-primary" %>
  </div>
<%% end %>

The Show View

lib/templates/erb/scaffold/show.html.erb.tt:

<h1><%%= @<%= singular_table_name %>.<%= attributes.first&.column_name || 'id' %> %></h1>

<dl>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
  <dt><%= attribute.human_name %></dt>
  <dd><%%= @<%= singular_table_name %>.<%= attribute.column_name %> %></dd>
<% end -%>
</dl>

<%%= link_to "Edit", edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
<%%= link_to "Back", <%= index_helper %>_path %>

Tips

  • Use --pretend to preview what a generator will create without writing files: rails g scaffold Post title body:text --pretend
  • Check the Rails source for available variables. The scaffold templates have access to attributes, singular_table_name, plural_table_name, class_name, human_name, orm_class, index_helper, and more.
  • Bootstrap or Tailwind classes go right in the templates. Set them once and every scaffold matches your design system.
  • Test your templates by generating a throwaway scaffold. If the output has syntax errors, the template has a bug — fix it before you forget.

In the next post I extend this approach with ActiveRecord reflection to automatically detect associations and generate smarter views.

Let’s work together! Tell Me About Your Project