filtering, searching, and sorting activerecord lists using filterrific
TRANSCRIPT
Filtering,Searching,andSor1ngAc1veRecordListsUsingFilterrific
WaihonYewGitHub(waihon)TwiCer(@waihon)
SearchingforaGem
WhatDoesFilterrificProvide?• Letyourapp'suserssearch,filterandsortlistsofAc1veRecordobjects.
• PersistfilterseOngsintheHTTPsessionorDB.• Integrateswithpagina(on(will_paginateorkaminari).
• ResetfiltertodefaultseOngs.• ReliesonAc1veRecordscopes.• ShuClesfilterseOngsfromafilterUItothecontrollerandAc1veRecord.
• CanbeusedforHTML/JSON/JS/XMLresponseformats.
• Documenta1on:hCp://filterrific.clearcove.ca/
Dependencies• RailsandAc1veRecord3.xandabove.• jQueryandassetpipelineforformobserversandspinner.
WhatYouHaveToDo?1. DefineAc1veRecordscopes.2. Buildandstyleyourfilterformandrecord
lists.
• Awebappthatstoresbasicinforma1onofstudents.
DemoApp-ERD
has_many
belongs_to
DemoApp–Base
hCps://github.com/jhund/filterrific_demohCp://filterrific-demo.herokuapp.com/
DemoApp–Enhanced
hCps://github.com/waihon/filterrific_demo
ShortDemooftheEnhancedApp
Gemfilegem "filterrific"
View• DisplaytheformtoupdatefilterseOngs.• Displaythelistofmatchingrecords.• UpdatethefilterseOngsviaAJAXformsubmission.
• ResetthefilterseOngs.• FilterrificworksbestwithAJAXupdates.• ThelibrarycomeswithformobserversforjQueryandanAJAXspinner.
ViewComponents
ViewComponentsindex.html
Thisisthemaintemplateforthestudentslist.Itisrenderedonfirstload.
ViewComponentsindex.html
students/_list.html
Thispar1alrenderstheactuallistofstudents.It’sextractedintoapar1alsothatitcanbeupdatedviaAJAXresponse.
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
Filterrificaddsthe'form_for_filterrific'viewhelper:*AddsDOMid'filterrific_filter'*AppliesJavascriptbehaviors:-AJAXformsubmissiononchange-AJAXspinnerwhileAJAXrequestisbeingprocessed*Setsform_forop1onslike:url,:methodandinputnameprefix
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
Givethesearchfieldthe'filterrific-periodically-observed'classforliveupdates.
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
index.js
ThisJavaScriptupdatesthestudentslistaherthefilterseOngswerechanged.
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
index.js
Searches/_modal_form.html
DisplayamodalformforsavingfilterseOngs.
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
index.js
searches/_list.html
ViewComponentsindex.html
students/_list.htmlform_for_filterrific
index.js
searches/_list.html
ResetfilterseOngstothedefaultsdefinedinthemodeloroverrideninthecontroller(reset_filterrific_url).
SortbyColumnTitle
Filterrificprovidessortablecolumnheaderlinkswhichtogglesortdirec1onwiththefilterrific_sor1ng_link()method.
SortbyColumnTitle
Themethodmustbeplacedinthe_list.htmlviewpar1al:<th><%= filterrific_sorting_link(@filterrific, :name) %></th>
Thefilterrific_sor1ng_linkmethodisexpec1ngasorted_byscopewhichcontainsthecolumnheadersandthemodelaCributetosortby.Columnheadersareautoma1callycapitalized. scope :sorted_by, lambda { |sort_option| # extract the sort direction from the param value. direction = (sort_option =~ /desc$/) ? 'desc' : 'asc' case sort_option.to_s when /^created_at_/ order("students.created_at #{ direction }") when /^name_/ order("LOWER(students.last_name) #{ direction }, LOWER(students.first_name) #{ direction }") when /^country_name_/ order("countries.name #{ direction }").includes(:country) else raise(ArgumentError, "Invalid sort option: #{ sort_option.inspect }") end }
SortbyColumnTitle
DisableAJAXAutoFormSubmits• BydefaultFilterrificwillautoma1callysubmitthefilterformassoonaswechangeanyofthefilterseOngs.
• Some1mesyoumaynotwantthisbehavior,e.g.,iftherenderingofthefilteredrecordsisfairlyexpensive.
• Theautosubmitbehavioristriggeredbythefilterform'sidwhichisautoma1callyaddedbytheform_for_filterrifichelpermethod.
• Inordertodeac1vateAJAXautosubmits– OverridetheDOMidfortheformwithsomethingotherthanthedefaultoffilterrific_filter.
– Addtheremote:trueop1ontoform_for_filterrific– Don'taddthe.filterrific-periodically-observedclasstoanyinputs.
– AddaregularsubmitbuCon
DisableAJAXAutoFormSubmits• BydefaultFilterrificwillautoma1callysubmitthefilterformassoonaswechangeanyofthefilterseOngs.
• Some1mesyoumaynotwantthisbehavior,e.g.,iftherenderingofthefilteredrecordsisfairlyexpensive.
• Theautosubmitbehavioristriggeredbythefilterform'sidwhichisautoma1callyaddedbytheform_for_filterrifichelpermethod.
• Inordertodeac1vateAJAXautosubmits– OverridetheDOMidfortheformwithsomethingotherthanthedefaultoffilterrific_filter.
– Addtheremote:trueop1ontoform_for_filterrific– Don'taddthe.filterrific-periodically-observedclasstoanyinputs.
– AddaregularsubmitbuCon
Ifyous1llwanttosubmittheformviaAJAX(justnotautoma1callyoneverychange).OtherwisetheformwillbesubmiCedasregularPOSTrequestandtheen1repagewillreload.
DisableAJAXAutoFormSubmits• BydefaultFilterrificwillautoma1callysubmitthefilterformassoonaswechangeanyofthefilterseOngs.
• Some1mesyoumaynotwantthisbehavior,e.g.,iftherenderingofthefilteredrecordsisfairlyexpensive.
• Theautosubmitbehavioristriggeredbythefilterform'sidwhichisautoma1callyaddedbytheform_for_filterrifichelpermethod.
• Inordertodeac1vateAJAXautosubmits– OverridetheDOMidfortheformwithsomethingotherthanthedefaultoffilterrific_filter.
– Addtheremote:trueop1ontoform_for_filterrific– Don'taddthe.filterrific-periodically-observedclasstoanyinputs.
– AddaregularsubmitbuCon
<%= form_for_filterrific @filterrific, remote: true html: { id: 'filterrific-no-ajax-auto-submit' } do |f| %> ... <%= f.submit 'Filter' %> <% end %>
Model–AddfilterrificDirec1ve# student.rb Filterrific( default_filter_params: { sorted_by: 'created_at_desc' }, available_filters: [ :sorted_by, :search_query, :with_country_id, :with_created_at_gte ] )
DefinedefaultfilterseOngs
SpecifywhichscopesareavailabletoFilterrific.Thisisasafetymechanismtopreventunauthorizedaccesstoyourdatabase.It’slikestrongparameters,justforfilterseOngs.
EnableFilterrificfortheStudentclass
Model–DefineSelectOp1ons# student.rb def self.options_for_sorted_by [ ['Name (a-z)', 'name_asc'], ['Registration date (newest first)', 'created_at_desc'], ['Registration date (oldest first)', 'created_at_asc'], ['Country (a-z)', 'country_name_asc'] ] End # country.rb def self.options_for_select order('LOWER(name)').map { |e| [e.name, e.id] } end
Theseclassmethodsprovideop1onsfor
selectdrop-downandarecalledinthe
controlleraspartofini1alize_filterrific.
Model–DefineScopesscope :sorted_by, -> { |sort_key| # Sort students by sort_key direction = (sort_key =~ /desc$/) ? 'desc' : 'asc’ ... } scope :search_query, -> { |query| # Filters students that matches the query ... } scope :with_country_id, -> { |country_ids| # Filters students with any of the given country_ids where(:country_id => [*country_ids]) } scope :with_created_at_gte, -> { |ref_date| # Filter students whom registered from the given date where('students.created_at >= ?', Date.strptime(ref_date, "%m/%d/%Y")) }
Model–DefineScopesscope :sorted_by, -> { |sort_key| # Sort students by sort_key direction = (sort_key =~ /desc$/) ? 'desc' : 'asc’ ... } scope :search_query, -> { |query| # Filters students that matches the query ... } scope :with_country_id, -> { |country_ids| # Filters students with any of the given country_ids where(:country_id => [*country_ids]) } scope :with_created_at_gte, -> { |ref_date| # Filter students whom registered from the given date where('students.created_at >= ?', Date.strptime(ref_date, "%m/%d/%Y")) }
FilterrificreliesheavilyonAc1veRecordscopesforfiltering,soitisimportantthatyouarefamiliarwithhowtousescopes.hCp://filterrific.clearcove.ca/pages/ac1ve_record_scope_paCerns.html
FilterrificAc1onController• Ini1alizefilterseOngsfromparams,persistenceordefaults.
• ExecutetheAc1veRecordquerytoloadthefilteredrecords.
• SendtheAc1veRecordcollec1ontotheviewforrendering.
• PersistthecurrentfilterseOngs.• ResetthefilterseOngs.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Filterrificlivesinthecontroller’sindexac1on.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
AnAc1veRecord-basedmodelclass.ItcanalsobeanAc1veRecordrela1on.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
AnyparamssubmiCedviawebrequest.Iftheyareblank,filterrificwilltryparamspersistedinthesessionnext.Ifthoseareblank,too,filterrificwillusethemodel'sdefaultfilterseOngs.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Storeanyop1onsfor<select>inputsintheform.ThekeyreferstoscopenamedefinedinthemodelThevaluereferstomethoddefinedinthemodelthatreturnanarrayofop1ons
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Defaultsto"<controller>#<ac1on>"stringtoisolatesessionpersistenceofmul1plefilterrificinstances.Overridethistosharesessionpersistedfilterparamsbetweenmul1plefilterrificinstances.Settofalsetodisablesessionpersistence.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Tooverridemodeldefaults
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Tofurtherrestrictwhichfiltersareinthisfilterrificinstance.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
ThismethodalsopersiststheparamsinthesessionandhandlesreseOngthefilterrificparams.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
Inorderforreset_filterrifictowork,it’simportantthatweaddthe'orreturn'bitaherthecallto'ini1alize_filterrific'.Otherwisetheredirectwillnotwork.
Controller–ini1alize_filterrific# students_controller.rb def index @filterrific = initialize_filterrific( Student, params[:filterrific], select_options: { sorted_by: Student.options_for_sorted_by, with_country_id: Country.options_for_select } persistence_id: 'shared_key', default_filter_params: {}, available_filters: [], ) or return @students = @filterrific.find.page(params[:page]) ... end
ThismethodalsopersiststheparamsinthesessionandhandlesreseOngthefilterrificparams.
ReturnsanAc1veRecordrela1onforallrecordsthatmatchthefilterseOngs.Wecanpaginatewithwill_paginateorkaminari.Therela1onreturnedcanbechainedwithotherscopestofurthernarrowdownthescopeofthelist,e.g.,toapplypermissionsortoexcludecertaintypesofrecords.
SavedSearches
• @search.filter=session["shared_key"]• @search.filter=session["students#index"] {\"sorted_by\"=>\"created_at_asc\",
\"with_country_id\"=>7,
\"with_created_at_gte\"=> \"01/01/2016\"}
SavedSearches
• @search.filter=session["shared_key"]• @search.filter=session["students#index"] {\"sorted_by\"=>\"created_at_asc\",
\"with_country_id\"=>7,
\"with_created_at_gte\"=> \"01/01/2016\"}
Thiskeyisthe:persistence_iddefinedinthecalltoini1alize_filteerrific
Thiskeyisthe:persistence_iddefinedinthecalltoini1alize_filterrific
Ini1alizePersistedFilterSeOngs• Inthecalltoini1alize_filterrific,replaceparams[:filterrific]withamethodcall.
def filter_settings
if params[:filterrific].present?
params[:filterrific]
elsif params[:search_id].present?
search = Search.find_by(id: params[:search_id]) search ? eval(search.filter) : Student.default_filter_params
else
Student.default_filter_params
end end
# students_controller.rb @filterrific = initialize_filterrific( Student, filter_settings, ...
Ini1alizePersistedFilterSeOngs• Inthecalltoini1alize_filterrific,replaceparams[:filterrific]withamethodcall
def filter_settings
if params[:filterrific].present?
params[:filterrific]
elsif params[:search_id].present?
search = Search.find_by(id: params[:search_id]) search ? eval(search.filter) : Student.default_filter_params
else
Student.default_filter_params
end end
#routes.rb get "/students/search/:search_id", to: "students#index", as: "search_students"
Ini1alizePersistedFilterSeOngs• Inthecalltoini1alize_filterrific,replaceparams[:filterrific]withamethodcall
def filter_settings
if params[:filterrific].present?
params[:filterrific]
elsif params[:search_id].present?
search = Search.find_by(id: params[:search_id]) search ? eval(search.filter) : Student.default_filter_params
else
Student.default_filter_params
end end
UsingevaltoconvertthepersistedfilterseOngsfromstringtohash.
ThankYouforYourACen1on&Pa1ence!
WaihonYewGitHub(waihon)TwiCer(@waihon)