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.15.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}>
  <Flop.Phoenix.filter_fields :let={entry} form={f} fields={[:name, :email]}>
    <%= entry.label %>
    <%= entry.input %>
  </Flop.Phoenix.filter_fields>
</.form>

Refer to the Flop.Phoenix module documentation for more examples.

custom-filter-form-component

Custom filter form component

Your filter form probably requires a bit of custom markup. 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={%{input: input, label: label}}
          form={f}
          fields={@fields}
          input_opts={[phx_debounce: @debounce]}
        >
          <div class="field">
            <span class="visually-hidden"><%= label %></span>
            <%= input %>
          </div>
        </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
    )

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