Search code examples
rubyapisearchsinatrayellow-pages

Connecting Search Form in Ruby with Sinatra


I have a search form I'm using with the YP (Yellow Pages) API, coded in Ruby, with Sinatra.

I've managed to connect all the dots getting the search to work on the back-end, but am having trouble connecting the search form to the API call. Everything displays properly on the page, but nothing shows up when I click the submit button.

The code I am using is below:

require 'rubygems'
require 'sinatra'
require 'yp'

# index.rb 

get "/" do
  # Only run the search if both of our params are available
  if params[:location] && params[:term]
  client = Yp::Client.new(api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
  results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
  erb :index, :locals => {results: results}
end

__END__

@@index

<h1>YP Search</h1>

<form action="/" method="get">  
  Location: <input type="text" name="location">  <br />
  Search Term: <input type="text" name="term" required> <br />
<input type="submit">  
</form> 

<% if results %>
 <% results.each do |result| %>
 # Print out the result info
<% end %>
<% else %>
 # They haven't searched yet.
<% end %>

Solution

  • As you've written it, you're not passing results to the template. The last call in the block becomes the return value, which is the body passed to the client, so with your code:

    get "/" do
      # Only run the search if both of our params are available
      if params[:location] && params[:term]
      client = Yp::Client.new(api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
      @results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
    end
    

    This line:

      @results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
    

    becomes the body. What you want is the rendered template to be the last line, so:

    get "/" do
      # Only run the search if both of our params are available
      if params[:location] && params[:term]
      client = Yp::Client.new(api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
      @results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
      erb :index
    end
    

    Instance variables are automatically visible to templates so that's all you'd need to do. If you wanted to pass a local variable:

    get "/" do
      # Only run the search if both of our params are available
      if params[:location] && params[:term]
      client = Yp::Client.new(api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
      results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
      erb :index, :locals => {results: results}
    end
    

    and then remove the @ from the front of the @results var in the template.

    erb :index is a method call. It says "call the template renderer, use the ERB engine and pass it the :index template`. The return value of the method is a string. That string can then be used anywhere, just like a normal string, but you'd obviously be most likely to put it at the end of a route block, since you want to return a string.

    Where you've placed it right now, in the class body, it's just a method call with a result that goes to nowhere. It's the same as doing:

    get "/you/can/access/this" do
      "and this would be the returned body"
    end
    
    get "/and/this" do
      erb :index, :locals=>{:what_youd_see => "would be the template"}
    end
    
    "But this is just a string that no one can access, it gets called every time, but who is the receiver?"
    

    Think of your Sinatra app like it's a class, and then the normal rules of Ruby become clearer to apply :)


    I cloned your Git repo and found a few problems:

    • You don't need to list rubygems as a dependency in the gemspec.
    • Shotgun wasn't listed in the gemspec or Gemfile as a development dependency.
    • Rails was listed even though it's not in the project.

    in the gemspec:

    Gem::Specification.new do |gem|
      #other code
      gem.version       = Yp::VERSION
      gem.add_development_dependency "shotgun"
      gem.add_dependency('faraday', ["< 0.8", ">= 0.6"])    
      gem.add_dependency('faraday_middleware', [">= 0.8"])  
      gem.add_dependency('sinatra')  
    end
    

    Above is how I changed the yp.gemspec file, but personally I don't put development dependencies in the gemspec, but in the Gemfile.

    # Gemfile
    
    group :development do
      gem "shotgun"
    end
    

    I think it's easier to manage and separates things better. Instead of running bundle install I run bundle install --binstubs --path vendor as it puts everything in the local project directory. That way all projects are sandboxed from each other and you'll notice if you've missed anything out. To run the app I used bundle exec ruby index.rb.

    • There were syntax errors with index.rb

    Use ruby -c filename to check for syntax errors. First thing to try when you get errors on load.

    • In Ruby you have to close if…else blocks with end.

    I also added in a halt if the params aren't given, but you could also use an error handler or pass the the error into the template to inform the user. YMMV.

    get "/" do
      halt 400, "You need to supply a location and a term"
      # Only run the search if both of our params are available
      if params[:location] && params[:term]
        client = Yp::Client.new(api_key: "e89cba4b974a122e408d1723626f3709")
        results = client.search(searchloc: params[:location], term: params[:term], listingcount: 1, sort: 'distance')
      end
      erb :index, :locals => {results: results}
    end
    
    • You've hardcoded the API key.

    I'm guessing this should be secret. Best thing is probably to get a new one, and never hardcode it and never check it in to Git. I use a file that's not tracked by Git and is loaded into the environment variables, e.g.

    YAML.load_file("config/secret_settings.yml").each do |key,value|
      ENV[key.upcase] = value
    end
    

    I add this into the Rakefile and run the app from the rack file, e.g.

    namespace :app do
    
      desc "Set up the environment locally"
      task :environment do
        warn "Entering :app:environment"
        YAML.load_file("config/secret_settings.yml").each do |key,value|
          ENV[key.upcase] = value
        end
      end
      
      desc "Run the app locally"
      task :run_local => "app:environment" do
        exec "bin/rackup config.ru -p 4567"
      end
    end
    

    from the commandline:

    bin/rake app:run_local
    

    and you'll probably want to add a config.ru.