View Source AshPagify.FilterForm (ash_pagify v1.1.0)
A module to help you create complex forms that generate Ash filters.
Disclaimer
This is a copy of the
AshPhoenix.FilterForm
module from theash_phoenix
package. We made some changes such asnillify_blanks?
option inparams_for_query/2
function. Further we fixed the issue with the duplicated [components][index] suffix in case you restore the form from the params. Additonal you can now provide aninitial_form
toAshPagify.FilterForm.new/2
to enforce a specific structure for the form and then merge in the params.
# Create a FilterForm
filter_form = AshPagify.FilterForm.new(MyApp.Payroll.Employee)
FilterForm's comprise two concepts, predicates and groups. Predicates are the simple boolean
expressions you can use to build a query (name == "Joe"
), and groups can be used to group
predicates and more groups together. Groups can apply and
or or
operators to its nested
components.
# Add a predicate to the root of the form (which is itself a group)
filter_form = AshPagify.FilterForm.add_predicate(filter_form, :some_field, :eq, "Some Value")
# Add a group and another predicate to that group
{filter_form, group_id} = AshPagify.FilterForm.add_group(filter_form, operator: :or, return_id?: true)
filter_form = AshPagify.FilterForm.add_predicate(filter_form, :another, :eq, "Other", to: group_id)
validate/1
is used to merge the submitted form params into the filter form, and one of the
provided filter functions to apply the filter as a query, or generate an expression map,
depending on your requirements:
filter_form = AshPagify.FilterForm.validate(socket.assigns.filter_form, params)
# Generate a query and pass it to the Domain
query = AshPagify.FilterForm.filter!(MyApp.Payroll.Employee, filter_form)
filtered_employees = MyApp.Payroll.read!(query)
# Or use one of the other filter functions
AshPagify.FilterForm.to_filter_expression(filter_form)
AshPagify.FilterForm.to_filter_map(filter_form)
LiveView Example
You can build a form and handle adding and removing nested groups and predicates with the following:
alias MyApp.Payroll.Employee
@impl true
def render(assigns) do
~H"""
<.simple_form
:let={filter_form}
for={@filter_form}
phx-change="filter_validate"
phx-submit="filter_submit"
>
<.filter_form_component component={filter_form} />
<:actions>
<.button>Submit</.button>
</:actions>
</.simple_form>
<.table id="employees" rows={@employees}>
<:col :let={employee} label="Payroll ID"><%= employee.employee_id %></:col>
<:col :let={employee} label="Name"><%= employee.name %></:col>
<:col :let={employee} label="Position"><%= employee.position %></:col>
</.table>
"""
end
attr :component, :map, required: true, doc: "Could be a FilterForm (group) or a Predicate"
defp filter_form_component(%{component: %{source: %AshPagify.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">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.source.id} type="button">
Add Group
</.button>
<.button
phx-click="add_filter_predicate"
phx-value-component-id={@component.source.id}
type="button"
>
Add Predicate
</.button>
<.button
phx-click="remove_filter_component"
phx-value-component-id={@component.source.id}
type="button"
>
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={AshPagify.FilterForm.fields(Employee)}
field={@component[:field]}
/>
<.input
type="select"
options={AshPagify.FilterForm.predicates(Employee)}
field={@component[:operator]}
/>
<.input field={@component[:value]} />
<.button
phx-click="remove_filter_component"
phx-value-component-id={@component.source.id}
type="button"
>
Remove
</.button>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:filter_form, AshPagify.FilterForm.new(Employee))
|> assign(:employees, Employee.read_all!())
{:ok, socket}
end
@impl true
def handle_event("filter_validate", %{"filter" => params}, socket) do
{:noreply,
assign(socket,
filter_form: AshPagify.FilterForm.validate(socket.assigns.filter_form, params)
)}
end
@impl true
def handle_event("filter_submit", %{"filter" => params}, socket) do
filter_form = AshPagify.FilterForm.validate(socket.assigns.filter_form, params)
case AshPagify.FilterForm.filter(Employee, filter_form) do
{:ok, query} ->
{:noreply,
socket
|> assign(:employees, Employee.read_all!(query: query))
|> assign(:filter_form, filter_form)}
{:error, filter_form} ->
{:noreply, assign(socket, filter_form: filter_form)}
end
end
@impl true
def handle_event("remove_filter_component", %{"component-id" => component_id}, socket) do
{:noreply,
assign(socket,
filter_form:
AshPagify.FilterForm.remove_component(socket.assigns.filter_form, component_id)
)}
end
@impl true
def handle_event("add_filter_group", %{"component-id" => component_id}, socket) do
{:noreply,
assign(socket,
filter_form: AshPagify.FilterForm.add_group(socket.assigns.filter_form, to: component_id)
)}
end
@impl true
def handle_event("add_filter_predicate", %{"component-id" => component_id}, socket) do
{:noreply,
assign(socket,
filter_form:
AshPagify.FilterForm.add_predicate(socket.assigns.filter_form, :name, :contains, nil,
to: component_id
)
)}
end
Summary
Functions
Helper function to extract all active filter form fields from a AshPagify.Meta struct.
Add a group to the filter. A group can contain predicates and other groups, allowing you to build quite complex nested filters.
Add a predicate to the filter.
Count the number of records that match the filter form parameters.
Returns a flat list of all errors on all predicates in the filter.
Helper function to extract all filter form fields from a AshPhoenix.FilterForm parameter.
Returns the list of available fields, which may be attributes, calculations, or aggregates.
Converts the form into a filter, and filters the provided query or resource with that filter.
Same as filter/2
but raises on errors.
Create a new filter form.
Returns the minimal set of params (at the moment just strips ids) for use in a query string.
Returns the list of available predicates for the given resource, which may be functions or operators.
Removes the group or predicate with the given id
Remove the group with the given id
Remove the predicate with the given id
Returns a filter expression that can be provided to Ash.Query.filter/2
Same as to_filter_expression/1
but raises on errors.
Returns a filter map that can be provided to Ash.Filter.parse
Update the predicates of the nested_form with the given key.
Update the predicate with the given id
Updates the filter with the provided input and validates it.
Types
@type t() :: %AshPagify.FilterForm{ components: [term() | t()], id: String.t(), key: term(), name: String.t(), negated?: boolean(), operator: :and | :or, params: map(), remove_empty_groups?: boolean(), resource: Ash.Resource.t(), serializer: (term() -> term()) | nil, transform_errors: term(), valid?: boolean() }
Functions
@spec active_filter_form_fields(AshPagify.Meta.t()) :: list()
Helper function to extract all active filter form fields from a AshPagify.Meta struct.
Add a group to the filter. A group can contain predicates and other groups, allowing you to build quite complex nested filters.
Options:
:to
(String.t/0
) - The nested group id to add the group to.:operator
- The operator that the group should have internally. Valid values are :and, :or The default value is:and
.:key
(term/0
) - The key to use for the group. The default value isnil
.:return_id?
(boolean/0
) - If set totrue
, the function returns{form, predicate_id}
The default value isfalse
.
add_predicate(form, field, operator_or_function, value, opts \\ [])
View SourceAdd a predicate to the filter.
Options:
:to
(String.t/0
) - The group id to add the predicate to. If not set, will be added to the top level group.:return_id?
(boolean/0
) - If set totrue
, the function returns{form, predicate_id}
The default value isfalse
.:path
- The relationship path to apply the predicate to
@spec count(AshPagify.Meta.t(), map(), boolean(), Ash.Query.t() | nil) :: non_neg_integer()
Count the number of records that match the filter form parameters.
If you pass a query, it will be used to count the records. Otherwise, the resource from the meta struct will be used.
If you pass reset: true
, the filter form will be reset to an empty map.
Returns a flat list of all errors on all predicates in the filter.
Helper function to extract all filter form fields from a AshPhoenix.FilterForm parameter.
Returns the list of available fields, which may be attributes, calculations, or aggregates.
Converts the form into a filter, and filters the provided query or resource with that filter.
Same as filter/2
but raises on errors.
Create a new filter form.
Options:
:params
(term/0
) - Initial parameters to create the form with The default value is%{}
.:as
(String.t/0
) - Set the parameter name for the form. The default value is"filter"
.:key
(term/0
) - Set the parameter key for the form. The default value isnil
.:transform_errors
(term/0
) - Allows for manual manipulation and transformation of errors.
If possible, try to implementAshPhoenix.FormData.Error
for the error (if it as a custom one, for example). If that isn't possible, you can provide this function which will get the predicate and the error, and should return a list of ash phoenix formatted errors, e.g[{field :: atom, message :: String.t(), substituations :: Keyword.t()}]
:remove_empty_groups?
(boolean/0
) - If true (the default), then any time a group would be made empty by removing a group or predicate, it is removed instead.
An empty form can still be added, this only affects a group if its last component is removed. The default value isfalse
.:root?
(boolean/0
) - If true (the default), the form's name will not be suffixed with [components][index] when adding / validating components.
This flag is used internally and should not be set manually. The default value istrue
.:initial_form
(term/0
) - The initial form to use when creating a new form.
This is usefully if you want to enforce a specific structure for the form and then merge in the params.:serializer
(term/0
) - A function that will be called on the predicate param during new predicate initialization.
This is useful for custom serialization of the form input values. The default value isnil
.
params_for_query(form, opts \\ [nillify_blanks?: true, keep_keys?: false])
View SourceReturns the minimal set of params (at the moment just strips ids) for use in a query string.
If nillify_blanks? is true (default to true), then any blank values will be set to nil and not included in the params. Furthermore, if a nested group results to an empty group (after nillification of it's components), it will be removed as well.
Returns the list of available predicates for the given resource, which may be functions or operators.
Removes the group or predicate with the given id
Remove the group with the given id
Remove the predicate with the given id
Returns a filter expression that can be provided to Ash.Query.filter/2
To add this to a query, remember to use ^
, for example:
filter = AshPagify.FilterForm.to_filter_expression(form)
Ash.Query.filter(MyApp.Post, ^filter)
Alternatively, you can use the shorthand: filter/2
to apply the expression directly to a query.
Same as to_filter_expression/1
but raises on errors.
Returns a filter map that can be provided to Ash.Filter.parse
This allows for things like saving a stored filter. Does not currently support parameterizing calculations or functions.
Update the predicates of the nested_form with the given key.
Works also for predicates in nested forms inside the nested form.
Update the predicate with the given id
Updates the filter with the provided input and validates it.
At present, no validation actually occurs, but this will eventually be added.
Passing reset_on_change?: false
into opts
will prevent predicates to reset
the value
and operator
fields to nil
if the predicate field
changes.