- Notifications
You must be signed in to change notification settings - Fork227
A wrapper around DataTable's ajax methods that allow synchronization with server-side pagination in a Rails app
License
jbox-web/ajax-datatables-rails
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Important : This gem is targeted at DataTables version 1.10.x.
It's tested against :
- Rails: 7.1 / 7.2 / 8.0
- Ruby: 3.1 / 3.2 / 3.3 / 3.4
- Databases: MySQL 8 / SQLite3 / Postgresql 16 / Oracle XE 11.2 (thanks totravis-oracle)
- Adapters: sqlite / mysql2 / trilogy / postgres / postgis / oracle
DataTables is a nifty jQuery plugin that adds the ability to paginate, sort, and search your html tables.When dealing with large tables (more than a couple of hundred rows) however, we run into performance issues.These can be fixed by using server-side pagination, but this breaks some DataTables functionality.
ajax-datatables-railsis a wrapper around DataTables ajax methods that allow synchronization with server-side pagination in a Rails app.It was inspired by thisRailscast.I needed to implement a similar solution in a couple projects I was working on, so I extracted a solution into a gem.Joel Quenneville (original author)
I needed a good gem to manage a lot of DataTables so I chose this one :)
Nicolas Rodriguez (current maintainer)
The final goal of this gem is togenerate a JSON content that will be given to jQuery DataTables.All the datatable customizations (header, tr, td, css classes, width, height, buttons, etc...)must take place in thejavascript definition of the datatable.jQuery DataTables is a very powerful tool with a lot of customizations available. Take the time toread the doc.
You'll find a sample project here :https://ajax-datatables-rails.herokuapp.com
Its real world examples. The code is here :https://github.com/jbox-web/ajax-datatables-rails-sample-project
Add these lines to your application's Gemfile:
gem'ajax-datatables-rails'
And then execute:
$ bundle install
We assume here that you have already installedjQuery DataTables.
You can install jQuery DataTables :
- with the
jquery-datatablesgem - by adding the assets manually (in
vendor/assets) - withRails webpacker gem (seehere for more infos)
CurrentlyAjaxDatatablesRails only supportsActiveRecord as ORM for performing database queries.
Adding support forSequel,Mongoid andMongoMapper is (more or less) a planned feature for this gem.
If you'd be interested in contributing to speed development, pleaseopen an issue and get in touch.
The following examples assume that we are setting upajax-datatables-rails for an index page of users from aUser model,and that we are using Postgresql as our db, because youshould be using it. (It also works with other DB,see above)
The goal is to render a users table and display :id,first name,last name,email, andbio for each user.
Something like this:
| ID | First Name | Last Name | Brief Bio | |
|---|---|---|---|---|
| 1 | John | Doe | john.doe@example.net | Is your default user everywhere |
| 2 | Jane | Doe | jane.doe@example.net | Is John's wife |
| 3 | James | Doe | james.doe@example.net | Is John's brother and best friend |
Here the steps we're going through :
- Generate the datatable class
- Build the View
- Customize the generated Datatables class
- Setup the Controller action
- Wire up the Javascript
Run the following command:
$ rails generate datatable User
This will generate a file nameduser_datatable.rb inapp/datatables.Open the file and customize in the functions as directed by the comments.
Take a lookhere for an explanation about the generator syntax.
You should always start by the single source of truth, which is your html view.
- Set up an html
<table>with a<thead>and<tbody> - Add in your table headers if desired
- Don't add any rows to the body of the table, DataTables does this automatically
- Add a data attribute to the
<table>tag with the url of the JSON feed, in our case is theusers_pathas we're pointing to theUsersController#indexaction
<tableid="users-datatable"data-source="<%= users_path(format: :json) %>"><thead><tr><th>ID</th><th>First Name</th><th>Last Name</th><th>Email</th><th>Brief Bio</th></tr></thead><tbody></tbody></table>
First we need to declare inview_columns the list of the model(s) columns mapped to the data we need to present.In this case:id,first_name,last_name,email andbio.
This gives us:
defview_columns@view_columns ||={id:{source:"User.id"},first_name:{source:"User.first_name",cond::like,searchable:true,orderable:true},last_name:{source:"User.last_name",cond::like,nulls_last:true},email:{source:"User.email"},bio:{source:"User.bio"},}end
Notes : by defaultorderable andsearchable are true andcond is:like.
cond can be :
:like,:start_with,:end_with,:string_eq,:string_infor string or full text search:eq,:not_eq,:lt,:gt,:lteq,:gteq,:infor numeric:date_rangefor date range:null_valuefor nil fieldProcfor whatever (seehere for real example)
Thenulls_last param allows for nulls to be ordered last. You can configure it by column, like above, or by datatable class :
classMyDatatable <AjaxDatatablesRails::ActiveRecordself.nulls_last=true# ... other methods (view_columns, data...)end
Seehere to get more details about columns definitions and how to play with associated models.
You can customize or sanitize the search value passed to the DB by using the:formatter option with a lambda :
defview_columns@view_columns ||={id:{source:"User.id"},first_name:{source:"User.first_name"},last_name:{source:"User.last_name"},email:{source:"User.email",formatter:->(o){o.upcase}},bio:{source:"User.bio"},}end
The object passed to the lambda is the search value.
Then we need to map the records retrieved by theget_raw_records method to the real values we want to display :
defdatarecords.mapdo |record|{id:record.id,first_name:record.first_name,last_name:record.last_name,email:record.email,bio:record.bio,DT_RowId:record.id,# This will automagically set the id attribute on the corresponding <tr> in the datatable}endend
Deprecated: You can either use the v0.3 Array style for your columns :
This method builds a 2d array that is used by datatables to construct the htmltable. Insert the values you want on each column.
defdatarecords.mapdo |record|[record.id,record.first_name,record.last_name,record.email,record.bio]endend
The drawback of this method is that you can't pass theDT_RowId so it's tricky to set the id attribute on the corresponding<tr> in the datatable (need to be done on JS side).
See here if you need to use view helpers likelink_to,mail_to, etc...
This is where your query goes.
defget_raw_recordsUser.allend
Obviously, you can construct your query as required for the use case the datatable is used.
Example:
defget_raw_recordsUser.active.with_recent_messagesend
You can put any logic inget_raw_recordsbased on any parameters you inject in theDatatable object.
IMPORTANT : Because the result of this method will be chained toActiveRecord methods for sorting, filtering and pagination,make sure to return anActiveRecord::Relation object.
You can inject other key/value pairs in the rendered JSON by defining the#additional_data method :
defadditional_data{foo:'bar'}end
Very useful withdatatables-factory (oryadcf) to provide values for dropdown filters.
Set the controller to respond to JSON
defindexrespond_todo |format|format.htmlformat.json{renderjson:UserDatatable.new(params)}endend
Don't forget to make sure the proper route has been added toconfig/routes.rb.
See here if you need to inject params in theUserDatatable.
Note : If you have more than2 datatables in your application, don't forget to readthis.
Finally, the javascript to tie this all together. In the appropriatecoffee file:
# users.coffee$->$('#users-datatable').dataTableprocessing:trueserverSide:trueajax:url:$('#users-datatable').data('source')pagingType:'full_numbers'columns: [ {data:'id'} {data:'first_name'} {data:'last_name'} {data:'email'} {data:'bio'} ]# pagingType is optional, if you want full pagination controls.# Check dataTables documentation to learn more about# available options.
or, if you're using plain #"auto" data-snippet-clipboard-copy-content="// users.jsjQuery(document).ready(function() { $('#users-datatable').dataTable({ "processing": true, "serverSide": true, "ajax": { "url": $('#users-datatable').data('source') }, "pagingType": "full_numbers", "columns": [ {"data": "id"}, {"data": "first_name"}, {"data": "last_name"}, {"data": "email"}, {"data": "bio"} ] // pagingType is optional, if you want full pagination controls. // Check dataTables documentation to learn more about // available options. });});">
// users.jsjQuery(document).ready(function(){$('#users-datatable').dataTable({"processing":true,"serverSide":true,"ajax":{"url":$('#users-datatable').data('source')},"pagingType":"full_numbers","columns":[{"data":"id"},{"data":"first_name"},{"data":"last_name"},{"data":"email"},{"data":"bio"}]// pagingType is optional, if you want full pagination controls.// Check dataTables documentation to learn more about// available options.});});
Sometimes you'll need to use view helper methods likelink_to,mail_to,edit_user_path,check_box_tag and so on in the returned JSON representation returned by thedata method.
To have these methods available to be used, this is the way to go:
classUserDatatable <AjaxDatatablesRails::ActiveRecordextendForwardable# either define them one-by-onedef_delegator:@view,:check_box_tagdef_delegator:@view,:link_todef_delegator:@view,:mail_todef_delegator:@view,:edit_user_path# or define them in one passdef_delegators:@view,:check_box_tag,:link_to,:mail_to,:edit_user_path# ... other methods (view_columns, get_raw_records...)definitialize(params,opts={})@view=opts[:view_context]superend# now, you'll have these methods available to be used anywheredefdatarecords.mapdo |record|{id:check_box_tag('users[]',record.id),first_name:link_to(record.first_name,edit_user_path(record)),last_name:record.last_name,email:mail_to(record.email),bio:record.bioDT_RowId:record.id,}endendend# and in your controller:defindexrespond_todo |format|format.htmlformat.json{renderjson:UserDatatable.new(params,view_context:view_context)}endend
If you want to keep things tidy in the data mapping method, you could useDraper to define column mappings like below.
Note : This is the recommanded way as you don't need to inject theview_context in the Datatable object to access helpers methods.It also helps in separating view/presentation logic from filtering logic (the only one that really matters in a datatable class).
Example :
classUserDatatable <AjaxDatatablesRails::ActiveRecord ...defdatarecords.mapdo |record|{id:record.decorate.check_box,first_name:record.decorate.link_to,last_name:record.decorate.last_nameemail:record.decorate.email,bio:record.decorate.bioDT_RowId:record.id,}endend ...endclassUserDecorator <ApplicationDecoratordelegate:last_name,:biodefcheck_boxh.check_box_tag'users[]',object.idenddeflink_toh.link_toobject.first_name,h.edit_user_path(object)enddefemailh.mail_toobject.emailend# Just an example of a complex method you can add to you decorator# To render it in a datatable just add a column 'dt_actions' in# 'view_columns' and 'data' methods and call record.decorate.dt_actionsdefdt_actionslinks=[]links <<h.link_to'Edit',h.edit_user_path(object)ifh.policy(object).update?links <<h.link_to'Delete',h.user_path(object),method::delete,remote:trueifh.policy(object).destroy?h.safe_join(links,'')endend
AnAjaxDatatablesRails::ActiveRecord inherited class can accept an options hash at initialization. This provides room for flexibility when required.
Example:
# In the controllerdefindexrespond_todo |format|format.htmlformat.json{renderjson:UserDatatable.new(params,user:current_user,from:1.month.ago)}endend# The datatable classclassUnrespondedMessagesDatatable <AjaxDatatablesRails::ActiveRecord# ... other methods (view_columns, data...)defuser@user ||=options[:user]enddeffrom@from ||=options[:from].beginning_of_dayenddefto@to ||=Date.today.end_of_dayend# We can now customize the get_raw_records method# with the options we've injecteddefget_raw_recordsuser.messages.unresponded.where(received_at:from..to)endend
If you have models from different databases you can set thedb_adapter on the datatable class :
classMySharedModelDatatable <AjaxDatatablesRails::ActiveRecordself.db_adapter=:oracle_enhanced# ... other methods (view_columns, data...)defget_raw_recordsAnimalsRecord.connected_to(role::reading)doDog.allendendend
You can mix several model in the same datatable.
Suppose we have the following models:User,PurchaseOrder,Purchase::LineItem and we need to have several columns from those modelsavailable in our datatable to search and sort by.
# we use the ModelName.column_name notation to declare our columnsdefview_columns@view_columns ||={first_name:{source:'User.first_name'},last_name:{source:'User.last_name'},order_number:{source:'PurchaseOrder.number'},order_created_at:{source:'PurchaseOrder.created_at'},quantity:{source:'Purchase::LineItem.quantity'},unit_price:{source:'Purchase::LineItem.unit_price'},item_total:{source:'Purchase::LineItem.item_total }'}end
The previous example has only one single model. But what about if you havesome associated nested models and in a report you want to show fields fromthese tables.
Take an example that has anEvent, Course, CourseType, Allocation, Teacher, Contact, Competency and CompetencyType models. We want to have a datatablesreport which has the following column:
'course_types.name''courses.name''contacts.full_name''competency_types.name''events.title''events.event_start''events.event_end''events.status'
We want to sort and search on all columns of the list.The related definition would be :
defview_columns@view_columns ||={course_type:{source:'CourseType.name'},course_name:{source:'Course.name'},contact_name:{source:'Contact.full_name'},competency_type:{source:'CompetencyType.name'},event_title:{source:'Event.title'},event_start:{source:'Event.event_start'},event_end:{source:'Event.event_end'},event_status:{source:'Event.status'},}enddefget_raw_recordsEvent.joins({course::course_type},{allocations:{teacher:[:contact,{competencies::competency_type}]}}).distinctend
Some comments for the above code :
In the
get_raw_recordsmethod we have quite a complex query having one tomany and many to many associations using the joins ActiveRecord method.The joins will generate INNER JOIN relations in the SQL query. In this case,we do not include all event in the report if we have events which is notassociated with any model record from the relation.To have all event records in the list we should use the
.includesmethod,which generate LEFT OUTER JOIN relation of the SQL query.
IMPORTANT :
Make sure to append.references(:related_model) with anyassociated model. That forces the eager loading of all the associated modelsby one SQL query, and the search condition for any column works fine.Otherwise the:recordsFiltered => filter_records(get_raw_records).count(:all)will generate 2 SQL queries (one for the Event model, and then another for theassociated tables). The:recordsFiltered => filter_records(get_raw_records).count(:all)will use only the first one to return from the ActiveRecord::Relation objectinget_raw_records and you will get an error message ofUnknown column'yourtable.yourfield' in 'where clause' in case the search field valueis not empty.
So the query using the.includes() method is:
defget_raw_recordsEvent.includes({course::course_type},{allocations:{teacher:[:contact,{competencies::competency_type}]}}).references(:course).distinctend
SeeDefaultScope is evil and#223 and#233.
This feature works withdatatables-factory (oryadcf).
To enable the date range search, for examplecreated_at :
- add a
created_at<th>in your html - declare your column in
view_columns:created_at: { source: 'Post.created_at', cond: :date_range, delimiter: '-yadcf_delim-' } - add it in
data:created_at: record.decorate.created_at - setup yadcf to make
created_atsearch field a range
Also, a class that inherits fromAjaxDatatablesRails::ActiveRecord is not tied to anexisting model, module, constant or any type of class in your Rails app.You can pass a name to your datatable class like this:
$ rails generate datatable users# returns a users_datatable.rb file with a UsersDatatable class$ rails generate datatable contact_messages# returns a contact_messages_datatable.rb file with a ContactMessagesDatatable class$ rails generate datatable UnrespondedMessages# returns an unresponded_messages_datatable.rb file with an UnrespondedMessagesDatatable class
In the end, it's up to the developer which model(s), scope(s), relationship(s)(or else) to employ inside the datatable class to retrieve records from thedatabase.
Datatables can be tested with Capybara provided you don't use Webrick during integration tests.
Long story short and as a rule of thumb : use the same webserver everywhere (dev, prod, staging, test, etc...).
If you use Puma (the Rails default webserver), use Puma everywhere, even in CI/test environment. The same goes for Thin.
You will avoid the usual story : it works in dev but not in test environment...
If you want to test datatables with a lot of data you might need this kind of tricks :https://robots.thoughtbot.com/automatically-wait-for-ajax-with-capybara. (thanks CharlieIGG)
In the same spirit of RailsApplicationController andApplicationRecord, you can create anApplicationDatatable class (inapp/datatables/application_datatable.rb)that will be inherited from other classes :
classApplicationDatatable <AjaxDatatablesRails::ActiveRecord# puts commonly used methods hereendclassPostDatatable <ApplicationDatatableend
This way it will be easier to DRY you datatables.
Installyajl-ruby, basically :
gem'yajl-ruby',require:'yajl'
then
$ bundle install
That's all :) (Automatically prefer Yajl or JSON backend over Yaml, if available)
Use HTTPPOST method to avoid414 Request-URI Too Large error. See :#278 and#308.
You can easily define a route concern inconfig/routes.rb and reuse it when you need it :
Rails.application.routes.drawdoconcern:with_datatabledopost'datatable',on::collectionendresources:posts,concerns:[:with_datatable]resources:users,concerns:[:with_datatable]end
then in your controllers :
# PostsControllerdefindexenddefdatatablerenderjson:PostDatatable.new(params)end# UsersControllerdefindexenddefdatatablerenderjson:UserDatatable.new(params)end
then in your views :
# posts/index.html.erb<tableid="posts-datatable"data-source="<%= datatable_posts_path(format: :json) %>"># users/index.html.erb<tableid="users-datatable"data-source="<%= datatable_users_path(format: :json) %>">
then in your Coffee/JS :
# send params in form data$->$('#posts-datatable').dataTableajax:url:$('#posts-datatable').data('source')type:'POST'# ...others options, see [here](#5-wire-up-the-javascript)# send params as json data$->$('#users-datatable').dataTableajax:url:$('#users-datatable').data('source')contentType:'application/json'type:'POST'data: (d)->JSON.stringify d# ...others options, see [here](#5-wire-up-the-javascript)
In order to speed up theILIKE queries that are executed when using the default configuration, you might want to consider adding some indices.For postgresql, you are advised to use thegin/gist index type.This makes it necessary to enable the postgrsql extensionpg_trgm. Double check that you have this extension installed before trying to enable it.A migration for enabling the extension and creating the indices could look like this:
defchangeenable_extension:pg_trgmTEXT_SEARCH_ATTRIBUTES=['your','attributes']TABLE='your_table'TEXT_SEARCH_ATTRIBUTES.eachdo |attr|reversibledo |dir|dir.updoexecute"CREATE INDEX#{TABLE}_#{attr}_gin ON#{TABLE} USING gin(#{attr} gin_trgm_ops)"enddir.downdoremove_indexTABLE.to_sym,name:"#{TABLE}_#{attr}_gin"endendendend
Filtering by JSONB column values :#277
Usehas_scope gem withajax-datatables-rails :#280
UseDatatable orthogonal data : see#269
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Added some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
About
A wrapper around DataTable's ajax methods that allow synchronization with server-side pagination in a Rails app
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.