Rails Pagination & Search Functionality - Without a Gem
Published: 2021-03-11 20:39:28

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

```terminal
 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
```

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.

```ruby
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
```

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.

```ruby
SampleModel.where("name LIKE ?", "%#{query}%") 
  .limit(results_per_page)
  .offset(results_per_page * (current_page-1)) 
```



## 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.
```html
  <%= 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%>
```

###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`
```html
  <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>
```

###3. Result items
 We will iterate through all the results and display on the page.
```html
  <%@results.each do |result|%>

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

  <%end%>
```

This is how your final result should look like. I've wrapped all my code in a `div` element.
```html
<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>
```

Here is the styling used in this demo, incase you want to follow along.
```scss
.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;
}
```