Filtering, Searching, and Sorting ActiveRecord Lists Using Filterrific

Post on 12-Apr-2017

125 views 6 download

Transcript of Filtering, Searching, and Sorting ActiveRecord Lists Using Filterrific

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)