This is a demo of the (Advanced)[https://github.com/rzane/advanced] gem.
It shows the largest 1,000 cities in the United States.
The StateSearch
. This object is responsible for querying the states table.
class StateSearch < Advanced::Search
def search_name(name:, **)
where('states.name like ?', "%#{name}%")
end
end
We can use our StateSearch
like this:
StateSearch.call(State.all, name: 'New')
# => SELECT * FROM states WHERE states.name like '%New%'
By extending the scope in our State
model, we can shorten this a bit.
class State < ApplicationRecord
extend StateSearch.scope
end
State.search(name: 'New')
Within the StateSearch
class, we'll also define a form object, StateSearch::Form
.
class StateSearch < Advanced::Search
# ...
class Form < Advanced::SearchForm
search StateSearch
end
end
It's basically a hash, except that it's compatible with Rails' form builder.
form = StateSearch::Form.new(name: 'New')
form.name #=> 'New'
State.search(form)
# => SELECT * FROM states WHERE states.name like '%New%'
In our controller, we're going to initialize a form from the incoming params hash and query for States
.
class StatesController < ApplicationController
def index
@form = StateSearch::Form.new(params[:q])
@states = State.order(:name).search(@form)
end
end
In this file, we'll wire everything up. There's no magic here. We're just using Rails built-in functionality to create our form.
<h1 class="page-header">
States
</h1>
<div class="row">
<div class="col-md-8">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<% @states.each do |state| %>
<tr>
<td><%= state.name %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="col-md-4">
<%= form_for @form, method: :get, url: states_path, as: :q do |f| %>
<div class="form-group">
<%= f.label :name, class: 'control-label' %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<%= f.submit 'Search', class: 'btn btn-primary' %>
<%= link_to 'Reset', states_path, class: 'btn btn-default' %>
<% end %>
</div>
</div>
For listing cities, we'll do a more complicated query. Advanced is designed to be composable, so we can take advantage of the StateSearch
that we already built.
class CitySearch < Advanced::Search
def search_name(name:, **)
where('cities.name like ?', "%#{name}%")
end
def search_state(state:, **)
joins(:state).merge State.search(state)
end
end
We can use our CitySearch
like this:
CitySearch.call(City.all, name: 'New York')
#=> SELECT "cities".* FROM "cities" WHERE (cities.name like '%New York%')
CitySearch.call(City.all, state: { name: 'New York' })
#=> SELECT "cities".* FROM "cities"
#=> INNER JOIN "states" ON "states"."id" = "cities"."state_id"
#=> WHERE (states.name like '%New York%')
Again, we can abbreviate this by adding the scope to our model:
class City < ApplicationRecord
extend CitySearch.scope
end
City.search(name: 'New York')
We can also implement nested forms. Doing so will allow us to take advantage of Rails' fields_for
.
class CitySearch < Advanced::Search
# ...
class Form < Advanced::SearchForm
search CitySearch
nested :state, StateSearch::Form
end
end
We can use the CitySearch::Form
like this:
form = CitySearch::Form.new(name: 'New York', state: { name: 'New York' })
form.name #=> 'New York'
form.state.name #=> 'New York'
City.search(form)
#=> SELECT "cities".* FROM "cities"
#=> INNER JOIN "states" ON "states"."id" = "cities"."state_id"
#=> WHERE (cities.name like '%New York%') AND (states.name like '%New York%')
As before, we'll wire up our controller:
class CitiesController < ApplicationController
def index
@form = CitySearch::Form.new(params[:q])
@cities = City.includes(:state).order(:rank).search(@form)
end
end
This file is very similar to the states example, except that in order to query the states table, we'll use Rails fields_for
.
<%= form_for @form, method: :get, url: cities_path, as: :q do |f| %>
...
<%= f.fields_for :state do |state| %>
<div class="form-group">
<%= state.label :name, 'State', class: 'control-label' %>
<%= state.text_field :name, class: 'form-control' %>
</div>
<% end %>
<% end %>