1

I'm trying to include some sort of 'widget' in one of my views that allows the user to consume an external 'search' API. The external API returns a JSON set of results. I would like to use this JSON to show the user a list of results.

What is the 'rails way' to make this happen?

For example, I would like to put a search input on the homepage, with a button to search the external API. I have created a PORO that calls the API and this PORO will return the results, but how do I take the users search string and post it to this PORO?

boydenhartog
  • 752
  • 2
  • 9
  • 22
  • 1
    How do you want the user to interact with the widget? Are you willing to reload the whole page after the user submits his search params? In my mind, a widget is the sort of thing you'd do solely with JS, querying the api directly and rendering results inplace without going through ruby. But if you must parse the response in ruby before presenting it to the view you'll have to submit the search params to your controller using a regular request or ajax. – Alexandre Angelim Feb 17 '17 at 00:09
  • Perhaps widget was the wrong term. I do want to reload the page after submitting the search params. I'm just not sure what the best practise is to do this. Using form_for suggests a form for a specific model which doesn't work in this case. – boydenhartog Feb 17 '17 at 00:11
  • 1
    You can try with `form_tag`, which is not bound to a model. http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html – Alexandre Angelim Feb 17 '17 at 00:17
  • 1
    The HTTParty gem might be your answer. Try this question: http://stackoverflow.com/questions/748614/what-is-the-proper-rails-way-to-consume-a-restful-web-service-on-another-domai – chester Feb 17 '17 at 00:18
  • @chester The poro I have is actually an HTTParty class, my problem is trying to connect this to a view/controller – boydenhartog Feb 17 '17 at 00:35
  • 2
    `Using form_for suggests a form for a specific model which doesn't work in this case.` make your PORO an Active Model and `form_for` will work with it perfectly. – Taryn East Feb 17 '17 at 00:38
  • Even if the poro is not tied to a model? It is just a class that I use to consume the external API. If the PORO is an extend of Active Model, I would need a controller for it too right? – boydenhartog Feb 17 '17 at 00:43
  • 2
    What @TarynEast is saying is to turn your PORO into an ActiveModel. http://api.rubyonrails.org/classes/ActiveModel/Model.html / and yes.. you'd still need a controller to handle that search request. – Alexandre Angelim Feb 17 '17 at 00:48
  • Thanks a lot people, I will try this approach! – boydenhartog Feb 17 '17 at 00:55
  • Sorry for the questions, but how would I set up routes for this controller, since there's no new/edit etc, only search and results? – boydenhartog Feb 17 '17 at 01:26

1 Answers1

3

Just summing up what we discussed in the comments. The simplest way to accomplish what you want will need a view to render the widget and a route + controller to handle the search request.

This is all untested code, but you can get the gist of it.

# routes.rb
# this can be named and pointed to anywhere you want.
# It's using get just so you can see the search params in the url, which is preety common for the search feature.
get '/search', to: 'searches#show', as: 'search'

# models/search.rb
# Your PORO
class Search
  attr_reader :results
  def initialize(query)
    # perform request and assign results
    @results = HTTParty.get(url, query: { keyword: query })
  end
end

# controllers/searches_controller.rb
def show
  if params[:query]
    @search = Search.new(params[:query])
  end
end

# views/searches/show.html.erb
<%= form_tag search_path, method: :get do %>
  <%= text_field_tag :query, params[:query] %>
  <%= submit_tag %>
<% end %>

# .... renders contents if they're present.
<%= @search.results if @search %>

What you could additionally do is turn Search into a model, so it would inherit Naming and you could deal with it as a regular resource.

# routes.rb
# A singular resource will do.
resource :search, only: [:show]

# models/search.rb
include ActiveModel::Model
attr_reader :results
attr_accessor :query
validates :query, presence: true
def perform
  @results = HTTParty.....
end

# controllers/searches_controller.rb
def show
  if search_params = params[:search] && params[:search][:query]
    @search = Search.new(query: search_params)
    @search.perform if @search.valid?
  else
    @search = Search.new
  end
end

# views/searches/show.html.erb
<%= form_for @search, method: :get do |f| %>
  <%= f.text_field :query %>
  <%= f.submit %>
<% end %>

<%= @search.results %>

Now... You probably want to render your widget everywhere and not only in /search, so you can extract the form into a partial and render it in application.html.erb.

If you went for the ActiveModel approach don't forget to initialize @search in every request you render the form. You can use a before_action for that.

Alexandre Angelim
  • 6,293
  • 1
  • 16
  • 13
  • Everything seems to work except for dig. undefined method `dig' for {"controller"=>"last_fms", "action"=>"show"}:ActionController::Parameters – boydenhartog Feb 17 '17 at 02:08
  • 1
    try `params[:search] && params[:search][:query]`. – Alexandre Angelim Feb 17 '17 at 02:10
  • 1
    Works like a charm. Thank you kind sir! – boydenhartog Feb 17 '17 at 02:28
  • 1
    You're welcome. Just so you know, `#dig` was introduced in ruby 2.3. It's a cleaner way to reach deep into a hash or array. – Alexandre Angelim Feb 17 '17 at 02:34
  • 1
    It seems they have changed ActionController::Parameters, it is not a hash anymore. Therefore it doesn't work. Source: https://github.com/rails/rails/issues/23104 – boydenhartog Feb 17 '17 at 02:50
  • 1
    I believe you mean passing a request error from your POGO to the controller, right? I'd set an instance variable to hold errors or response status there and check it inside the controller after performing the search. Then you could pass it to the view with a new controller variable or flash message. – Alexandre Angelim Feb 17 '17 at 20:33
  • Perfect, this is how I solved it. Cheers mate you've been really helpful! – boydenhartog Feb 17 '17 at 21:57
  • So one more question, in the model you validate the presence of :query, but in the controller you invoke it without :query. What is the idea behind this and how do I approach testing the search model? Does it make sense to test validation? – boydenhartog Feb 18 '17 at 16:20
  • 1
    `Search.new` without params in the controller is just initializing `@search`, so `form_for` and correlate helpers could deal with it properly. I added that validation as an example so you could see that including ActiveModel would give you perks, but if you want to keep that and enforce that behaviour, then 'yes', you should write a test for it. You can write tests or specs for the search model just like any other model. You should consider using vcr or webmock gems to test your external requests. Good luck! – Alexandre Angelim Feb 19 '17 at 01:41