Hello,
I am trying to implement more or less basic search screen. I use Phoenix/LiveView.
I’ve come across an issue that means either I chose the wrong approach, or I suck with forms (most likely - both).
The screen is super simple at this point: search bar in the middle, filters to the right, search results in the middle.
I put all three of these elements into the single form, like this:
<div class="container">
<.form
for={@search_form}
phx-change="apply_filters"
phx-submit="search"
class="..."
>
<.search_bar field={@search_form[:query]} />
<div class="flex flex-col md:flex-row">
<!-- Filters Column -->
<div class="...">
<.filters_section
form={@search_form}
filter1={filter1_list()}
filter2 ={filter2_list()}
filter3 ={filter3_list()}
/>
</div>
<!-- Search Results Column -->
<div class="w-full md:w-3/4">
<.search_results results={@results} />
</div>
</div>
</.form>
</div>
filter1, filter2, filter3 is sections on the right side of the screen, each consisting of several options. Consider, the list of brands, for example, list of locations etc. They all are multi selects.
My idea is to build the screen from the endpoint. The endpoint would look something like /search?q="..."&f1=...&f2=...
So, I put this into the live view code (for now, ignoring filters, only supporting query)
def mount(_params, _session, socket) do
socket =
socket
|> assign(search_form: to_form(Search.make_search_form()))
|> assign(:results, [])
{:ok, socket}
end
def handle_params(params, _uri, socket) do
query = params["q"] || ""
filters = []
search_form = Search.make_search_form(query, filters)
socket =
socket
|> update(:search_form, fn _ -> to_form(search_form, as: :search_form) end)
|> update(:results, fn _ -> search_results(search_form) end) # this builds and executes Ecto query returning the list of results
{:noreply, socket}
end
Then, when the search button is pressed I do this:
def handle_event("search", %{"search_form" => search_form}, socket) do
{:noreply, push_patch(socket, to: "/search?#{build_search_query(search_form)}")}
end
defp build_search_query(search_form) do
filter1 = []
filter2 = []
filter3 = []
"q=#{search_form["query"]}&f1=#{filter1}&f2=#{filter2}&f3=#{filter3}"
end
So far, this works ok. The search bar has the correct text at all times, the results are filtered properly, the filters checkboxes are enabled and disabled (UI-wise) without any problems.
The multi-select is implemented like this:
attr :form, Phoenix.HTML.Form, required: true
attr :field, Phoenix.HTML.FormField, required: true
attr :options, :list, required: true
defp multi_select(assigns) do
~H"""
<div class="...">
<%= for option <- @options do %>
<label class="...">
<input
type="checkbox"
name={"#{input_name(@form, @field.field)}[]"}
value={option}
checked={option in @field.value}
class="..."
/>
<span class="...">{option}</span>
</label>
<% end %>
</div>
"""
end
and is added to the parent tag like this:
<div>
<h3 class="...">Filter Name, e.g. Brands</h3>
<.multi_select
form={@form}
field={@form[:param1]}
options={@filter1}
/>
</div>
So far, so good.
Now, I would like for filters to take part in Ecto logic as well.
I do literally 3 changes.
- Add
handle_event
function:
def handle_event("apply_filters", %{"search_form" => search_form}, socket) do
{:noreply, push_patch(socket, to: "/search?#{build_search_query(search_form)}")}
end
- Modify how I build the search query:
defp build_search_query(search_form) do
filter1 = convert_list_to_string(search_form["filter1"])
filter2 = convert_list_to_string(search_form["filter2"])
filter3 = convert_list_to_string(search_form["filter3"])
"q=#{search_form["query"]}&f1=#{filter1}&f2=#{filter2}&f3=#{filter3}"
end
The result of this function looks good, e.g. "q=test&f1=brand1,brand2,brand3..."
etc
3. Modify the handle_params
function:
def handle_params(params, _uri, socket) do
query = params["q"] || ""
filters = %{
:filter1 => String.split(params["f1"] || "", ","),
:filter2 => String.split(params["f2"] || "", ","),
:filter3 => String.split(params["f3"] || "", ",")
}
search_form = Search.make_search_form(query, filters)
socket =
socket
|> update(:search_form, fn _ -> to_form(search_form, as: :search_form) end)
|> update(:results, fn _ -> search_results(search_form) end) # this builds and executes Ecto query returning the list of results
{:noreply, socket}
end
What happens after this is the following:
If I enter the screen via /search
endpoint and then start enabling checkboxes - they work fine.
If I enter the screen via the pre-built search query, all looks good too: the text in the search box is fine, and the checkboxes that should be enabled, are.
In both cases, problem starts when I try to disable a checkbox. It becomes super buggy: enabled/disabled state does not correspond to the params in the query, and at some point the query starts having the same param several times, e.g. f1=brand1,brand2,brand1
.
I would welcome any advice.