A Rails engine for rapidly generating CRUD interfaces for ActiveRecord models with minimal configuration. This controller will create you the following CRUD interface:
class TaskController < ElaineCrud::BaseController
layout 'application'
model Task # Your ActiveRecord model
permit_params :title, :description, :priority, :completed, :due_date
end- Zero Configuration: Works out of the box with any ActiveRecord model
- Rails Conventions: Follows standard Rails patterns
- Minimal Code: Just specify model and permitted params
- Search & Filter: Built-in search across text fields
- Sortable Columns: Click column headers to sort
- Pagination: Automatic pagination with configurable page size
- Export: Download data as CSV, Excel, or JSON
- Extensible: Override any view or behavior in your host app
Add to your Gemfile:
gem 'elaine_crud'Then run:
bundle installbin/rails generate model Task title:string description:text priority:integer completed:boolean due_date:date
bin/rails db:migrateThis controller handles one ActiveRecord model and presents a CRUD interface to the user. It derives from the ElaineCrud::BaseController, which provides good defaults for how the CRUD view should operate. The controller requires a working layout (see step 4).
The main configuration is setting the ActiveRecord model using the model Task keyword, where Task is an ActiveRecord model. You also need to explicitly specify which model fields are permitted using the permit_params command.
The following example controller provides a fully working CRUD view for the ActiveRecord with pagination, sorting, filtering and exporting. You can customise the behaviour, which is explained in more details later.
class TaskController < ElaineCrud::BaseController
layout 'application' # Use your app's layout (wraps ElaineCrud's content)
model Task
permit_params :title, :description, :priority, :completed, :due_date
endYou need to add the created Controller to the Rails routes.rb. In this example you simply declare resources :tasks where :tasks maps to the TaskController.
# config/routes.rb
Rails.application.routes.draw do
resources :tasks
root "tasks#index"
endElaineCrud is a content-only engine - it provides CRUD views but relies on your application to provide the HTML structure (layout, navigation, styling).
Your Rails app should have a layout file (typically app/views/layouts/application.html.erb) that includes:
- Basic HTML structure (
<html>,<head>,<body>) - TailwindCSS stylesheets
- JavaScript imports (including Turbo)
- Navigation/header/footer (optional, your choice)
Here's an example layout file, which contains the critical stylesheet_link_tag for elaine_crud:
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Taskmanager" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "elaine_crud", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body class="bg-gray-100">
<main class="w-full px-4 py-6">
<%= yield %>
</main>
<%# Modal container for nested record creation %>
<%= render 'elaine_crud/base/modal' %>
</body>
</html>bin/devAs described earlier, this example controller uses a lot of default from the ElaineCrud::BaseController.
One of the first things user usually wants to customise is how a certain ActiveRecord property is rendered to the user.
class TaskController < ElaineCrud::BaseController
layout 'application' # Use your app's layout (wraps ElaineCrud's content)
model Task
permit_params :title, :description, :priority, :completed, :due_date
# Configures the ActiveRecord field :title
field :title do
# Human readable name for the field, shown in the column header.
title "Task title"
# Description is shown in the edit form
description "Write a short title for your task, so that it's easily understandable"
# Custom rendering. `value` is the raw value of the field and `record` is the entire ActiveRecord
display_as { |value, record| "<b>#{value}</b>" if value.present? }
# By default text columns are searchable but not filterable. You can set a field to be filterable
# so that it shows in Advanced Filters section, but then it's no longer searchable
filterable true
# By default text columns are searchable and other columns are not
searchable false
end
field :priority do
# Gives a html dropdown for available options
options [1,2,3,4,5]
end
# `read_only` can be used prevent field from being edited
field :modified_at do
read_only true
end
# We can also hide a field completely
field :created_at do
hidden true
end
endIt is possible to create a virtual column which is not backed by any actual field in the ActiveRecord, but instead its contents is calculated based on other fields. For example if your Order has amount and price_per_unit fields, you could have a total_cost virtual field:
class OrderController < ElaineCrud::BaseController
layout 'application' # Use your app's layout (wraps ElaineCrud's content)
model Order
permit_params :product, :amount, :price_per_unit
# The field `total_cost` simply does not exists in the ActiveRecord model
field :total_cost do
readonly true
display_as { |value,record|
# The `value` parameter will be nil as it does not exists.
record.amount * record.price_per_unit
}
end
endElaineCrud supports ActiveRecord relations automatically, if they are configured in the underlying ActiveRecords. Lets assume these models:
bin/rails generate model Department name:string
bin/rails generate model Employee name:string email:string department:references
bin/rails db:migrate
# app/models/department.rb
class Department < ApplicationRecord
has_many :employees
end
# app/models/employee.rb
class Employee < ApplicationRecord
belongs_to :department
end
We can then have a simple EmployeesController and DepartmentsController
# app/controllers/employees_controller.rb
class EmployeesController < ElaineCrud::BaseController
layout 'application'
model Employee
permit_params :name, :email, :department_id
end
# app/controllers/departments_controller.rb
class DepartmentsController < ElaineCrud::BaseController
layout 'application'
model Department
permit_params :name
endWhen you set model Task in your controller, ElaineCrud automatically detect all belongs_to relationships on the model
and configures the fields accordingly. This will result a dropdown menu to select the belongs_to target, like this:

Also the Departments controller will have a back reference to show how many Employees are bound to the Department.

You can customise the way how the foreign key behaves with the foreign_key configuration command. You can replicate the
default behavious by setting model parameter to the ActiveRecord to which the relation belongs to and setting display to the name
of the field in the belongs_to model which should be displayed to the user.
field :department_id do
foreign_key model: Department, display: :name
endYou can customise the rendering:
field :department_id do
foreign_key model: Department, display: ->(record) { "#{record.name} (#{record.id})" }
endYou can also limit which entries are shown in the dropdown using the scope filtering tool:
field :department_id do
foreign_key(
model: Department,
display: :name,
scope: -> { Department.where(active: true).order(:name) }
)
endBy default, you need to have the Department created in advance before creating a new Employee.
To make this easier you can also enable nested object creation. For this you would use the nested_create and foreign_key properties.
class EmployeesController < ElaineCrud::BaseController
layout 'application'
model Employee
permit_params :name, :email, :department_id
field :department_id do
foreign_key model: Department, display: :name
nested_create true
end
endFor this to work you need to have the modal container in your layout (<%= render 'elaine_crud/base/modal' %>), just before the closing body tag. We already had this in the layout example above.
# app/controllers/employees_controller.rb
class EmployeesController < ElaineCrud::BaseController
layout 'application'
model Employee
permit_params :name, :email, :department_id
# Sets default sorting when the controller is opened
default_sort column: :email, direction: :asc
# Instead of editing a row in the inline editor using turbo, the Edit button
# opens /controller/#{id}/edit page
disable_turbo
# This adds a View button next to Edit and Delete buttons, which links to
# /controller/#{id}
show_view_button
# Limit how many rows the export functionality will be exporting. The default is 10000
max_export(5000)
endA more comprehensive example showing custom field formatting, sorting, and relationships. This example uses the more verbose block syntax with fields. Both approaches work and are valid.
# app/controllers/products_controller.rb
class ProductsController < ElaineCrud::BaseController
layout 'application'
model Product
permit_params :name, :description, :price, :stock_quantity, :category_id, :active
# Sort by name alphabetically by default
default_sort column: :name, direction: :asc
# Show the View button in actions column
show_view_button
# Format price as currency
field :price do |f|
f.title "Price"
f.display_as { |value, record| number_to_currency(value) if value.present? }
end
# Custom display for stock status
field :stock_quantity do |f|
f.title "Stock"
f.display_as { |value, record|
if value.to_i > 10
content_tag(:span, "#{value} in stock", class: "text-green-600")
elsif value.to_i > 0
content_tag(:span, "Low: #{value}", class: "text-yellow-600 font-semibold")
else
content_tag(:span, "Out of stock", class: "text-red-600 font-semibold")
end
}
end
# Boolean with badge display
field :active do |f|
f.title "Status"
f.display_as { |value, record|
if value
content_tag(:span, "Active", class: "px-2 py-1 text-xs rounded bg-green-100 text-green-800")
else
content_tag(:span, "Inactive", class: "px-2 py-1 text-xs rounded bg-gray-100 text-gray-600")
end
}
end
# Foreign key - automatically renders as dropdown in forms
field :category_id do |f|
f.foreign_key model: Category, display: :name
end
end- Rails 7.0+
See ARCHITECTURE.md for detailed technical documentation.
- Open a new issue and suggest a feature
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT License. See LICENSE for details.
