@khjrtbrg

Skip to main content

Simple Sinatra Search

A few people have asked me how I made the search feature on my blog. This post shows how I did it, but since it's based on what we've learned so far in class (we're finishing up week 5 right now), there are probably better ways to go about it. Let me know if you have any suggestions!

For any non-Adies reading, this was our first real project with a web framework (Sinatra). We did RailsBridge during our first week, but this was the first project we did entirely from scratch with a framework. As we haven't learned much about databases yet, our posts are actually just ERB files in our views folder. This search feature should work when you have a database connected up, with some changes.

In order to create the search feature, I needed four things:

  • A way to read the contents of each post
  • A method that searches the contents of all the posts
  • A search form that would trigger the search method
  • A results page populated with the search method's results

Reading Post Contents

This step was actually completed during an earlier part of the project (displaying the five most recent posts on the homepage), but it came in handy for the search feature. In order to display the contents of each selected post, I created an attribute (@contents) in my Post class to store the contents of its respective ERB file. Here's the code for grabbing the contents:

  def get_contents
    IO.read("./views/posts/#{@date}/#{@slug}.erb")
  end

Creating a Search Method

For the search method, I appropriated the search method Stephanie Pi and I wrote for our FarMarFinder project:

  def self.search(search_term)
    search = search_term.downcase
    array = []
    array << self.all.find_all { |market| market.name.downcase.include?(search) }
    vendors = FarMar::Vendor.all.find_all { |v| v.name.downcase.include?(search) }
    array << lookup_vendor_markets(vendors)
    return array.flatten
  end

And adjusted it to work for the Post class:

  def self.search(search_term)
    self.all.find_all { |post| post.contents.downcase.include?(search_term.downcase) }
  end

(The self.all method creates Post objects from all the ERB files it finds in /view/posts/.)

I also wrote two RSPEC tests for my new search method, for good measure and to make sure it keeps working in the future:

  describe ".search(search_term)" do
    it "returns post with search_term" do
      expect(Post.search("wEeK")[0].contents).to include("week")
    end

    it "returns no posts with invalid search_term" do
      expect(Post.search("banana").size).to eq 0
    end
  end

The Search Form

So far, so good: we've written a ton of similar methods and tests during FarMar. The next part -- implementing the search on the blog -- was by far the trickiest part, as we had only briefly talked about using forms in class.

I started by adding a search field and submit button to my header, without thinking about how to connect it to my search method...

  <form>
    <input type="text" name="search" />
    <input type="submit" value="search" />
  </form>
unstyled search

Then I styled it to procrastinate some more...

styled search

Here's the basic CSS for this. Things change for narrower windows, but for the desktop view:

  input {
    margin: 10px 0 0 0;
    padding: 10px;
    border: none;
    background-color: #d3d3d3;
  }

  input[type=text] {
    width: 90px;
    background-color: #e0e0e0;
  }

  input[type=submit]:hover {
    background-color: #FF5E5E;
    color: #FFF;
    cursor: pointer;
  }

To make the search for do something, I added an action and method to the form's HTML:

<form action="/search" method="post">

I knew I wanted to send my form somewhere, so a page called /search seemed reasonable. I liked the cleaner look of post over get as far as extra parameters displayed in the URL, so I went with that.

At this point, I had a somewhat functional search form that sends you to a Sinatra Doesn't Know This Ditty page. Cool!

Search Results Page

To connect everything up, I needed to add a route for /search in my controller. get "/search" do would fetch the page, but since I only wanted people to get to the search results page if they searched for something, I tried this instead:

  post "/search" do
    # smancy code to go here...
    erb :search
  end

search.erb was a blank ERB file I had just added to my views folder. There's probably a DRY-er way to do this, but I was more concerned with getting it to work at this point. To test that I could receive the parameters of a search, I added Params: <%= params.inspect %> to search.erb and did some test searches.

Seeing that it was working, it was time to connect my search method with the form:

  post "/search" do
    @posts = Post.search(params[:search])
    erb :search
  end

Then, I copied and pasted my code from index.erb into search.erb (changing the h2 to "Search Results!") and gave it a test ride:

  <h2>Search Results!</h2>
  <div class="recent-posts">
    <% @posts.each do |post| %>
      <article>
        <a href="<%= post.url %>" class= "post"><%= post.date %></a>
        <h2><a href="<%= post.url %>"><%= post.title %></a></h2>
        <%= post.excerpt %>
        <p class= "read-more"><a href="<%= post.url %>">Read More →</a></p>
      </article>
    <% end %>
  </div>

With the basics working, I added a variable that counts the number of results, a variable to store search term, and a catch for when no results are returned to the controller:

  post "/search" do
    @search_term = params[:search]
    @posts = Post.search(@search_term)
    @size = @posts.size
    @size == 0 ? @no_results = "Sorry, No Results!" : @no_results = nil
    erb :search
  end

And I changed search.erb to use those variables:

  <h2><%= @size %> Results for "<%= @search_term %>"</h2>
  <div class="recent-posts">
    <% @posts.each do |post| %>
      <article>
        <a href="<%= post.url %>" class= "post"><%= post.date %></a>
        <h2><a href="<%= post.url %>"><%= post.title %></a></h2>
        <%= post.excerpt %>
        <p class= "read-more"><a href="<%= post.url %>">Read More →</a></p>
      </article>
    <% end %>
    <%= @no_results %>
  </div>

And that's it! It feels a little repetitive to have multiple ERB files that do essentially the same thing (listing out blog posts), but for now, it works.

jQuery Magic

search with jquery

When we got to jQuery, I added a search icon and a slideToggle to hide and then dropdown the search field smoothly. To make that happen, I added an ID to the form:

  <form id="jquery-toggle" action="/search" method="post">

And added this jQuery:

  $(document).ready( function () {

    $('#jquery-toggle').hide();
    $('a.search-icon').click (function () {
        event.preventDefault();
        $('#jquery-toggle').slideToggle();
    });
  });

If you want to peruse the code, checkout the repo on GitHub. I commit a lot.

Sidenote: Our Cohort Is Awesome

Most of our cohort didn't have any HTML/CSS experience before heading in, which I think makes our end results all the more impressive. I had a fair amount of CSS under my belt from my previous job, but this was the first time I built anything completely from scratch. Big thanks especially to Rebecca for her patient, thoughtful help.