Skip to content

Instantly share code, notes, and snippets.

@wazery
Created November 16, 2014 01:42
Show Gist options
  • Save wazery/f9b8b55902b245ecd0d5 to your computer and use it in GitHub Desktop.
Save wazery/f9b8b55902b245ecd0d5 to your computer and use it in GitHub Desktop.
ComplexSearchForms
###**Introduction**
In this tutorial we will explore how to add complex searching functionalities into your Rails application. We will explore how to use the awesome Ransack Ruby gem and the view helpers and builders it provides for handling advanced searching on your models. You will find some really powerful features that is available out of the box without writing a lot of code like sorting, or conditional search. We will use the same application used in the **"Braintree series By Karim El Husseiny"**, in which the basic application creation process is explained in detail, you can refer to it to learn how Karim scaffolded and styled the basic application. Here is the fork repository that we will build on our application [Ransack - MovieStore](https://github.com/wazery/moviestore-ransack).
###**What we're going to achieve**
As mentioned in the previous section we will add complex searching capabilities to the movie store application. Here is a list of features we are going to implement:
- Searching movies by title
- Searching movies by price ranges
- Sorting capability (sorting by title, price, release year) ascending or descending
- Searching movies by any of its (model) attributes, with a lot of options like (equal to, greater to, less than, etc)
- Sorting by any model attribute
You can view a working demo of this app deployed on [Heroku](http://......)
###**Exploring Ransack provided demo**
**ActiveRecordHackery** has their own demo application for Ransack gem, you can find it [here](http://ransack-demo.herokuapp.com/) and explore it.
It provides two modes for searching (basic, and advanced). In basic mode you can search by only user first or last name, email or post title. In contrast to the advanced mode in which you have a lot of options to customize your search, try them to find the power that Ransack provides in complex searching.
###**Skills needed**
- Good experience to work with Ruby on Rails.
- Basic knowledge of HTML/CSS3.
- Understanding the concept of responsive CSS frameworks.
###**Tools**
- **Ruby** `(2.1.0)` - Programming language
- **Rails** `(4.1.1)` - Back-end framework
- **Foundation 5** - Front-end CSS framework
- **Ransack** - Ruby gem
- **Devise** - User authentication
###**Step 1:** Adding **Ransack** gem
Sure the first step is to add the gem to your Gemfile and run bundle to install it into yout Rails application. Just add the following line to it:
gem 'ransack'
Make sure to run *bundle install* in order for the changes to take effect.
###**Step 2:** Adding **search object**
In order for Ransack to work we need to create a new search object (instance variable) in the relevant controller action (which is index in our case) and give it the search params it will use to do the actual searching:
@search = Movie.search(params[:q])
Now we need to pass the search results to the **movies** instance variable which is used in the index view page to view only the returned movies from the search.
@movies = @search.result
So our index action would look like:
def index
@search = Movie.search(params[:q])
@movies = @search.result
end
###**Step 3:** Adding **search form** to the index page
Now we need to add the actual form that will handle the search, we just need a text field and a submit button, this text field will take the movie title user want to search for, we will add that simple form to the header of only the index page, so how to do that?
Very simple, we can add a yield statement in the header section which is located in the application layout itself, this yield statement would be replaced with the content_for method from the index view page with the simple search form we need, that makes it very easy for us to include this search form in any other page we want.
So open up your *layouts/application.html.erb* file and add:
<%= yield(:search) %>
In the header section. Then in your index view page add the simple search form as follows:
<% content_for :search do %>
<div class="large-8 small-9 columns">
<%= search_form_for @search do |f| %>
<%= f.text_field :title_cont, class: "radius-left expand", placeholder: "Movie title" %>
</div>
<div class="large-4 small-3 columns">
<%= f.submit "Search", class: "radius-right button" %>
</div>
<% end %>
<% end %>
As you can see we used Ransack's helper method `search_form_for` to create our searching form, and passed the search object to it we created before. It contains a text_field with a symbol `:title_cont` passed to it, which indicates for Ransack that we want to search for a title that *contains* some value. The *cont* here is called a predicate and it is the way for Ransack to determine what information to match.
As a simple refactoring I moved this content for to a separate partial *(_title_search_box.html.erb)* in order for it to be reusable in any other page. And will add a line in the index page to render this partial:
<%= render "title_search_box" %>
**Note:** Ransack also has a helper for labels you could use, we didn't need it in our form.
![((screenshot))](https://dl.dropboxusercontent.com/u/71605080/1.png)
###**Step 4 - 1:** Adding **price ranges** advanced search box
In the previous step we created a very simple searching functionality, lets try something more advanced like searching for movie price ranges (min, max prices)
This is what we are going to achieve:
![((screenshot-2))](https://dl.dropboxusercontent.com/u/71605080/2.png)
![((screenshot-3))](https://dl.dropboxusercontent.com/u/71605080/3.png)
So lets start with this search box styling, we will borrow some CSS classes from the MovieStore application like **form-container and glassy-bg** to add a glassy background to our search box, you can find this two classes in *layout.css.scss* and *helpers.scss* respectively. I created a new div with class column to contain the advanced search box, and given the classes **advanced-search and hide** to it to be hidden by default and shown later with a button.
<div class="column">
<div class="advanced-search hide form-container glassy-bg columns">
<a class="close-advanced-search fi-x"></a>
</div>
</div>
As you can see in the screenshot, we need to add a button which when clicked opens up the advanced search box. So we would add that link with a class **show-advanced-search** to be used in querying it, as follows:
<div class="column">
<h5>
<a class="show-advanced-search">Advanced Search <span class="fi-plus"/></a>
</h5>
</div>
Now in the *movies.js.coffee* we would add the click event for this link which will show the advanced search box:
$ ->
$('.show-advanced-search').click ->
$('.advanced-search').show() // which removes the hide class
$(this).hide()
And lets handle the close advanced search box click event:
$('.close-advanced-search').click ->
$('.advanced-search').hide()
$('.show-advanced-search').show()
It hides the search box and shows the button to re show it again.
###**Step 4 - 2:** Adding **price ranges** advanced search box
Now lets add the search form which will be used to handle price ranges search, we will use the same search_form_for method to create our searching form with the same **@search** instance variable, we will make 3 columns one for minimum price input, another for the word **And**, the last for the maximum price.
You can see in the below code that we used the symbols **:price_gteq** and **price_lteq** so **gteq** stands for *greater than or equal to* and **lteq** stands for *less than or equal to* you can view a list of this predicates from [here](https://github.com/activerecord-hackery/ransack/wiki/Basic-Searching)
<h4>Advanced Search</h4>
<%= search_form_for @search do |f| %>
<div class="large-5 small-4 columns">
<%= f.label :price_gteq, class: "movie-label" %>
<%= f.text_field :price_gteq, class: "radius", placeholder: "Minumum Price" %>
</div>
<h6 class="large-2 small-4 columns center"><span>And</span></h6>
<div class="large-5 small-4 columns">
<%= f.label :price_lteq, class: "movie-label" %>
<%= f.text_field :price_lteq, class: "radius", placeholder: "Maximum Price" %>
</div>
<div class="column">
<%= f.submit "Search", class: " radius button" %>
</div>
<% end %>
![((screenshot-4))](https://dl.dropboxusercontent.com/u/71605080/4.png)
**Note:** to view this code from the source repository just do a simple Git checkout:
$ git checkout ab44548
###**Step 5:** Adding **sorting** by titles, prices, release years
Ransack has a form builder that provides links that can be used to sort columns ascending or descending. This builder is **sort_link**. It is very simple to use just pass the search object and the column name (attribute) you want to sort and a placeholder for it, as you see in the code below.
I overrided three Foundation labels, you can check their code in *ln#678* in file *foundation_and_overrides.scss*
<h4 class="column">Featured Movies</h4>
<div class="row right padm">
<div class="column">
<div class="filter-label red">
<%= sort_link @search, :title, "Title" %>
</div>
<div class="filter-label dark-golden-rod">
<%= sort_link @search, :price, "Price" %>
</div>
<div class="filter-label dark-slate-gray">
<%= sort_link @search, :release_year, "Release Year" %>
</div>
</div>
</div>
![((screenshot-5))](https://dl.dropboxusercontent.com/u/71605080/5.png)
**Note:** to view this code from the source repository just do a simple Git checkout:
$ git checkout bec3594
###**Step 6:** Adding **conditional** searching
In this step we will explore how to add conditional search fields, we will make a select box with all of the movies model attributes and another one with all of the predicates to choose from, the third will be a text field for the value that user can enter to match that case. So Ransack has some form builders for that as shown in the below code:
Also we will add the same sorting functionality but in a form of select boxes, one for model attributes and the other of the order (ascending, or descending).
<h4>Advanced Search</h4>
<%= search_form_for @search do |f| %>
<%= f.condition_fields do |c| %>
<div class="large-4 small-4 columns">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select nil, class: "radius" %>
<% end %>
</div>
<div class="large-4 small-4 columns">
<%= c.predicate_select Hash.new, class: "radius" %>
</div>
<div class="large-4 small-4 columns">
<%= c.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
</div>
<% end %>
<h5>Sort</h5>
<div class="column">
<%= f.sort_fields do |s| %>
<%= s.sort_select Hash.new, class: "large-5 small-4 columns mrs radius" %>
<% end %>
</div>
<%= f.submit "Search", class: "radius button" %>
<% end %>
![((screenshot-6))](https://dl.dropboxusercontent.com/u/71605080/6%20%281%29.png)
**Note:** to view this code from the source repository just do a simple Git checkout:
$ git checkout 38f637d
###**Step 7:** Refactoring **conditional** searching
Lets move our code to a separate partial called **condition_fields**, here is how it looks:
*_condition_fields.html.erb*
<div class="field">
<div class="large-4 small-4 columns">
<%= f.attribute_fields do |a| %>
<%= a.attribute_select nil, class: "radius" %>
<% end %>
</div>
<div class="large-4 small-4 columns">
<%= f.predicate_select Hash.new, class: "radius" %>
</div>
<div class="large-4 small-4 columns">
<%= f.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
<%= link_to "remove", "#", class: "remove_fields" %>
</div>
</div>
###**Step 8:** Link to add fields
Why not add links in the advanced search box to add more conditions or remove them? So lets do that.
In the `index.html.erb` we can add the link to add the fields, like so:
<%= f.condition_fields do |c| %>
<%= render "condition_fields", f: c %>
<% end %>
<p><%= link_to_add_fields "Add Conditions", f, :condition %></p>
But first we need to add the link_to_add_fields helper method in the application helper to be used across the app:
def link_to_add_fields(name, f, type)
new_object = f.object.send "build_#{type}"
id = "new_#{type}"
fields = f.send("#{type}_fields", new_object, child_index: id) do |builder|
render(type.to_s + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
And in the *movies.js.coffee* we can add click event handler for the *add_fields* class:
$('form').on 'click', '.add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()
###**Step 9:** Link to remove fields
We can also add a link to remove the fields, we can add it in the *condition_fields* partial to be added with every condition:
<div class="large-4 small-4 columns">
<%= f.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
<%= link_to "", "#", class: "remove_fields fi-x" %>
</div>
And in the *movies.js.coffee* we can add click event handler for the *remove_fields* class which searches the DOM to top to find the first matching *field* class which will be removed.
$('form').on 'click', '.remove_fields', (event) ->
$(this).closest('.field').remove()
event.preventDefault()
###**Step 10:** Refactor the search form to issue a POST request
When we add too many conditions, the number of params will be increased and to avoid getting into the limit with the GET request we can change the form to do a POST instead, so how to do this?
Very simple just in the routes file add:
resources :movies, only: [:show, :index] do
match :search, to: 'movies#index', via: :post, on: :collection
end
And in the *index.html.erb* we need to change the search form to the new search path, like so:
<%= search_form_for @search, url: search_movies_path, method: :post do |f| %>
###Summary
In this tutorial we used the same online movie store application used in the "[Build an Online Store with Rails](http://www.sitepoint.com/build-online-store-rails/)" tutorial, to add complex searching functionalities for its movies, we explored how to use the **Ransack** gem in detail and the available helpers and options in it. Hope to see you soon in upcoming tutorials.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment