View Source Flop Phoenix

CI Hex Coverage Status

Phoenix components for pagination, sortable tables and filter forms with Flop and Ecto.

installation

Installation

Add flop_phoenix to your list of dependencies in the mix.exs of your Phoenix application.

def deps do
  [
    {:flop_phoenix, "~> 0.17.0"}
  ]
end

Follow the instructions in the Flop documentation to set up your business logic.

fetch-the-data

Fetch the data

Define a function that calls Flop.validate_and_run/3 to query the list of pets.

defmodule MyApp.Pets do
  alias MyApp.Pet

  def list_pets(params) do
    Flop.validate_and_run(Pet, params, for: Pet)
  end
end

In your controller, pass the data and the Flop meta struct to your template.

defmodule MyAppWeb.PetController do
  use MyAppWeb, :controller

  alias MyApp.Pets

  action_fallback MyAppWeb.FallbackController

  def index(conn, params) do
    with {:ok, {pets, meta}} <- Pets.list_pets(params) do
      render(conn, "index.html", meta: meta, pets: pets)
    end
  end
end

You can fetch the data similarly in the handle_params/3 function of a LiveView or the update/2 function of a LiveComponent.

defmodule MyAppWeb.PetLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.Pets

  @impl Phoenix.LiveView
  def handle_params(params, _, socket) do
    case Pets.list_pets(params) do
      {:ok, {pets, meta}} ->
        {:noreply, assign(socket, %{pets: pets, meta: meta})}

      _ ->
        {:noreply, push_navigate(socket, to: Routes.pet_index_path(socket, :index))}
    end
  end
end

sortable-tables-and-pagination

Sortable tables and pagination

In your template, add a sortable table and pagination links.

<h1>Pets</h1>

<Flop.Phoenix.table
  items={@pets}
  meta={@meta}
  path={{Routes, :pet_path, [@socket, :index]}}
>
  <:col :let={pet} label="Name" field={:name}><%= pet.name %></:col>
  <:col :let={pet} label="Age" field={:age}><%= pet.age %></:col>
</Flop.Phoenix.table>

<Flop.Phoenix.pagination
  meta={@meta}
  path={{Routes, :pet_path, [@socket, :index]}}
/>

path should reference the path helper function that builds a path to the current page. Add any additional path and query parameters to the argument list.

<Flop.Phoenix.pagination
  meta={@meta}
  path={{Routes, :pet_path, [@conn, :index, @owner, [hide_menu: true]]}}
/>

Alternatively, you can pass a URI string, which allows you to use the verified routes introduced in Phoenix 1.7.

<Flop.Phoenix.pagination
  meta={@meta}
  path={~p"/pets"}
/>

You can also use a custom path builder function, in case you need to set some parameters in the path instead of the query. For more examples, have a look at the documentation of Flop.Phoenix.build_path/3. The path assign can use any format that is accepted by Flop.Phoenix.build_path/3.

If you pass the for option when making the query with Flop, Flop Phoenix can determine which table columns are sortable. It also hides the order and page_size parameters if they match the default values defined with Flop.Schema.

See Flop.Phoenix.cursor_pagination/1 for instructions to set up cursor-based pagination.

filter-forms

Filter forms

This library implements Phoenix.HTML.FormData for the Flop.Meta struct, which means you can pass the struct to the Phoenix form functions. The easiest way to render a filter form is to use the Flop.Phoenix.filter_fields/1 component:

<.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}
    />
  </.filter_fields>
</.form>

The filter_fields component renders all necessary hidden inputs, but it does not render the inputs for the filter values on its own. Instead, it passes all necessary details to the inner block. This allows you to render the filter inputs with your custom input component.

You can pass additional options for each field. Refer to the Flop.Phoenix.filter_fields/1 documentation for details.

custom-filter-form-component

Custom filter form component

It is recommended to define a custom filter_form component that wraps Flop.Phoenix.filter_fields/1, so that you can apply the same markup throughout your live views.

attr :meta, Flop.Meta, required: true
attr :fields, :list, required: true
attr :id, :string, default: nil
attr :change_event, :string, default: "update-filter"
attr :reset_event, :string, default: "reset-filter"
attr :target, :string, default: nil
attr :debounce, :integer, default: 100

def filter_form(assigns) do
  ~H"""
  <div class="filter-form">
    <.form
      :let={f}
      for={@meta}
      as={:filter}
      id={@id}
      phx-target={@target}
      phx-change={@change_event}
    >
      <div class="filter-form-inputs">
        <Flop.Phoenix.filter_fields :let={i} form={f} fields={@fields}>
          <.input
            id={i.id}
            name={i.name}
            label={i.label}
            type={i.type}
            value={i.value}
            field={{i.form, i.field}}
            hide_labels={true}
            phx-debounce={@debounce}
            {i.rest}
          />
        </Flop.Phoenix.filter_fields>
      </div>

      <div class="filter-form-reset">
        <a href="#" class="button" phx_target={@target} phx_click={@reset_event}>
          reset
        </a>
      </div>
    </.form>
  </div>
  """
end

Now you can render a filter form like this:

<.filter_form
  fields={[:name, :email]}
  meta={@meta}
  id="user-filter-form"
/>

You will need to handle the update-filter and reset-filter events with the handle_event/3 callback function of your LiveView.

@impl true
def handle_event("update-filter", %{"filter" => params}, socket) do
  {:noreply,
   push_patch(socket, to: Routes.pet_index_path(socket, :index, params))}
end

@impl true
def handle_event("reset-filter", _, %{assigns: assigns} = socket) do
  flop = assigns.meta.flop |> Flop.set_page(1) |> Flop.reset_filters()

  path =
    Flop.Phoenix.build_path(
      {Routes, :pet_index_path, [socket, :index]},
      flop,
      backend: assigns.meta.backend
    )

  {:noreply, push_patch(socket, to: path)}
end