DEV Community

Cover image for Building an Advanced Search Form in Rails 6
Brandon Marrero πŸ‡ΊπŸ‡Έ
Brandon Marrero πŸ‡ΊπŸ‡Έ

Posted on

Building an Advanced Search Form in Rails 6

Introduction

A simple search form is great, but one with advanced search options can have many use cases. Can you imagine Amazon with a search form that only accepts item names? Pssh...that sounds like a nightmare to me.

I prefer to provide more options for my users. Adding more filters can help users narrow down the results and find exactly what they are looking for.

You can use gems like Ransack to build search forms much faster, but for the purpose of learning and performance we will be building this feature ourselves. Throughout the process, you will also learn how to customize Rails default pluralization. By the end, we will be able to search for Pokemon by name, type, and region.

Before We Dive In

I recommend testing the application throughout the process. You can run rails server in your terminal and visit localhost:3000 after each feature implementation and see if the application works as expected.

Getting Started

Let us begin by creating a new Rails app. Execute the following code in your command line:

rails new PokemonDB

This will set up the entire Rails directory structure that we need to run our application. Before we do our scaffold, we need to setup Pokemon to have proper pluralization. Since Pokemon is already plural, we do not want Rails to add an 's' after it.

# pokemondb/config/initializers/inflections.rb

ActiveSupport::Inflector.inflections do |inflect|
    inflect.uncountable "pokemon"
end
Enter fullscreen mode Exit fullscreen mode

Now we can create our first model, Pokemon. Our model will have three strings of name, type, and region. We will generate this using a scaffold, which will generate the database and the basic MVC configuration we need. Feel free to build this without using a scaffold if you want an extra challenge.

NOTE: Rails sets attributes as strings by default. Since all of our attributes are strings, we do not need to specify the datatypes.

rails generate scaffold Pokemon name type region --force-plural

After your scaffold is built, update the Pokemon class to make 'type' an acceptable attribute for Pokemon. Rails reserves 'type' by default.

# pokemondb/app/models/pokemon.rb

class Pokemon < ApplicationRecord
    self.inheritance_column = "not_sti"
end
Enter fullscreen mode Exit fullscreen mode

Be sure to follow up with rails db:migrate to update the schema. Our application will not run with pending migrations.

Updating Routes

Now we need to update our routes to change the action for the root page, indicated by a forward slash. Use the 'pokemon#index' action for the root. Update the routes file:

# pokemondb/config/routes.rb

Rails.application.routes.draw do
  root 'pokemon#index'
  resources :pokemon
end
Enter fullscreen mode Exit fullscreen mode

Now our root page should look like this.

Index page

Test Your Create Form

Go ahead and click 'New Pokemon.' Create three or four Pokemon to test the form, then check the index page. This will also provide objects to work with. I prefer this method because I can test my forms and build 'seeds' at the same time.

New pokemon form

Once you have added Pokemon, you should see them on the index page. They will be displayed in the order you created them with show, edit, and destroy links. The next step will be to add a basic search form and logic.

Search by Name

Before we build the advanced search, let us build a basic Pokemon search form to search by name. We will expand on it later.

To build this use the form_with tag in the Pokemon index file. If you are using an older version of Rails, form_tag will work just fine with a few slight differences. Add the following code to the index view above the Pokemon table.

# pokemondb/app/views/pokemon/index.html.erb

<%= form_with(url: '/pokemon', method: 'get', local: true) do %>
  <%= text_field_tag(:search) %>
  <%= submit_tag("Search") %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Our form is taking in a path and method. The text_field_tag displays a textbox where users can enter a search value, while the submit tag sends the information for us to use and filter results. Here is the documentation if you would like to brush up on forms.

Basic Search Method

It is time to build the search method. Since this is database logic, it will be handled by the model. This method will find Pokemon based on a given name. If no search parameters are provided, then it will display all Pokemon. Update your 'pokemon.rb' file to look like this:

# pokemondb/app/models/pokemon.rb

class Pokemon < ApplicationRecord
    self.inheritance_column = "not_sti"

    def self.search(search)
        if search 
            where(["name LIKE ?","%#{search}%"])
        else
            all
        end
    end 
end
Enter fullscreen mode Exit fullscreen mode

We will put this new class method to use by calling it in our Pokemon index. The following code will take in search params from the search form we built earlier.

# pokemondb/app/controllers/pokemon_controller.rb

def index
    @pokemon = Pokemon.search(params[:search])
end
Enter fullscreen mode Exit fullscreen mode

Search form

The basic search form is now functional. I was able to find Bulbasaur! Time to make this form more useful and interesting.

Search Model

Next, we will add functionality to search for Pokemon by type and region. This is a bit more advanced and will require a separate model to handle searches. We will use rails generate, but this time for a model. This model will have name, type, and region. Be sure to run rails db:migrate after generating.

rails generate model Search name type region

Searches Controller

For our advanced search form to work properly, it will need its own controller. Make sure the controller name is pluralized. Run the following in your terminal, then update your routes.

rails generate controller searches

# pokemondb/config/routes.rb

Rails.application.routes.draw do
  root 'pokemon#index'
  resources :pokemon
  resources :searches
end
Enter fullscreen mode Exit fullscreen mode

Controller Actions

The controller will have three actions and a 'search_params' private method. Our actions are show, new, and create. Here is the code.

NOTE: Older versions of rails may use .uniq rather than .distinct. Try them out to see which one throws errors.

# pokemondb/app/controllers/searches_controller.rb

class SearchesController < ApplicationController
    def show
        @search = Search.find(params[:id])
    end 

    def new 
        @search = Search.new
        @types = Pokemon.distinct.pluck(:type)
        @regions = Pokemon.distinct.pluck(:region)
    end

    def create
        @search = Search.create(search_params)
        redirect_to @search
    end 

    private

    def search_params
        params.require(:search).permit(:name, :type, :region)
    end 
end
Enter fullscreen mode Exit fullscreen mode

Building a New Form

Start by creating a link to our new advanced search form. This can go directly under the standard search form in the Pokemon index view.

# pokemondb/app/views/pokemon/index.html.erb
<%= link_to "Advanced Search", new_search_path %>

You may notice that this link does not go anywhere. We need to create our views in the searches folder! Create the views new.html.erb and show.html.erb. Make sure they are in the # pokemondb/app/views/searches directory.

Now we have some empty views. Time to add some code to create new searches. This will be a form with select menus and a submit field.

# pokemondb/app/views/searches/new.html.erb</strong

<h1>Advanced Search</h1>

<%= form_with model: @search do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>

    <%= f.label :type %>
    <%= f.select :type, options_for_select(@types), include_blank: true %><br>

    <%= f.label :region %>
    <%= f.select :region, options_for_select(@regions), include_blank: true %><br><br>

    <%= f.submit "Search" %>
<% end %>

<p><%= link_to "Back", '/pokemon' %></p>
Enter fullscreen mode Exit fullscreen mode

Our advanced search view should look very clean. Try the 'Advanced Search' link on the index page to see if everything works as expected. Move on to the show view.

# pokemondb/app/views/searches/show.html.erb

<h1>Search Results</h1>
<p><%= link_to "Back", new_search_path %></p>

<% if @search.search_pokemon.empty? %>
    <p>No Pokemon were found.</p>
<% else %>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Type</th>
                <th>Region</th>
                <th colspan="3"></th>
            </tr>
        </thead>

        <tbody>
            <% @search.search_pokemon.each do |pokemon| %>
            <tr>
                <td><%= pokemon.name %></td>
                <td><%= pokemon.type %></td>
                <td><%= pokemon.region %></td>
                <td><%= link_to 'Show', pokemon %></td>
                <td><%= link_to 'Edit', edit_pokemon_path(pokemon) %></td>
                <td><%= link_to 'Destroy', pokemon, method: :delete, data: { confirm: 'Are you sure?' } %></td>
            </tr>
            <% end %>
        </tbody>
</table>
<% end %>
Enter fullscreen mode Exit fullscreen mode

You may notice this table is almost identical to the one generated by the scaffold. I decided to use it since it looks clean and easy to read.

Updating the Search Model

Just like our Pokemon model, the Search model needs a method to find Pokemon. This one will be an instance method that will be called on instances of Search. We have called it in our views with .search_pokemon. It is time to build it. Keep in mind that since this model also has a 'type' attribute, we will need to make it an acceptable attribute. We have done this on the second line of the search model.

# pokemondb/app/models/search.rb

class Search < ApplicationRecord
    self.inheritance_column = "not_sti"

    def search_pokemon
        pokemon = Pokemon.all 

        pokemon = pokemon.where(['name LIKE ?', name]) if name.present?
        pokemon = pokemon.where(['type LIKE ?', type]) if type.present?
        pokemon = pokemon.where(['region LIKE ?', region]) if region.present?

        return pokemon
    end 
end
Enter fullscreen mode Exit fullscreen mode

Test the Search

Everything should work properly! We should have the ability to filter Pokemon by name, type, and region. The various links and buttons we added also allow for great user-friendliness. It is nice to have a link to go back to the previous page and links to alter our Pokemon.

Run your rails server and visit localhost:3000 to see everything in action.

Advanced search

Thank you for reading this tutorial. I hope this can be of use to you in a future Rails project.

Top comments (6)

Collapse
 
sakko profile image
SaKKo

You can quickly search multiple columns in 1 line with ransack gem.
github.com/activerecord-hackery/ra...

example

Pokemon.ransack(name_or_type_cont: params[:q])
Enter fullscreen mode Exit fullscreen mode
Collapse
 
branmar97 profile image
Brandon Marrero πŸ‡ΊπŸ‡Έ

Hi, Sakko! That is one powerful gem, but this tutorial is more about learning how to build forms without gems. Using a gem may not always be the best choice, especially since excessive use of gems can slow down performance and increase memory usage. I also believe it is nice to learn how to build many features manually. I edited this post to reflect that. Thank you :-)

Collapse
 
sakko profile image
SaKKo

Oh, I like the whole article. Very good tutorial ∠( ̄∧ ̄)
I just wanna share this gem so that SQL search won't be so painful.
please write more (β‰§βˆ‡β‰¦)

Thread Thread
 
branmar97 profile image
Brandon Marrero πŸ‡ΊπŸ‡Έ

Haha, I completely understand. And please stick around. I like to share what I learn and learn from others :-)

Collapse
 
ryanb1303 profile image
Ryan B

TIL that we can de-pluralize model, great article thanks.

Collapse
 
sanchezdav profile image
David Sanchez

Awesome example! Thanks for sharing πŸ™Œ