Check out the Alchemist's Edition

#40: Simple Search Form with Phoenix

Elixir 1.5.3

Phoenix 1.3.0

View source on GitHub


Sign up for our newsletter to get notified when new episodes drop

Here is our movie app and one feature that would be really nice to have is a way to search for movies. In this episode we’ll build a simple search form that will allow us to quickly find the movie we want based on its title.

Let’s get started.

We’ll open our app.html.eex template.

Then we’ll add a form_for where we want our search for to appear.

We’ll use @conn since our search form doesn’t use any kind of schema.

And we’ll send our form data to our movie_path with the index action.

Let’s also add our pull-right class so our form appears and the right side of our site.

Template path: lib/teacher_web/templates/layout/app.html.eex

    <%= form_for @conn, movie_path(@conn, :index), [class: "pull-right"], fn f -> %>
      <%= search_input f, :query %>
      <%= submit "Search" %>
    <% end %>

Let’s go back to the browser and great we see our form.

Let’ try submitting it - and we get an error.

That’s because the form_for defaults to a “POST” and our index action is a “GET”.

Let’s update our search form to use “GET” as the method.

Template path: lib/teacher_web/templates/layout/app.html.eex

    <%= form_for @conn, movie_path(@conn, :index), [method: :get, class: "pull-right"], fn f -> %>
      <%= search_input f, :query %>
      <%= submit "Search" %>
    <% end %>

Then let’s go back to the browser and try our search again (GODFATHER) it loads loads the page and we see our search params in the URL.

Now let’s check out our server logs so we can see what the structure of our search looks like.

(Check the logs)

We can see our search term will be the value of our key "query" in our params.

Now that we know how to access our search term, we need to use it to search our movies.

Let’s open our movie_controller.ex

And in our index action, we’re ignoring the params. Let’s change that since this will contain our search query.

And then we can pass them into the Features.list_moviesfunction.

lib/teacher_web/controllers/movie_controller.ex

...
def index(conn, params) do
  movies = Features.list_movies(params)
  render(conn, "index.html", movies: movies)
end
...

Now that we’re passing our params in, let’s open our features.ex module.

And we’ll update the list_movies function to accept the params.

Now that we have our params here, how can we use them to search our movies?

In episode 31 we learned how to compose Ecto queries. Let’s use that same pattern here.

We want to create a pipeline that will build up our query and then hit the database.

The first thing we’ll need is our search term. We’ll use get_in to get our query form the params.

Now we can take our Movie and pipe it into a new function that we’ll need to create, but will contain our search query.

Let’s call it search and we’ll pass our search_term into it.

Then we’ll take its result and pipe it into Repo.all

lib/teacher/features/features.ex

...
def list_movies(params) do
  search_term = get_in(params, ["query"])

  Movie
  |> Movie.search(search_term)
  |> Repo.all()
end
...

Now let’s implement that search function.

We’ll open our movie.ex module.

And we’ll define our search function - it will take two arguments, our query and our search_term.

Since we’ll be using the Ecto.Query module here, let’s import it above. And we’ll limit it to only import the from function with an arity of 2.

Now let’s go back to our function and let’s wrap our search_term with wildcards (%). Wrapping our search_term in wildcards like this will select all movies that contain our search_term in the columns we specify.

Then let’s define our Ecto query.

We’ll get all of our movies from the query

And since we’re using PosgreSQL let’s use ilike to perform a case-insensitive search for all movies where our search_term is contained in the title.

lib/teacher/features/movie.ex

defmodule Teacher.Features.Movie do
…
import Ecto.Query, only: [from: 2]
…
  def search(query, search_term) do
    wildcard_search = "%#{search_term}%"

    from movie in query,
    where: ilike(movie.title, ^wildcard_search)
  end
end

Then if we go back to the browser and try our search - we see it’s working.

The params are being pulled in and our movie is returned.

Let’s also try it with all caps.

And great that works too.

But if we search for something not in the title, but in the description (wicked witch) nothing is returned.

This is because our query is only searching with our movie’s title.

Let’s update our search function to include our movie’s summary.

We’ll go back to our movie.ex module.

And in our search function let’s add or_where

With another ilike for where our search is included in our movie summary.

lib/teacher/features/movie.ex

...
def search(query, search_term) do
  wildcard_search = "%#{search_term}%"

  from movie in query,
  where: ilike(movie.title, ^wildcard_search),
  or_where: ilike(movie.summary, ^wildcard_search)
end
...

Now let’s see if our updated search works.

We’ll go back to the browser and try a search term that is only in our a movie summary.

And great it gives us the correct movie.