Hello all,
I’m trying out Ash in a new Phoenix project and I came across the AshPhoenix.FilterForm
documentation here. I implemented it into my LiveView following the documentation, making only a few changes to account for my resource name. Here is the code I put:
# Note that Group is the aliased name of my Resource, and the actual
# resource streaming takes place in `handle_params/3`
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:filter_form, AshPhoenix.FilterForm.new(Group))
|> stream(:groups, [])}
end
I added the following handle_event/3
handlers below:
@impl true
def handle_event("filter_validate", %{"filter" => params}, socket) do
{:noreply,
assign(socket,
filter_form: AshPhoenix.FilterForm.validate(socket.assigns.filter_form, params)
)}
end
def handle_event("filter_submit", %{"filter" => params}, socket) do
filter_form = AshPhoenix.FilterForm.validate(socket.assigns.filter_form, params)
case AshPhoenix.FilterForm.filter(Group, filter_form) do
{:ok, query} ->
{:noreply,
socket
|> stream(:groups, Group.read_all!(query: query), reset: true)
|> assign(:filter_form, filter_form)}
{:error, filter_form} ->
{:noreply, assign(socket, filter_form: filter_form)}
end
end
def handle_event("remove_filter_component", %{"component-id" => component_id}, socket) do
# I added this to debug what the value of the filter form is
IO.inspect(socket.assigns.filter_form)
{:noreply,
assign(socket,
filter_form:
AshPhoenix.FilterForm.remove_component(socket.assigns.filter_form, component_id)
)}
end
def handle_event("add_filter_group", %{"component-id" => component_id}, socket) do
{:noreply,
assign(socket,
filter_form: AshPhoenix.FilterForm.add_group(socket.assigns.filter_form, to: component_id)
)}
end
def handle_event("add_filter_predicate", %{"component-id" => component_id}, socket) do
{:noreply,
assign(socket,
filter_form:
AshPhoenix.FilterForm.add_predicate(socket.assigns.filter_form, :name, :contains, nil,
to: component_id
)
)}
end
I then added this to my render/1
function:
<.simple_form
:let={filter_form}
for={@filter_form}
phx-change="filter_validate"
phx-submit="filter_submit"
>
<.filter_form_component component={filter_form} />
<:actions>
<.button><%= gettext("Search") %></.button>
</:actions>
</.simple_form>
Finally, I copied the filter_form_component/1
function below:
attr :component, :map, required: true, doc: "Could be a FilterForm (group) or a Predicate"
defp filter_form_component(%{component: %{source: %AshPhoenix.FilterForm{}}} = assigns) do
~H"""
<div class="border-gray-50 border-8 p-4 rounded-xl mt-4">
<div class="flex flex-row justify-between">
<div class="flex flex-row gap-2 items-center"><%= gettext("Filter") %></div>
<div class="flex flex-row gap-2 items-center">
<.input type="select" field={@component[:operator]} options={["and", "or"]} />
<.button phx-click="add_filter_group" phx-value-component-id={@component.id} type="button">
<%= gettext("Add Group") %>
</.button>
<.button
phx-click="add_filter_predicate"
phx-value-component-id={@component.id}
type="button"
>
<%= gettext("Add Predicate") %>
</.button>
<.button
phx-click="remove_filter_component"
phx-value-component-id={@component.id}
type="button"
>
<%= gettext("Remove Group") %>
</.button>
</div>
</div>
<.inputs_for :let={component} field={@component[:components]}>
<.filter_form_component component={component} />
</.inputs_for>
</div>
"""
end
defp filter_form_component(
%{component: %{source: %AshPhoenix.FilterForm.Predicate{}}} = assigns
) do
~H"""
<div class="flex flex-row gap-2 mt-4">
<.input type="select" options={AshPhoenix.FilterForm.fields(Group)} field={@component[:field]} />
<.input
type="select"
options={AshPhoenix.FilterForm.predicates(Group)}
field={@component[:operator]}
/>
<.input field={@component[:value]} />
<.button
phx-click="remove_filter_component"
phx-value-component-id={@component.id}
type="button"
>
<%= gettext("Remove") %>
</.button>
</div>
"""
end
This results in the filter working as I’d expect, except for when it comes to removing components or groups. Looking at the UI nothing changes, however, the logging output shows the following:
[debug] HANDLE EVENT "remove_filter_component" in PlatformWeb.Groups.IndexLive
Parameters: %{"component-id" => "98db18d6-2f6c-43db-a5e2-82dfe192b859_components_0", "value" => ""}
%AshPhoenix.FilterForm{
id: "98db18d6-2f6c-43db-a5e2-82dfe192b859",
resource: Platform.Group,
transform_errors: nil,
name: "filter",
valid?: true,
negated?: false,
params: %{
"components" => %{
"0" => %{
"arguments" => nil,
"field" => "name",
"id" => "cb07878f-9eb0-449d-8227-b35a49a11c62",
"negated" => false,
"operator" => "contains",
"path" => nil,
"value" => "foo bar"
}
},
"id" => "2143092c-b7aa-4cf4-b73f-16c61217de2b",
"negated" => false,
"operator" => "and"
},
components: [
%AshPhoenix.FilterForm.Predicate{
id: "cb07878f-9eb0-449d-8227-b35a49a11c62",
field: :name,
value: "foo bar",
transform_errors: nil,
operator: :contains,
params: %{
"arguments" => nil,
"field" => "name",
"id" => "cb07878f-9eb0-449d-8227-b35a49a11c62",
"negated" => false,
"operator" => "contains",
"path" => nil,
"value" => "foo bar"
},
arguments: %AshPhoenix.FilterForm.Arguments{
input: %{},
params: %{},
arguments: [],
errors: []
},
negated?: false,
path: [],
errors: [],
valid?: true
}
],
operator: :and,
remove_empty_groups?: false
}
[debug] Replied in 4ms
It seems to me like it’s not able to identify the component to remove. I tried playing around with the structure and calling the remove_component/2
function with a few variations but I couldn’t get it to remove the predicate.
In the meantime I can just fall back to displaying a static form of some sort, but I thought this would be super useful.
Thanks!