Live search is a feature commonly found in applications. There are various solutions to implementing search functionality on a website, but “live search” promotes a better user experience over the traditional way of searching (i.e. type in keyword and click to view results); it’s satisfying to a user since immediate feedback is received based on what they have typed, without having to click some form of a “submit” or “search” button. It’s refreshing to see results narrow as you type more words, or broaden as you backspace to delete already-typed characters from the search box. The less work your user has to do, the better.
Live search is not new concept whatsoever, but if you’re new to the JSON API specification and would like to follow its conventions, this may be helpful.
The specification states the following on the subject of filtering:
- The
filter
query parameter is reserved for filtering data. - Servers and clients SHOULD use this key for filtering operations.
Given that info, we’ll go over how to set up the client-side (Ember), and the
server-side (in both Phoenix and Rails) to get live search working. In the
following examples, we’ll work with a Food
model having a category
attribute.
On the Ember side
To get started, make sure your application is using the DS.JSONAPIADAPTER
;
it’s the default adapter in a new Ember application. This informs your
application of the type of data that it should be expecting from the server. In
our case, the payload will be expected to have a specific format and will be
expected to contain certain keys. Check out the spec if you’d
like more details on this.
Having that, we only need to add a couple of things: query parameters and the call to the server itself.
// controllers/foods.js
import Ember from 'ember';
const { Controller } = Ember;
export default Controller.extend({
queryParams: 'category',
category: ''
});
// routes/foods.js
import Ember from 'ember';
const { Route } = Ember;
export default Route.extend({
queryParams: {
category: { refreshModel: true }
},
model(params) {
return this.store.query('food', { filter: { category: params.category } });
}
});
It’s that simple. Notice that we’re using the store’s query
method and
providing it with a filter
. This filter
must be included in the call.
This will result in a GET
request containing a URL encoded string with the
filter
query parameter:
/foods?filter%5Bcategory%5D=pastry
Now let’s see how to set this up on the backend for a seamless integration.
On the Phoenix side
- Hex package needed: ja_resource
- Recommended to use with: ja_serializer
After following the lib’s quick installation instructions, and aside from needing to add our route and schema, that’s all we need to do in Phoenix before heading over to our controller for some filtering logic.
defmodule MyApp.FoodController do
import Ecto.Query
use JaResource
use MyApp.Web, :controller
plug JaResource
def filter(_conn, query, "category", category) do
from f in query,
where: ilike(f.category, ^("%#{category}%"))
end
end
On L7, plug JaResource
is reponsible for providing all the controller actions
by default. There is no need for you to implement these actions unless you’d
like to add custom logic. That’s a pretty nice time saver! Plus we can
customize our controller’s behavior via the many callbacks that the library
provides. JaSerializer conveniently provides the callback filter/4
where we
can handle our custom filtering given our filter parameters. In the example, we
only want to filter by category, so we add “category” as the third argument
so that we get a match. You’ll have to define one of these filter callbacks for
as many filter parameters as you want to pass. “Anything not explicitly matched
by your callbacks will get ignored.”
On the Rails side
- Gem needed: jsonapi-resources
After having installed the gem, like in the Phoenix section above, you’ll need to
declare your routes and models. To gain the simplest form of the filter
functionality, you just need to add the following (L5) to the corresponding
resource file (this will find an exact match):
class FoodResource < JSONAPI::Resource
attributes :category
filter :category
end
The filter will be based on the term passed in from the GET
request coming
from the Ember side; it will make sure that we are only returned Food
records
whose category
value matches exactly that of the request parameter
(i.e. “pastry”).
Below, I show another example that leverages the :apply
option whose
arguments are records (an ActiveRecord::Relation
), the value to filter by,
and an options hash. However, you have much flexibility on how you decide to
implement your filter. The README filter section
has a more comprehensive list of the possibilities.
class FoodResource < JSONAPI::Resource
attributes :category
filter :category, apply: -> (records, value, _options) {
records.where('category LIKE ?', "%#{value[0]}%")
}
end
Conclusion
That wraps it up! The Ember frontend and the Phoenix/Rails backends now work together to provide a live search functionality to a web application. Since we’re following the JSON API spec, there is little to no friction on either side in order to get this working as expected. Happy filtering!