Skip to content

garo/elaine_crud

Repository files navigation

ElaineCrud

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

Image

Features

  • 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

Installation

Add to your Gemfile:

gem 'elaine_crud'

Then run:

bundle install

Quick Start

1. Ensure you have ActiveRecord model representing your data.

bin/rails generate model Task title:string description:text priority:integer completed:boolean due_date:date
bin/rails db:migrate

2. Create a Controller for your ActiveRecord Model

This 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
end

3. Add Routes

You 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"
end

4. Ensure Your Application Has a Layout

ElaineCrud 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>

5. Start Your Server

bin/dev

Usage

Extending the default Controller

As 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

end

Virtual fields

It 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
end

Relations

ElaineCrud 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

end

When 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: Image

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

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
  end

You can customise the rendering:

  field :department_id do
    foreign_key model: Department, display: ->(record) { "#{record.name} (#{record.id})" }
  end

You 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) }
    )
  end

By 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
end

For 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.

Misc options

# 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)
end

Examples

More detailed example with customisations

A 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

Requirements

  • Rails 7.0+

Architecture

See ARCHITECTURE.md for detailed technical documentation.

Contributing

  1. Open a new issue and suggest a feature
  2. Fork the repository
  3. Create a feature branch
  4. Make your changes
  5. Add tests
  6. Submit a pull request

License

MIT License. See LICENSE for details.

About

Fast CRUD library for Ruby on Rails applications

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages