SmartListing

Easily create AJAX-enabled data lists
with out-of-the-box support for sorting, filtering and in-place editing.

Ruby on Rails gem with built-in Twitter Bootstrap 3 support.

Get it!

Just add it to your Gemfile:

gem "smart_listing"

You can also fetch it from our GitHub repository.

Usage

You'll find here demonstration of few use cases where SmartListing comes in handy.

For more detailed technical docs, please refer to README.md.

Plain list

This is the simplest use of SmartListing. Just declare your list in the controller and render it in views.

def index
  # Create your list consisting of all active users
  smart_listing_create :users, User.active, partial: "users/list"
end
-# Render list
= smart_listing_render :users
<%# Render list for XHR requests %>
<%= smart_listing_update :users %>
- unless smart_listing.empty?
  %table.table.table-striped
    %thead
      %th.col-md-3= "Name"
      %th.col-md-3= "Phone"
      %th.col-md-4= "Location"
      %th
        %span.glyphicon.glyphicon-shopping-cart{title: "Has credit card?"}
      %th.col-md-2= "Wallet saldo"
    %tbody
      - smart_listing.collection.each do |o|
        %tr
          -# User row goes here...
      
  -# Render nice pagination links fitted for Bootstrap 3 by default
  = smart_listing.paginate
  = smart_listing.pagination_per_page_links
- else
  %p.warning No records!
Name Phone Location Wallet saldo
Kimberly Romero 0-(135)527-3971 Springfield Nuclear Power Plant 1776.29
Michael Sullivan 5-(367)591-2872 Churchill Downs 9817.90
Jessica Carroll 9-(294)417-1260 Try-N-Save 7397.37
Margaret Williams 7-(120)368-7292 The Palace of Westminster 3934.57
Betty Dean 9-(303)520-2435 LP Field 8327.39
Sara Howell 8-(262)976-9431 The Burrow 4205.45
Lois Gilbert 8-(682)840-9489 Qualcomm Stadium 8324.66
Robin Perkins 4-(991)447-4770 City Hall 4602.92
Sean Hunt 7-(192)337-9074 The Cathedral of Learning 69.55
Andrew Weaver 5-(157)145-3337 The Possum Lodge 5505.01
Per page 10 20 50 | Total 50

Sorting

SmartListing supports implicit sorting by default. In order to enable it, just use sortable helper in view. Optionally, you can also specify default sort attribute.

For more capabilities, see explicit sorting described in the docs.

def index
  # Make users initally sorted by name ascending
  smart_listing_create :users, 
                       User.active, 
                       partial: "users/list", 
                       default_sort: {name: "asc"}
end
= smart_listing_render :users
<%# Render list for XHR requests %>
<%= smart_listing_update :users %>
- unless smart_listing.empty?
  %table.table.table-striped
    %thead
      -# Second argument of sortable is sorted attribute name
      %th.col-md-3= smart_listing.sortable "Name", "name"
      %th.col-md-3= "Phone"
      %th.col-md-4= smart_listing.sortable "Location", "location"
      %th
        %span.glyphicon.glyphicon-shopping-cart{title: "Has credit card?"}
      %th.col-md-2= smart_listing.sortable "Wallet saldo", "saldo"
    %tbody
      - smart_listing.collection.each do |o|
        %tr
          -# User row goes here...
      
  = smart_listing.paginate
  = smart_listing.pagination_per_page_links
- else
  %p.warning No records!
Name Phone Location Wallet saldo
Aaron Brown 9-(183)849-2299 The International Space Station 5158.30
Amanda Gilbert 0-(176)755-0146 Ford Field 4722.75
Andrew Harper 0-(694)078-1303 Number 4, Privet Drive 419.15
Andrew Weaver 5-(157)145-3337 The Possum Lodge 5505.01
Angela Coleman 3-(426)608-4734 Mount Rushmore 649.18
Antonio Hansen 7-(606)128-5986 Starfleet Headquarters 4452.75
Betty Burns 1-(191)313-7622 New Meadowlands Stadium 9210.77
Betty Dean 9-(303)520-2435 LP Field 8327.39
Billy Lane 0-(411)762-5273 The International Space Station 4345.68
Cynthia Graham 6-(928)644-6550 Monstro Mart 2659.59
Per page 10 20 50 | Total 50

In-place editing

SmartListing allows creating, editing, updating and deleting records natively. You can also define your own custom actions.

before_filter :find_user, except: [:index, :new, :create]

def index
  smart_listing_create :users, 
                       User.active, 
                       partial: "users/list", 
                       default_sort: {name: "asc"}
end

def new
  @user = User.new
end

def create
  @user = User.create(user_params)
end

def edit
end

def update
  @user.update_attributes(user_params)
end

def destroy
  @user.destroy
end

def credit_card_switch
  @user.toggle :credit_card
end

private

def find_user
  @user = User.find(params[:id])
end

def user_params
  params.require(:user).permit(:name, :location, :phone, 
                               :credit_card, :saldo, :session_id)
end
<%# index.js.erb %>
<%= smart_listing_update(:users) %>

<%# new.js.erb %>
<%= smart_listing_item :users, :new, @user, "users/form" %>

<%# create.js.erb %>
<%= smart_listing_item :users, :create, @user, 
                       @user.valid? ? "users/user" : "users/form" %>

<%# edit.js.erb %>
<%= smart_listing_item :users, :edit, @user, "users/form" %>

<%# update.js.erb %>
<%= smart_listing_item :users, :update, @user, 
                       @user.valid? ? "users/user" : "users/form" %>

<%# destroy.js.erb %>
<%= smart_listing_item :users, :destroy, @user %>
- unless smart_listing.empty?
  %table.table.table-striped
    %thead
      %th.col-md-3= smart_listing.sortable "Name", "name"
      %th.col-md-3= "Phone"
      %th.col-md-4= smart_listing.sortable "Location", "location"
      %th
        %span.glyphicon.glyphicon-shopping-cart{title: "Has credit card?"}
      %th.col-md-2= smart_listing.sortable "Wallet saldo", "saldo"
    %tbody
      - smart_listing.collection.each do |o|
        %tr.editable{data: {id: o.id}}
          -# Render user row partial
          = smart_listing.render object: o, partial: "users/user", locals: {object: o}

      -# Render nice "new item" button fitted for Bootstrap 3 by default
      = smart_listing.item_new colspan: 6, link: new_user_path
      
  = smart_listing.paginate
  = smart_listing.pagination_per_page_links
- else
  %p.warning No records!
%td= object.name
%td= object.phone
%td= object.location
%td= display_tick object.credit_card?
%td= format_decimal object.saldo
%td.actions= smart_listing_item_actions [ {name:   :custom, 
  url:  credit_card_switch_user_path(u),   method: :put, 
  icon: "glyphicon glyphicon-credit-card", remote: true, 
  if:   !object.credit_card?,              title:  "Enable credit card"},
 {name: :edit,    url: edit_user_path(object)},
 {name: :destroy, url: user_path(object), confirmation: "Are you sure you want to delete this?"}]
%td{colspan: 6}
  = form_for object, url: object.new_record? ? users_path : user_path(object), 
                     remote: true, html: {class: "form-horizontal"} do |f|

    -# Form fields go here...

    = f.submit "Save", class: "btn btn-primary"
    %button.btn.btn-link.cancel Cancel
Name Phone Location Wallet saldo
Aaron Brown 9-(183)849-2299 The International Space Station 5158.30
Amanda Gilbert 0-(176)755-0146 Ford Field 4722.75
Andrew Harper 0-(694)078-1303 Number 4, Privet Drive 419.15
Andrew Weaver 5-(157)145-3337 The Possum Lodge 5505.01
Angela Coleman 3-(426)608-4734 Mount Rushmore 649.18
Antonio Hansen 7-(606)128-5986 Starfleet Headquarters 4452.75
Betty Burns 1-(191)313-7622 New Meadowlands Stadium 9210.77
Betty Dean 9-(303)520-2435 LP Field 8327.39
Billy Lane 0-(411)762-5273 The International Space Station 4345.68
Cynthia Graham 6-(928)644-6550 Monstro Mart 2659.59
New item
Per page 10 20 50 | Total 50

Controls form

SmartListing includes controls form that allows for AJAX-enabled filtering of entries. You can add it in your view using smart_listing_controls_for helper. You also obviously need to handle controls form parameters in your controller.

def index
  users_scope = User.active

  # Apply the search control filter.
  # Note: `like` method here is not built-in Rails scope. You need to define it by yourself.
  users_scope = users_scope.like(params[:filter]) if params[:filter]

  # Apply the credit card checkbox filter
  users_scope = users_scope.with_credit_card if params[:with_credit_card] == "1"

  @users = smart_listing_create :users, users_scope, partial: "users/list",
                                        default_sort: {name: "asc"}
end
-# Render smart listing controls form
= smart_listing_controls_for(:users, {class: "form-inline text-right"}) do
  -# Add checkbox to form - switching it submits the form
  .checkbox
    %label.checkbox.inline
      = hidden_field_tag :with_credit_card, "0"
      = check_box_tag :with_credit_card
      = "Has credit card?"

  -# Add search box to form - it submits the form automatically on text change
  .form-group.filter.input-append
    = text_field_tag :filter, '', class: "search form-control", 
                              placeholder: "Search...", autocomplete: :off

  %button.btn.btn-primary.disabled{type: :submit}
    %span.glyphicon.glyphicon-search
 
Name Phone Location Wallet saldo
Aaron Brown 9-(183)849-2299 The International Space Station 5158.30
Amanda Gilbert 0-(176)755-0146 Ford Field 4722.75
Andrew Harper 0-(694)078-1303 Number 4, Privet Drive 419.15
Andrew Weaver 5-(157)145-3337 The Possum Lodge 5505.01
Angela Coleman 3-(426)608-4734 Mount Rushmore 649.18
Antonio Hansen 7-(606)128-5986 Starfleet Headquarters 4452.75
Betty Burns 1-(191)313-7622 New Meadowlands Stadium 9210.77
Betty Dean 9-(303)520-2435 LP Field 8327.39
Billy Lane 0-(411)762-5273 The International Space Station 4345.68
Cynthia Graham 6-(928)644-6550 Monstro Mart 2659.59
Per page 10 20 50 | Total 50

Cart support

You can also build some kind of cart-like functionality using SmartListing. This can be achieved using callbacks.

def index
  session[:selected_users] = {} if params.[:clear]
  @users = smart_listing_create(:users, User.all, partial: "users/list", default_sort: {name: "asc"})

  set_withdrawal_status
end

def withdraw
  if params[:id]
    @users = User.where(id: params[:id])
  else
    @users = User.where(id: session[:selected_users].try(:keys))
    session[:selected_users] = {}
  end

  @users.update_all(saldo: 0)

  set_withdrawal_status
end

def select
  params[:user_selection].each do |key, value|
    if value == "true"
      session[:selected_users] ||= {}
      session[:selected_users][key] = 1
    else
      session[:selected_users].delete(key)
    end
  end

  set_withdrawal_status
end

private

def set_withdrawal_status
  @selected_users = User.where(id: session[:selected_users].try(:keys))
  @selected_saldo = @selected_users.sum(:saldo) unless @selected_users.empty?
end
= smart_listing_render(:users, data: {callback_href: select_users_path})
%td= check_box_tag :checkbox, "user_selection[#{object.id}]", 
                               session[:selected_users].try(:[], object.id.to_s) == 1, 
                               :class => "callback", :autocomplete => "off"
%td= object.name
%td= object.phone
%td= object.location
%td= display_tick object.credit_card?
%td= format_decimal object.saldo
%td.actions= smart_listing_item_actions [{:name => :custom, 
                                          :url => withdraw_users_path(:id => u.id), 
                                          :method => :put, 
                                          :icon => "glyphicon glyphicon-send", 
                                          :remote => true}]
-# Contents of "users/cart_summary" partial:
- if @selected_saldo
  = link_to withdraw_users_path, :class => "btn btn-primary pull-right", 
                                 :remote => true, :method => :put do
    %i.glyphicon.glyphicon-send
    = "Withdraw"
  = link_to "Clear", users_path(:clear => true), 
                     :class => "btn btn-link pull-right", :remote => true
  %h4
    = "You have selected "
    = @selected_users.count
    = " users with overall saldo of "
    = "%.2f" % @selected_saldo
-# Put this into "users/list" partial:
#cart_summary
  = render "users/cart_summary"
<%# Contents of "users/select.js.erb": %>
$("#cart_summary").html("<%= escape_javascript(render("users/cart_summary")) %>");
<%# Contents of "users/withdraw.js.erb": %>
<% @users.each do |user| %>
  <%= smart_listing_item :cart, :update, user, "users/user" %>
<% end %>
$("#users").smart_listing().fadeLoaded();
$("#cart_summary").html("<%= escape_javascript(render("users/cart_summary")) %>");
Name Phone Location Wallet saldo
Aaron Brown 9-(183)849-2299 The International Space Station 5158.30
Amanda Gilbert 0-(176)755-0146 Ford Field 4722.75
Andrew Harper 0-(694)078-1303 Number 4, Privet Drive 419.15
Andrew Weaver 5-(157)145-3337 The Possum Lodge 5505.01
Angela Coleman 3-(426)608-4734 Mount Rushmore 649.18
Antonio Hansen 7-(606)128-5986 Starfleet Headquarters 4452.75
Betty Burns 1-(191)313-7622 New Meadowlands Stadium 9210.77
Betty Dean 9-(303)520-2435 LP Field 8327.39
Billy Lane 0-(411)762-5273 The International Space Station 4345.68
Cynthia Graham 6-(928)644-6550 Monstro Mart 2659.59
Per page 10 20 50 | Total 50

Custom list

SmartListing can be used not only with tables. Just create custom view definitions in SmartListing partials and you can for example show a nice FAQ list with Bootstrap 3 components using only the usual smart_listing_render method in main view. You can also add controls form for simple searching.

faqs_scope = Faq.all
faqs_scope = faqs_scope.like(params[:filter]) if params[:filter]
@faqs = smart_listing_create :faqs, faqs_scope, partial: "faqs/list"
.faqlist
  - unless smart_listing.empty?
    -# Rendering the FAQs in Bootstrap 3' s accordion
    .panel-group#accordion
      - smart_listing.collection.each do |o|
        -# Here goes our custom rendering of FAQ content e.g. as Bootstrap 3 panels
    = smart_listing.paginate
    = smart_listing.pagination_per_page_links
  - else
    %p.alert.alert-info No records!
= smart_listing_controls_for(:faqs, {class: "form-inline text-right"}) do
  .form-group.filter.input-append
    = text_field_tag :filter, '', class: "search form-control",
    placeholder: "Search...", autocomplete: :off
  %button.btn.btn-primary.disabled{type: :submit}
    %span.glyphicon.glyphicon-search
= smart_listing_render(:faqs)
<%= smart_listing_update(:faqs) %>
Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.
Praesent id massa id nisl venenatis lacinia. Aenean sit amet justo. Morbi ut odio.
Praesent id massa id nisl venenatis lacinia. Aenean sit amet justo. Morbi ut odio.
Aenean fermentum. Donec ut mauris eget massa tempor convallis. Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.
Praesent id massa id nisl venenatis lacinia. Aenean sit amet justo. Morbi ut odio.
In hac habitasse platea dictumst. Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante. Nulla justo.
In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.
Vestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat.
Maecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem. Quisque ut erat.
Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.
Per page 10 20 | Total 20

Simplified views

Since SmartListing 1.1.0, in cases where you have single listing per controller, you don't need to create dedicated views like index.js.erb, update.js.erb etc anymore.

Take for example UsersController which is supposed to render single list of all your users.

Just create your controller actions like you usually do, but use SmartListing methods in their alternative syntax, without specifying the name, ie.:

smart_listing_create partial: "users/list"

In this case, SmartListing will be named automatically after your controller (ie. 'users'). One last thing you need to do is defining resource-finder and collection-finder helper methods:

def smart_listing_resource
  @user ||= params[:id] ? User.find(params[:id]) : User.new(params[:user])
end
helper_method :smart_listing_resource

def smart_listing_collection
  @users ||= User.all
end
helper_method :smart_listing_collection

Missing views that you don't need to create now, are provided by default SmartListing templates for create, destroy, edit, index, new, and update actions.


More Customization

SmartListing 1.1.0 brings also some improvement in terms of customization. Apart from application-wide class and attribute customization (via initializer file) you can now define your named config profiles and use different config profile for every SmartListing in your app. This is particularly helpful when you are building Rails engine and don't want to interfere with main app configuration.

To start with config profiles, create an initializer and name your profile:

SmartListing.configure(:awesome_profile) do |config|
  # put your definitions here
end

Then make a helper method named smart_listing_config_profile returning your profile name, ie.:

def smart_listing_config_profile
  :awesome_profile
end
helper_method :smart_listing_config_profile

This will make all SmartListing helpers refer to your :awesome_profile in all rendered views.

One last thing is to make Javascript helpers using your profile too. SmartListing.config.merge() method does that by accepting JSON with config values. This JSON on the other hand can be obtained by using helper SmartListing.config(:awesome_profile).to_json

The typical use case for this is to dump your config profile variables as body data attribute:

%body{data: {smart_listing_config: SmartListing.config(:awesome_profile).to_json}}

And then somewhere in your Javascript code:

SmartListing.config.merge()

SmartListing.config.merge() without parameters reads body data attribute defined above and updates Javascript config variables accordingly.


That's all folks!

If you liked SmartListing, please feel free to share!

We'd love to see you contributing to SmartListing development. Start with forking our repository.

In case you have any questions, comments, or found bug - file an issue on GitHub or contact us directly.


Check out also our newest gem - MailyHerald, Ruby on Rails email processing solution.


SmartListing is created by Sology. We are a Ruby on Rails software house based in Kraków, Poland.


Copyright © 2013-2015 Sology

Initial development sponsored by Smart Language Apps