Facing Difficulty in creating a Filter Button

I am trying to implement a filter action on a list of tasks where I create a button or link that can filter the tasks with respect to their completion status the task list has a description: string and a completion: boolean I have implemented a search function on description before and know that I need to create a search function after taking input about what to search from templates/task/index.html.heex in the form of search_input

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

and then implement some changes like-

# changing _params->params so that the search query is not neglected
# task-controller.ex
def index(conn, params) do
  tasks = Tasks.list_tasks(params)
  render(conn, "index.html", tasks: tasks)
end

also the list_tasks function is defined here-

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

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

the search function is an ecto query which goes like this-

def search(query, search_term) do
    from task in query,
    where: ilike(task.title, search_term)
  end

but I do not know of how to create a button to filter completion status which is a Boolean, I thought of creating a function which return string “true” if the input is :true but that doesn’t seem a good to way to do this, so please help me out, any link related to implementing search, filter and pagination function without any library will be helpful and appreciated

2 Likes

Let’s improve your list_tasks first, to allow filtering by query, and also by completion status.

def list_tasks(filters \\ []) do
  query = from(task in Task)

  filters
  |> Enum.reduce(query, fn filter, query_acc ->
    case filter do
      {:query, query} ->
        from task in query_acc, where: ilike(task.title, ^query)
      {:completed, "1"} ->
        from task in query_acc, where: task.completed == true
      _ -> query_acc
    end
  end)
  |> Repo.all()
end

Now we moved the querying logic away from the schema, and also made the filtering logic extendable for anything else you might come up with (including pagination). Also our function now is not tied to the params structure of the controller.

To use it now, you can do:

Tasks.list_tasks(query: "some name")
or
Tasks.list_tasks(query: "some name", completed: "1")

Now let’s work on the form. We want to somehow send the completed parameter. Many ways how to do it, let’s try a checkbox.

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

This will send these params to the controller:

%{"query" => "some query", "completed" => "1"}
or
%{"query" => "some query"} # if the checkbox is not checked

And now let’s handle it in the controller action:

def index(conn, params) do
  completed = Map.get(params, "completed")
  query = Map.get(params, "query")
  
  movies = Tasks.list_tasks(query: query, completed: completed)

  render(conn, "index.html", movies: movies)
end

Coding blind here, but I hope it sends you in the right direction :wink:

6 Likes

to add a couple extra notes:

get_in/2 is intended for nested data structures, if your data is only one level deep you can use Map.get(params, "query") or params["query"].

It feels a little icky to me personally to alias an application module as Task, since this collides with the standard library. If I had to have a module called MyApp.Task I would always fully qualify it.

2 Likes

i do not know why but when I implemented the change you mentioned-
my list of task is gone from /tasks page


also the checklist is acting wierd and the response that is being inputted does not matter to the search query.

also see the url it is taking 2 completion status in the params after I initially left the box unchecked and then checked the box.

I have updated my code here please do check- GitHub - nightfury17200/Task_List: A task list with a filter

I have a doubt- wouldn’t implementing the search in lib/search/tasks.ex in list_tasks be more taxing with respect to time and memory consumption versus creating an ecto query that directly works with your database in search/tasks.task.ex

The params are also not working as you said

for the GET request-

http://localhost:4000/tasks?query=&completed=false

the params are -

params: %{"completed" => "false", "query" => ""}
[query: "", completed: "false"]

for the GET request-

http://localhost:4000/tasks?query=&completed=false&completed=true

the params are -

params: %{"completed" => "true", "query" => ""}
[query: "", completed: "true"]

to accomodate these changes I made some edit in list_tasks function-

  def list_tasks(filters \\ []) do
    query = from(task in Task)
    IO.inspect filters
    filters
    |> Enum.reduce(query, fn filter, query_acc ->
      case filter do
        {:query, query} ->
          from task in query_acc, where: ilike(task.description, ^query)
        {:completed, "true"} ->
          from task in query_acc, where: task.completed == true
        {:completed, "false"} ->
          from task in query_acc, where: task.completed == false
        _ -> query_acc
      end
    end)
    |> Repo.all()
  end

It does work with the database. All it’s doing is reducing the filters keyword list into an Ecto Query. You can start iex and play with it. See what SQL queries it produces.

but @egze as per my understanding are we not fetching all task from database and then filter them on memory making it less optimal?
If we are then what better way can I do this to make it more optimal?

No, that’s not what happens.

Run it in iex Tasks.list_tasks(query: "some name") and you will see that the condition is part of the SQL query.

yeah I understand that part what I am saying is that we are calling
query = from(task in Task) , so wouldn’t that fetch data from our database then we will run

filters
    |> Enum.reduce(query, fn filter, query_acc ->
      case filter do
        {:query, query} ->
          from task in query_acc, where: ilike(task.description, ^query)
        {:completed, "true"} ->
          from task in query_acc, where: task.completed == true
        {:completed, "false"} ->
          from task in query_acc, where: task.completed == false
        _ -> query_acc
      end
    end)

which will filter the data we fetched from database and then return that.

am i wrong in what i said till now?

Note - i am a noob and have just started learning elixir if i am wrong in my understanding till now please mention why I am wrong and what i should read to understand better, thank for your patience.

No worries.

query = from(task in Task)

will not immediately execute anything. Ecto.Query is just data that knows what to query for. What makes it run against the DB is the Repo.all() which receives the Ecto.Query as argument.

What is happening in the code is, we take the base query query = from(task in Task) and then add more things to it with Enum.reduce. Once we are done, we run it with Repo.all

3 Likes

You do not hit database until You use Repo…

UPDATE: as mentionned in the previous post :slight_smile:

1 Like