RecordCollection

Gem Version Build Status Code Climate Test Coverage

record_collection is a gem that adds functionality to rails to work with collections. This consists of a few components:

  • Collection objects containing some active record models and acting on that collection.
  • the multi_select helpers for selecting records from the index page
  • the optionals helpers for managing attributes on the collection of records you may or may not want to edit in the collection form

This gem is created for a project where acting on collections of records was a key feature of the application. If you require really heavy client side logic on collections, a client side framework might be the way to go treating all records CRUD individually. This gem add two 'resourceful' actions to a resource controller to act on collections (a subset) of your records. Advanced failure handling is missing. Normal active model validation is implemented.

Installation

Add this line to your application's Gemfile:

gem 'record_collection'

And then execute:

$ bundle

Or install it yourself as:

$ gem install record_collection

Adding routes

Add two collection routes to the normal resources definition. This call behaves exactly as the normal resources :... call, but adds:

 collection do
   get :collection_edit
   post :collection_update
 end

So the route definition in config/routes.rb defined as:

  collection_resources :employees, except: [:new]

is exactly the same as:

  resources :employees, except: [:new] do
    collection do
      get :collection_edit
      post :collection_update
    end
  end

Defining the collection

A good practice is to define your collection as a subclass of your resource class. So an employees collection should be defined like: app/models/employee.rb:

class Employee < ActiveRecord::Base
  # attribute :admin, type: Boolean (defined by database)
  validates :name, presence: true

end

app/models/employee/collection.rb:

class Employee::Collection < RecordCollection::Base
  attribute :name
  validates :section, format: { with: /\A\w{3}\Z/ }
  attribute :admin, type: Boolean
  attribute :vegan, type: Boolean
end

See the active_attr gem for attribute definitions.

Validations

The validations for the collection are exactly the same as your active_model validations. The only difference is that the allow_nil: true option is standard set to true. Since a nil value of a collection attribute means you do not want to change that value for the individual records. To make an attribute explicitly required for a collection add the allow_nil option:

  validates :email, email: true, allow_nil: false

If the update on a record by the collection results in an invalid the record will not be updated and the collection will not (yet) give the feedback. The future idea is to create a #invalid_records attribute that will contain those records

The .record_class attribute

The record collection needs to know the class of the records it is containing, since it need to share some of its behaviour. To do this a collection assumes that it is subclassed by the model, eg:

class Project::Prince2::Collection < RecordCollection::Base
end

Project::Prince2::Collection.record_class #=> Project::Prince2

If this is not the case, you have to define the record_class manually:

class MyAwesomeCollection < RecordCollection::Base
  self.record_class = LpRecord
end

The before_record_update hook

The collection implements a general update(attributes) method that will update all the attributes that are set in the collection on the records it contains. If you want precondition your data you can do so in this hook:

class Project::Prince2::Collection < RecordCollection::Base
  before_record_update do |record|
    record.plan_date_set = true if plan_date.present?
  end
end

The after_record_update hook

The collection implements a general update(attributes) method that will update all the attributes that are set in the collection on the records it contains. If you want to trigger a conditional for example a state machine trigger, you can do it like:

class Project::Prince2::Collection < RecordCollection::Base
  after_record_update do |record|
    record.is_planned! if record.plan_date.present?
  end
end

Defining your controllers

If you already used the specification collection_resources :employees in your config/routes.rb file you can add the actions in your controller typically looking like:

class EmployeesController < ApplicationController
  # your standard actions here

  # GET /employees/collection_edit?ids[]=1&ids[]=3&...
  def collection_edit
    if params[:batch_id].present? # This is for feature demo, not for controller code practice
      @collection = Employee::Collection.joins(:project).where(projects: {batch_id: params[:batch_id]})
    else
      @collection = Employee::Collection.find(params[:ids])
    end
    redirect_to employees_path, alert: 'No employees selected' if @collection.empty?
  end

  # POST /employees/collection_update
  def collection_update
    @collection = Employee::Collection.find(params[:ids])
    if @collection.update params[:collection]
      redirect_to employees_path, notice: 'Collection is updated'
    else
      render 'collection_edit'
    end
  end  
end

For more advanced use of the collection the pattern above can of course be different eg: different collection objects for the same active record model types.

Creating your views

The app/views/employess/collection_edit.html.slim view is a tricky one. Since we are working on a collection of record, and want to edit those attributes we just want a normal form for editing the attributes, treating the collection as the record itself. The problem however is that some attributes can be in a mixed state, say two employees, one having admin => true, the other one admin => false. If I only want to update the section they are both in, I want to leave the admin attribute allone. To accomplish this, this gem provides the optional helpers. These helpers make it easy to manage a form of attributes where you can determine which attributes you want to manage for this particular collection of records. This gem also support simple_form gem where you can replace f.input :attribute, ...etc with f.optional_input :attribute, ...etc. Our current example works with the standard form_helpers

currently supported helpers:

  • optional_boolean with alias optional_check_box
  • optional_text_field
  • optional_text_area
  • optional_input (simple_form)

The form you create typically looks like app/views/employees/collection_edit.html.slim:

h1 Edit multiple employees
= form_for @collection, url: [:collection_update, @collection] do |f|
  = f.collection_ids
  .form-inputs= f.optional_text_field :section
  .form-inputs= f.optional_boolean :admin
  .form-inputs= f.optional_boolean :vegan
  .form-actions= f.submit
.page-actions
  = link_to 'Back', employees_path

That is the view part. Be sure to read the optionals section for a better understanding of how the optional fields work.

Selecting records from the index using checkboxes (multi_select)

Screenshot

The idea behind working with collections is that you end up as a GET request at: +controller+/collection_edit?ids[]=2&ids[]=3 etc. How you achieve this is totally up to yourself, but this gem provides you with a nice standard way of selecting records from the index page. To filter records to a specific subset the ransack gem also provides a nice way to add filtering to the index page. To add checkbox selecting to your page this gem assumes the following structure using the Slim lang

app/views/employees/index.html.slim

h1 Listing Employees
table.with-selection
  thead
    tr
      th Name
      th Section
  tbody
    - @employees.each do |employee|
      tr data-record=employee.attributes.to_json
        td= employee.name
        td= employee.section

Note that each row needs a json version of the record at least containing its id.
Implement the multiselect dependencies in your manifest files, typically being app/assets/javascripts/application.js:

//= require record_collection/multi_select
// Or require record_collection/all for all components

And for the styling provided by this gem (app/assets/stylesheets/application.css):

/*
 *= require record_collection/multi_select
 * Or require record_collection/all for all components
 */

The styling uses the font-awesome-rails gem, so this gem should be present in your Gemfile:

gem 'font-awesome-rails'

Of course you are welcome to create your own awesome styling and send it to me so I can add it as a theme :smile:.

To activate multi_select for your page put in your jQuery onload function:

$(function(){
  $(document).multi_select()
});

You can also apply it to dynamically loaded html replacing document for the html added to your page:

$.get('/ajax-page.html', function(response){
  $('#ajax-container').html(response);
  $('#ajax-container').multi_select();
});

The selection action button

Selecting records from the tabble is the first step. Then going to the edit page to edit the selection is another. At the moment there is not yet a standardized solution in the record_collection gem, but with your suggestions there will be one in the future. A current method can be:

table.with-selection
  ...
  tfoot
    tr
      td
        button#selected-records-action Actions

And in your app/assets/javascripts/application.js.coffee

$ ->
  if selector = $(document).multi_select()
    $('#selected-records-action').click ->
      ids = selector.selected_ids()
      return alert "No records selected" unless ids.length
      window.location = "/employees/collection_edit?#{$.param(ids: ids)}"

This indicates the controll you can implement on your collectoins. Another way could be to use the less advicable way when the js-routes gem is added to just have a button like:

<button onclick="window.location = Routes.collection_edit_employees_path({ids: MultiSelect.selected_ids()})">Actions</button>

without any extra javascript.

Optionals

Screenshot Screenshot

Optionals is the name for the feature in this gem that activates collection attributes to be sumitted in the form or not. Since for a mixed collection on an attribute you might not want to edit, but another attribute you do want to edit you add the optionals functionality to your manifests. This is similar to the multi_select feature:

//= require record_collection/optionals
// Or require record_collection/all for all components

And for the styling provided by this gem (app/assets/stylesheets/application.css):

/*
 *= require record_collection/optionals
 * Or require record_collection/all for all components
 */

To activate the optionals for your page put in your jQuery onload function:

$(function(){
  $(document).optionals()
});

You can also apply it to dynamically loaded html replacing document for the html added to your page.

TODO: more and better explanation about optionals

I18n translations

To manipulate the name of a collection and the standard f.submit form label text add the following translation file config/locales/record_collection.en.yml

en:
  activerecord:
    collections:
      employee: Group
  helpers:
    submit:
      collection:
        create: "Update %{model}"

Generators

There is a scaffold generator available for collection resources. The behaviour is very similar to the normal scaffold generator:

rails g collection_scaffold Project name:string finished:boolean description:text

This will generate the routes, model, migration, collection model and views.

NOTE: At the moment only haml support for generated views. Also note that the generators make an assumption about having the following translations available:

en:
  action:
    create:
      successful: Successfully created %{model}
    update:
      successful: Successfully updated %{model}
    collection_update:
      successful: Successfully updated %{model} collection
    destroy:
      successful: Successfully destroyed %{model}
    new:
      link: New
    index:
      link: Back
    edit:
      link: Edit
    show:
      link: Show

Special thanks

Special thanks for this project goes to:
Companytools    FourStack

Contributing

  1. Fork it ( https://github.com/bterkuile/record_collection/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request