` element."
attr :opts, :list,
default: [],
doc: """
Keyword list with additional options (see `t:Flop.Phoenix.table_option/0`).
Note that the options passed to the function are deep merged into the
default options. Since these options will likely be the same for all the
tables in a project, it is recommended to define them once in a function or
set them in a wrapper function as described in the `Customization` section
of the module documentation.
"""
slot :col,
required: true,
doc: """
For each column to render, add one `<:col>` element.
```elixir
<:col :let={pet} label="Name" field={:name} col_style="width: 20%;">
<%= pet.name %>
```
Any additional assigns will be added as attributes to the `` elements.
""" do
attr :label, :string, doc: "The content for the header column."
attr :field, :atom, doc: "The field name for sorting."
attr :show, :boolean,
doc: "Boolean value to conditionally show the column. Defaults to `true`."
attr :hide, :boolean,
doc:
"Boolean value to conditionally hide the column. Defaults to `false`."
attr :col_style, :string,
doc: """
If set, a `` element is rendered and the value of the
`col_style` assign is set as `style` attribute for the `` element of
the respective column. You can set the `width`, `background` and `border`
of a column this way.
"""
attr :rest, :global,
doc: """
Any additional attributes to pass to the ``.
"""
end
slot :foot,
default: nil,
doc: """
You can optionally add a `foot`. The inner block will be rendered inside
a `tfoot` element.
<:foot>
| Total: <%= @total %> |
"""
def table(assigns) do
assigns = Table.init_assigns(assigns)
~H"""
<%= if @items == [] do %>
<%= @opts[:no_results_content] %>
<% else %>
<%= if @opts[:container] do %>
<% else %>
<% end %>
<% end %>
"""
end
@doc """
Renders all inputs for a filter form including the hidden inputs.
## Example
<.form :let={f} for={@meta}>
<.filter_fields :let={i} form={f} fields={[:email, :name]}>
<.input
id={i.id}
name={i.name}
label={i.label}
type={i.type}
value={i.value}
field={{i.form, i.field}}
{i.rest}
/>
This assumes that you have defined an `input` component that renders a form
input including the label.
Most options passed to the inner block should be self-explaining.
- The `type` is the input type as a string, _not_ the name of the
`Phoenix.HTML.Form` input function (e.g. `"text"`, not `:text_input`). The
type is derived from the type of the field being filtered on, but it can
be overridden in the field options.
- The `field` is a `Phoenix.HTML.Form.t` / field name tuple
(e.g. `{f, :name}`).
- `rest` contains any additional field options passed.
## Field configuration
The fields can be passed as atoms or keywords with additional options.
fields={[:name, :email]}
Or
fields={[
name: [label: gettext("Name")],
email: [
label: gettext("Email"),
op: :ilike_and,
type: "email_input"
],
age: [
label: gettext("Age"),
type: "select",
prompt: "",
options: [
{gettext("young"), :young},
{gettext("old"), :old)}
]
]
]}
Available options:
- `label`
- `op`
- `type`
Any additional options will be passed to the input component (e.g. HTML
classes or a list of options).
"""
@doc since: "0.12.0"
@doc section: :components
@spec filter_fields(map) :: Phoenix.LiveView.Rendered.t()
attr :form, Phoenix.HTML.Form, required: true
attr :fields, :list,
default: [],
doc: """
The list of fields and field options. Note that inputs will not be rendered
for fields that are not marked as filterable in the schema
(see `Flop.Schema`).
If `dynamic` is set to `false`, only fields in this list are rendered. If
`dynamic` is set to `true`, only fields for filters present in the given
`Flop.Meta` struct are rendered, and the fields are rendered even if they
are not passed in the `fields` list. In the latter case, `fields` is
optional, but you can still pass label and input configuration this way.
Note that in a dynamic form, it is not possible to configure a single field
multiple times.
"""
attr :dynamic, :boolean,
default: false,
doc: """
If `true`, fields are only rendered for filters that are present in the
`Flop.Meta` struct passed to the form. You can use this for rendering filter
forms that allow the user to add and remove filters dynamically. The
`fields` assign is only used for looking up the options in that case.
"""
slot :inner_block,
doc: """
The necessary options for rendering a label and an input are passed to the
inner block, which allows you to render the fields with your existing
components.
<.filter_fields :let={i} form={f} fields={[:email, :name]}>
<.label for={i.id}><%= i.label %>
<.input
id={i.id}
name={i.name}
label={i.label}
type={i.type}
value={i.value}
field={{i.form, i.field}}
{i.rest}
/>
The options passed to the inner block are:
- `id` - The input ID.
- `name` - The input name.
- `label` - The label text as a string.
- `type` - The input type as a string. This is _not_ the value returned
by `Phoenix.HTML.Form.input_type/2`.
- `value` - The input value.
- `form` - The `%Phoenix.HTML.Form{}` struct.
- `field` - The field name as an atom.
- `rest` - Any additional options passed in the field options.
"""
def filter_fields(assigns) do
is_meta_form!(assigns.form)
fields = normalize_filter_fields(assigns[:fields] || [])
field_opts = match_field_opts(assigns, fields)
inputs_for_fields = if assigns[:dynamic], do: nil, else: fields
assigns =
assigns
|> assign(:fields, inputs_for_fields)
|> assign(:field_opts, field_opts)
~H"""
<.hidden_inputs_for_filter form={@form} />
<%= for {ff, opts} <- inputs_for_filters(@form, @fields, @field_opts) do %>
<.hidden_inputs_for_filter form={ff} />
<%= render_slot(@inner_block, %{
id: Phoenix.HTML.Form.input_id(ff, :value),
name: Phoenix.HTML.Form.input_name(ff, :value),
label: input_label(ff, opts[:label]),
type: type_for(ff, opts[:type]),
value: Phoenix.HTML.Form.input_value(ff, :value),
form: ff,
field: :value,
rest: Keyword.drop(opts, [:label, :op, :type])
}) %>
<% end %>
"""
end
defp inputs_for_filters(form, fields, field_opts) do
form
|> inputs_for(:filters, fields: fields)
|> Enum.zip(field_opts)
end
defp normalize_filter_fields(fields) do
Enum.map(fields, fn
field when is_atom(field) ->
{field, []}
{field, opts} when is_atom(field) and is_list(opts) ->
{field, opts}
field ->
raise """
Invalid filter field config
Filters fields must be passed as a list of atoms or {atom, keyword} tuples.
Got:
#{inspect(field)}
"""
end)
end
defp match_field_opts(%{dynamic: true, form: form}, fields) do
Enum.map(form.data.filters, fn %Flop.Filter{field: field} ->
fields[field] || []
end)
end
defp match_field_opts(_, fields) do
Keyword.values(fields)
end
defp input_label(_form, text) when is_binary(text), do: text
defp input_label(form, nil), do: form |> input_value(:field) |> humanize()
defp type_for(_form, type) when is_binary(type), do: type
defp type_for(form, nil), do: input_type_as_string(form)
defp input_type_as_string(form) do
form
|> input_type(:value)
|> to_html_input_type()
end
# coveralls-ignore-start
defp to_html_input_type(:checkbox), do: "checkbox"
defp to_html_input_type(:color_input), do: "color"
defp to_html_input_type(:date_input), do: "date"
defp to_html_input_type(:date_select), do: "date"
defp to_html_input_type(:datetime_local_input), do: "datetime-local"
defp to_html_input_type(:datetime_select), do: "datetime-local"
defp to_html_input_type(:email_input), do: "email"
defp to_html_input_type(:file_input), do: "file"
defp to_html_input_type(:hidden_input), do: "hidden"
defp to_html_input_type(:multiple_select), do: "select"
defp to_html_input_type(:number_input), do: "number"
defp to_html_input_type(:password_input), do: "password"
defp to_html_input_type(:radio_button), do: "radio"
defp to_html_input_type(:range_input), do: "range"
defp to_html_input_type(:search_input), do: "search"
defp to_html_input_type(:select), do: "select"
defp to_html_input_type(:telephone_input), do: "tel"
defp to_html_input_type(:text_input), do: "text"
defp to_html_input_type(:textarea), do: "textarea"
defp to_html_input_type(:time_input), do: "time"
defp to_html_input_type(:time_select), do: "time"
defp to_html_input_type(:url_input), do: "url"
# coveralls-ignore-end
defp is_meta_form!(%Form{data: %Flop{}, source: %Meta{}}), do: :ok
defp is_meta_form!(_) do
raise ArgumentError, """
must be used with a filter form
Example:
<.form :let={f} for={@meta}>
<.filter_fields :let={i} form={f} fields={[:email, :name]}>
<.label for={i.id}><%= i.label %>
<.input
id={i.id}
name={i.name}
label={i.label}
type={i.type}
value={i.value}
field={{i.form, i.field}}
{i.rest}
/>
"""
end
@doc """
Renders hidden inputs for the given form.
You can use this for convenience if you have a complex form layout that cannot
be accomplished with `Flop.Phoenix.filter_fields/1`. Put it as a direct child
of the `form` component to render the hidden inputs for pagination and order
parameters. Then use `Phoenix.HTML.Form.inputs_for/3` to render a single
filter field, and place this component within the `do` block to render the
hidden inputs for the filter field and operator.
<.form :let={f} for={@meta}>
<.hidden_inputs_for_filter form={@form} />
<%= for ff <- Phoenix.HTML.Form.inputs_for(f, :filters, fields: [:name]) do %>
<.hidden_inputs_for_filter form={ff} />
<.input label="Name" type="text" field={{ff, :value}} />
<% end %>
<%= for ff <- Phoenix.HTML.Form.inputs_for(f, :filters, fields: [:email]) do %>
<.hidden_inputs_for_filter form={ff} />
<.input label="E-mail" type="email" field={{ff, :value}} />
<% end %>
"""
@doc since: "0.16.0"
@doc section: :components
attr :form, Phoenix.HTML.Form, required: true
attr :id, :string, default: nil
def hidden_inputs_for_filter(assigns) do
~H"""
<%= for {field, value} <- @form.hidden do %>
<.hidden_inputs form={@form} field={field} value={value} />
<% end %>
"""
end
attr :form, Phoenix.HTML.Form, required: true
attr :field, :atom, required: true
attr :value, :any, required: true
defp hidden_inputs(%{field: _, value: value} = assigns)
when is_list(value) do
~H"""
<%= for {v, index} <- Enum.with_index(@value) do %>
"_#{index}"}
name={input_name(@form, @field) <> "[]"}
value={v}
/>
<% end %>
"""
end
defp hidden_inputs(assigns) do
~H"""
"""
end
@doc """
Converts a Flop struct into a keyword list that can be used as a query with
Phoenix route helper functions.
Default limits and default order parameters are omitted.
The defaults are determined by calling `Flop.get_option/3`, which means you
can pass `default_limit` and `default_order` directly, you can pass the `:for`
option to pick up the default options from a schema module deriving
`Flop.Schema`, and you can pass the `backend` option, so that Flop can fall
back to the your backend options. If the defaults are set at neither of these
places, it will fall back to the options set in the application environment.
## Examples
iex> to_query(%Flop{})
[]
iex> f = %Flop{order_by: [:name, :age], order_directions: [:desc, :asc]}
iex> to_query(f)
[order_directions: [:desc, :asc], order_by: [:name, :age]]
iex> f |> to_query |> Plug.Conn.Query.encode()
"order_directions[]=desc&order_directions[]=asc&order_by[]=name&order_by[]=age"
iex> f = %Flop{page: 5, page_size: 20}
iex> to_query(f)
[page_size: 20, page: 5]
iex> f = %Flop{first: 20, after: "g3QAAAABZAAEbmFtZW0AAAAFQXBwbGU="}
iex> to_query(f)
[first: 20, after: "g3QAAAABZAAEbmFtZW0AAAAFQXBwbGU="]
iex> f = %Flop{
...> filters: [
...> %Flop.Filter{field: :name, op: :=~, value: "Mag"},
...> %Flop.Filter{field: :age, op: :>, value: 25}
...> ]
...> }
iex> to_query(f)
[
filters: %{
0 => %{field: :name, op: :=~, value: "Mag"},
1 => %{field: :age, op: :>, value: 25}
}
]
iex> f |> to_query() |> Plug.Conn.Query.encode()
"filters[0][field]=name&filters[0][op]=%3D~&filters[0][value]=Mag&filters[1][field]=age&filters[1][op]=%3E&filters[1][value]=25"
iex> f = %Flop{page: 5, page_size: 20}
iex> to_query(f, default_limit: 20)
[page: 5]
"""
@doc since: "0.6.0"
@doc section: :miscellaneous
@spec to_query(Flop.t()) :: keyword
def to_query(%Flop{filters: filters} = flop, opts \\ []) do
filter_map =
filters
|> Stream.with_index()
|> Enum.into(%{}, fn {filter, index} ->
{index, Map.from_struct(filter)}
end)
default_limit = Flop.get_option(:default_limit, opts)
default_order = Flop.get_option(:default_order, opts)
[]
|> Misc.maybe_put(:offset, flop.offset, 0)
|> Misc.maybe_put(:page, flop.page, 1)
|> Misc.maybe_put(:after, flop.after)
|> Misc.maybe_put(:before, flop.before)
|> Misc.maybe_put(:page_size, flop.page_size, default_limit)
|> Misc.maybe_put(:limit, flop.limit, default_limit)
|> Misc.maybe_put(:first, flop.first, default_limit)
|> Misc.maybe_put(:last, flop.last, default_limit)
|> Misc.maybe_put_order_params(flop, default_order)
|> Misc.maybe_put(:filters, filter_map)
end
@doc """
Builds a path that includes query parameters for the given `Flop` struct
using the referenced Phoenix path helper function.
The first argument can be either one of:
- an MFA tuple (module, function name as atom, arguments)
- a 2-tuple (function, arguments)
- a URL string (e.g. `"/some/path"`; this option has been added so that you
can use Phoenix verified routes with the library)
- a function that takes the Flop parameters as a keyword list as an argument
Default values for `limit`, `page_size`, `order_by` and `order_directions` are
omitted from the query parameters. To pick up the default parameters from a
schema module deriving `Flop.Schema`, you need to pass the `:for` option. To
pick up the default parameters from the backend module, you need to pass the
`:backend` option. If you pass a `Flop.Meta` struct as the second argument,
these options are retrieved from the struct automatically.
## Examples
### With an MFA tuple
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path(
...> {Flop.PhoenixTest, :route_helper, [%Plug.Conn{}, :pets]},
...> flop
...> )
"/pets?page_size=10&page=2"
### With a function/arguments tuple
iex> pet_path = fn _conn, :index, query ->
...> "/pets?" <> Plug.Conn.Query.encode(query)
...> end
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path({pet_path, [%Plug.Conn{}, :index]}, flop)
"/pets?page_size=10&page=2"
We're defining fake path helpers for the scope of the doctests. In a real
Phoenix application, you would pass something like
`{Routes, :pet_path, args}` or `{&Routes.pet_path/3, args}` as the
first argument.
### Passing a `Flop.Meta` struct or a keyword list
You can also pass a `Flop.Meta` struct or a keyword list as the third
argument.
iex> pet_path = fn _conn, :index, query ->
...> "/pets?" <> Plug.Conn.Query.encode(query)
...> end
iex> flop = %Flop{page: 2, page_size: 10}
iex> meta = %Flop.Meta{flop: flop}
iex> build_path({pet_path, [%Plug.Conn{}, :index]}, meta)
"/pets?page_size=10&page=2"
iex> query_params = to_query(flop)
iex> build_path({pet_path, [%Plug.Conn{}, :index]}, query_params)
"/pets?page_size=10&page=2"
### Additional path parameters
If the path helper takes additional path parameters, just add them to the
second argument.
iex> user_pet_path = fn _conn, :index, id, query ->
...> "/users/\#{id}/pets?" <> Plug.Conn.Query.encode(query)
...> end
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path({user_pet_path, [%Plug.Conn{}, :index, 123]}, flop)
"/users/123/pets?page_size=10&page=2"
### Additional query parameters
If the last path helper argument is a query parameter list, the Flop
parameters are merged into it.
iex> pet_url = fn _conn, :index, query ->
...> "https://pets.flop/pets?" <> Plug.Conn.Query.encode(query)
...> end
iex> flop = %Flop{order_by: :name, order_directions: [:desc]}
iex> build_path({pet_url, [%Plug.Conn{}, :index, [user_id: 123]]}, flop)
"https://pets.flop/pets?user_id=123&order_directions[]=desc&order_by=name"
iex> build_path(
...> {pet_url,
...> [%Plug.Conn{}, :index, [category: "small", user_id: 123]]},
...> flop
...> )
"https://pets.flop/pets?category=small&user_id=123&order_directions[]=desc&order_by=name"
### With a URI string or verified route
You can also use this function with a verified route. Note that this example
uses a plain string which isn't verified, because we need the doctest to work,
and `flop_phoenix` does not depend on Phoenix 1.7. In a real application with
Phoenix 1.7, you would use the `p` sigil instead (`~p"/pets"`).
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path("/pets", flop)
"/pets?page=2&page_size=10"
The Flop query parameters will be merged into existing query parameters.
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path("/pets?species=dogs", flop)
"/pets?page=2&page_size=10&species=dogs"
### Set page as path parameter
Finally, you can also pass a function that takes the Flop parameters as
a keyword list as an argument. Default values will not be included in the
parameters passed to the function. You can use this if you need to set some
of the parameters as path parameters instead of query parameters.
iex> flop = %Flop{page: 2, page_size: 10}
iex> build_path(
...> fn params ->
...> {page, params} = Keyword.pop(params, :page)
...> query = Plug.Conn.Query.encode(params)
...> if page, do: "/pets/page/\#{page}?\#{query}", else: "/pets?\#{query}"
...> end,
...> flop
...> )
"/pets/page/2?page_size=10"
Note that in this example, the anonymous function just returns a string. With
Phoenix 1.7, you will be able to use verified routes.
build_path(
fn params ->
{page, query} = Keyword.pop(params, :page)
if page, do: ~p"/pets/page/\#{page}?\#{query}", else: ~p"/pets?\#{query}"
end,
flop
)
Note that the keyword list passed to the path builder function is built using
`Plug.Conn.Query.encode/2`, which means filters are formatted as map with
integer keys.
### Set filter value as path parameter
If you need to set a filter value as a path parameter, you can use
`Flop.Phoenix.pop_filter/2` to manipulate the parameters (again, replace the
plain strings with verified routes and remove the `encode` line in Phoenix
1.7).
iex> flop = %Flop{
...> page: 5,
...> order_by: [:published_at],
...> filters: [
...> %Flop.Filter{field: :category, op: :==, value: "announcements"}
...> ]
...> }
iex> build_path(
...> fn params ->
...> {page, params} = Keyword.pop(params, :page)
...> {category, params} = pop_filter(params, :category)
...> query = Plug.Conn.Query.encode(params)
...>
...> case {page, category} do
...> {nil, nil} -> "/articles?\#{query}"
...> {page, nil} -> "/articles/page/\#{page}?\#{query}"
...> {nil, %{value: category}} -> "/articles/category/\#{category}?\#{query}"
...> {page, %{value: category}} -> "/articles/category/\#{category}/page/\#{page}?\#{query}"
...> end
...> end,
...> flop
...> )
"/articles/category/announcements/page/5?order_by[]=published_at"
"""
@doc since: "0.6.0"
@doc section: :miscellaneous
@spec build_path(
String.t()
| {module, atom, [any]}
| {function, [any]}
| (keyword -> String.t()),
Meta.t() | Flop.t() | keyword,
keyword
) :: String.t()
def build_path(path, meta_or_flop_or_params, opts \\ [])
def build_path(
path,
%Meta{backend: backend, flop: flop, schema: schema},
opts
) do
build_path(
path,
flop,
opts |> Keyword.put(:backend, backend) |> Keyword.put(:for, schema)
)
end
def build_path(path, %Flop{} = flop, opts) do
build_path(path, Flop.Phoenix.to_query(flop, opts))
end
def build_path({module, func, args}, flop_params, _opts)
when is_atom(module) and
is_atom(func) and
is_list(args) and
is_list(flop_params) do
final_args = build_final_args(args, flop_params)
apply(module, func, final_args)
end
def build_path({func, args}, flop_params, _opts)
when is_function(func) and
is_list(args) and
is_list(flop_params) do
final_args = build_final_args(args, flop_params)
apply(func, final_args)
end
def build_path(func, flop_params, _opts)
when is_function(func, 1) and is_list(flop_params) do
func.(flop_params)
end
def build_path(uri, flop_params, _opts)
when is_binary(uri) and is_list(flop_params) do
uri = URI.parse(uri)
query =
(uri.query || "")
|> Query.decode()
|> Map.merge(Map.new(flop_params))
query = if query != %{}, do: Query.encode(query), else: nil
uri
|> Map.put(:query, query)
|> URI.to_string()
end
defp build_final_args(args, flop_params) do
case Enum.reverse(args) do
[last_arg | rest] when is_list(last_arg) ->
query_arg = Keyword.merge(last_arg, flop_params)
Enum.reverse([query_arg | rest])
_ ->
args ++ [flop_params]
end
end
@doc """
Removes the first filter for the given field in the `Flop.t` struct or keyword
list and returns the filter value and the updated struct or keyword list.
If a keyword list is passed, it is expected to have the same format as
returned by `Flop.Phoenix.to_query/2`.
You can use this function to write a custom path builder function in cases
where you need to set a filter value as a path parameter instead of a query
parameter. See `Flop.Phoenix.build_path/3` for an example.
## Examples
### With a Flop struct
iex> flop = %Flop{
...> page: 5,
...> filters: [
...> %Flop.Filter{field: :category, op: :==, value: "announcements"},
...> %Flop.Filter{field: :title, op: :==, value: "geo"}
...> ]
...> }
iex> pop_filter(flop, :category)
{%Flop.Filter{field: :category, op: :==, value: "announcements"},
%Flop{
page: 5,
filters: [%Flop.Filter{field: :title, op: :==, value: "geo"}]
}}
iex> pop_filter(flop, :author)
{nil,
%Flop{
page: 5,
filters: [
%Flop.Filter{field: :category, op: :==, value: "announcements"},
%Flop.Filter{field: :title, op: :==, value: "geo"}
]
}
}
### With a keyword list
iex> params = [
...> filters: %{
...> 0 => %{field: :category, op: :==, value: "announcements"},
...> 1 => %{field: :title, op: :==, value: "geo"}
...> },
...> page: 5
...> ]
iex> pop_filter(params, :category)
{%{field: :category, op: :==, value: "announcements"},
[
filters: %{0 => %{field: :title, op: :==, value: "geo"}},
page: 5
]}
iex> pop_filter(params, :author)
{nil,
[
filters: %{
0 => %{field: :category, op: :==, value: "announcements"},
1 => %{field: :title, op: :==, value: "geo"}
},
page: 5
]}
iex> pop_filter([], :category)
{nil, []}
"""
@doc since: "0.15.0"
@doc section: :miscellaneous
@spec pop_filter(Flop.t(), atom) :: {any, Flop.t()}
@spec pop_filter(keyword, atom) :: {any, keyword}
def pop_filter(%Flop{} = flop, field) do
case Enum.find_index(flop.filters, &(&1.field == field)) do
nil ->
{nil, flop}
index ->
{filter, filters} = List.pop_at(flop.filters, index)
{filter, %{flop | filters: filters}}
end
end
def pop_filter(params, field) when is_list(params) do
filters = Keyword.get(params, :filters, %{})
index =
Enum.find_index(filters, fn {_, filter} ->
filter.field == field
end)
case index do
nil ->
{nil, params}
index ->
{filter, filters} = Map.pop(filters, index)
filters =
filters
|> Enum.with_index(fn {_, filter}, index -> {index, filter} end)
|> Enum.into(%{})
{filter, Keyword.put(params, :filters, filters)}
end
end
end
| |