DEV Community

Cover image for Rails Pagination & Search Functionality - Without a Gem
Chris
Chris

Posted on • Edited on

Rails Pagination & Search Functionality - Without a Gem

Today we'll go over how to add pagination to your Rails app using URL parameters and no external gems.

Creating our files

To get started we need to create our controller and set up our root route, for convenience I will use rails generator and execute rails g controller Home index in my terminal. Feel free to use another name for your controller, instead of Home. Rails will generate a few files for you and provide a detailed summary of where they can be found

 create  app/controllers/home_controller.rb
  route  get 'home/index'
 invoke  erb
 create    app/views/home
 create    app/views/home/index.html.erb
 invoke  test_unit
 create    test/controllers/home_controller_test.rb
 invoke  helper
 create    app/helpers/home_helper.rb
 invoke    test_unit
 invoke  assets
 invoke    scss
 create      app/assets/stylesheets/home.scss
Enter fullscreen mode Exit fullscreen mode

Optional: I will set my home#index as my root path by going to config > routes.rb and replacing get 'home/index' with root 'home#index'

Setting up our Controller

For demonstration purposes, I am using a class method #database instead of using an actual database.

class HomeController < ApplicationController
  # number of results to be shown per page
  @@results_per_page = 2 
  def index
    # params will be used in views for input and links
    @params = params.permit(:query, :page)
    # current page will be 1 by deault unless
    # user clicks on another page 
    @current_page = params[:page].try(:to_i) || 1
    # create a fetch request to database and retrieve the results
    @results, @pages_count  = database(@params, @current_page)
  end

  # sample database
  def database(params, current_page)
    # dummy data
    data = [ 'apple', 'cake', 'candy apple', 'car',
      'friend', 'run', 'hunt', 'ship', 'tip'
    ]
    # filter data using provided parameters. You can filter the data 
    # with as many parameters as you'd like
    results = data.filter{|i| i.include? params[:query].to_s} 
    # total number of pages that should be displayed
    pages_count = (results.length/@@results_per_page.to_f).ceil

    # results to display based on current page and result limit
    start = @@results_per_page * (current_page - 1)
    results = results[start, @@results_per_page]
    [results, pages_count]
  end
end
Enter fullscreen mode Exit fullscreen mode

Here is what your code could look like if you were using Active Record. Please note this will only return the results for the page selected. You may need to create a separate call to your database which will return the total number of results, which is needed to calculate the number of page items that need to be created.

SampleModel.where("name LIKE ?", "%#{query}%") 
  .limit(results_per_page)
  .offset(results_per_page * (current_page-1)) 
Enter fullscreen mode Exit fullscreen mode

Our Views

We'll break down the views in three steps. Our search field, pagination items, and result items.

1. Search field

At the top we will have a form to allow users to search for item by name. The form will be submitted to our root_path. For our search field I've added JavaScript to our onkeydown attribute. If the search field is empty, it will not have a name assigned. This will prevent our URL from having a query parameter, and make things a bit cleaner.

*I've added inline javascript for demonstration purposes. But adding javascript to your javascript folder is better. Or you could add an event listener to iterate through the form inputs before submitting.

  <%= form_with url: root_path, method: :get do %>
  <input onkeydown="if(this.value == '') {this.name = 'query'} else {this.name = 'query'}" value=<%="#{@params[:query]}"%>>
  <%= submit_tag "Search", name:nil %>
  <%end%>
Enter fullscreen mode Exit fullscreen mode

2. Pagination Items

The pagination items will be wrapped in an unordered list. When making each item, we will update the params page value to match the corresponding item. And the rest of the parameters will be passed along as well. A .active class is used to add styling to pagination item if it matches the @current_page

  <ul>
    <%[*1..@pages_count].each do |page|%>

    <%@params[:page] = page%>
    <li class=<%="#{@current_page == page ? 'active' : ''}"%>>
      <%= link_to page, root_path(@params)%>
    </li>

    <%end%>
  </ul>
Enter fullscreen mode Exit fullscreen mode

3. Result items

We will iterate through all the results and display on the page.

  <%@results.each do |result|%>

  <div class='results'>
    <%=result%>
  </div>

  <%end%>
Enter fullscreen mode Exit fullscreen mode

This is how your final result should look like. I've wrapped all my code in a div element.

<div class='demo'>
  <%= form_with url: root_path, method: :get do %>
  <input onkeydown="if(this.value == '') {this.name = 'query'} else {this.name = 'query'}" value=<%="#{@params[:query]}"%>>
  <%= submit_tag "Search", name:nil %>
  <%end%>

  <ul>
    <%[*1..@pages_count].each do |page|%>
    <%@params[:page] = page%>
    <li class=<%="#{@current_page == page ? 'active' : ''}"%>>
      <%= link_to page, root_path(@params)%>
    </li>
    <%end%>
  </ul>

  <%@results.each do |result|%>
  <div class='results'>
    <%=result%>
  </div>
  <%end%>
</div>
Enter fullscreen mode Exit fullscreen mode

Here is the styling used in this demo, incase you want to follow along.

.demo {
  margin: 80px auto 0;
  width: 400px;
  display: flex;
  flex-direction: column;
  input:first-of-type {
    width: 200px;
    padding: 5px 10px;
    border-radius: 10px;
    outline: none;
    border: 1px solid black;
  }
  input:last-of-type {
    margin-left: 5px;
  }
  ul {
    display: flex;
    margin-bottom: 20px;
    margin-top: 30px;
    // width: 100%;
    li {
      display: flex;
      justify-content: center;
      border: 1px solid rgb(194, 194, 194);
      font-size: 15px;
      border-radius: 8px;
      padding: 5px 10px;
      margin-right: 20px;
      &.active {
        background: cornflowerblue;
        a {
          color: white;
          font-weight: bold;
        }
      }
    }
  }
}

.results {
  border-radius: 8px;
  box-shadow: 0 0px 2.5px rgba(0, 0, 0, 0.05), 0 0px 20px rgba(0, 0, 0, 0.1);
  font-size: 20px;
  padding: 5px;
  margin-bottom: 5px;
  padding-left: 20px;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)