RecordCollection
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 aliasoptional_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)
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 😄.
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
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:
 Â
Contributing
- Fork it ( https://github.com/bterkuile/record_collection/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request