AshPhoenix.FilterForm cannot remove components

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!

Hey @mylan your filter form looks good to me. The documentation was based on this working example (well it worked 8 months ago!): GitHub - totaltrash/filter_form_example

Could you inspect the component_id you get when trying to remove the predicate or group. Also, does the remove fail for both predicates and groups?

Yeah the component ID that is passed through the action is (in this case) 98db18d6-2f6c-43db-a5e2-82dfe192b859_components_0, and the predicate has an ID of cb07878f-9eb0-449d-8227-b35a49a11c62.

I attempted to have it remove a group as well as a predicate and it seems to fail at either of those.

I tried doing it manually in the iex console as well using the IDs passed in the action as well as the IDs of the predicates or groups themselves and it didn’t seem to remove them.

Ok, so i just updated all Ash deps with the filter form example I built a while back and everything works fine (leaving liveview at version 0.18). I then updated LiveView to 0.19 and yeah it’s broken pretty badly. It’s been a while since I’ve done any LiveView stuff, so I’m not too sure what’s changed? But the example given in the docs is no good for LiveView version 0.19 onwards.

2 Likes

I figured out how to make it work (I’m not sure if this is the right strategy, but it works). Replace all occurrences of phx-value-component-id={@component.id} with phx-value-component-id={@component.source.id}. There should be 4 occurrences (the buttons to add and remove groups and predicates)

3 Likes

Thanks! That seems to have fixed it!

1 Like

Thanks for helping out @dblack :raised_hands:

1 Like