flop_phoenix

repository·main·Indexed 19 days ago

https://github.com/woylie/flop_phoenix

Phoenix components for pagination, sortable tables, and filter forms, designed to work with the Flop and Ecto libraries. It provides Flop.Phoenix.table and Flop.Phoenix.pagination components, as well as Flop.Phoenix.filter_fields for creating filter forms. The library supports page-based and cursor-based pagination, LiveView streams, and event-based sorting and pagination.

Tokens
11.7K
Snippets
28
Records
33
Agent score
18%

What's inside flop_phoenix

  1. Implement event-based pagination and sorting

    main
    If you want to avoid appending parameters to the URL (e.g., for widgets or multiple pageable areas), use the on_paginate and on_sort attributes on the components instead of the path attribute. You must then handle these events using the handle_event/3 callback in your LiveView.
  2. Create filter forms with Flop.Phoenix.filter_fields

    main

    Flop.Meta implements Phoenix.HTML.FormData, allowing it to be used directly with Phoenix forms.

    To render a filter form:

    1. Convert the meta struct to a form using Phoenix.Component.to_form/1.
    2. Use the Flop.Phoenix.filter_fields/1 component inside a <.form> block. This component generates necessary hidden inputs and provides a :let binding containing field and label details for each field.
    3. You must provide your own input components (e.g., <.input />) inside the filter_fields block to render the actual visible inputs.
    4. Handle the form's change/submit event (e.g., phx-change="update-filter") in your LiveView to update the URL or state.
    # Example of a custom filter form component
    def filter_form(%{meta: meta} = assigns) do
      assigns = assign(assigns, form: Phoenix.Component.to_form(meta), meta: nil)
    
      ~H"""
      <.form
        for={@form}
        id={@id}
        phx-target={@target}
        phx-change={@on_change}
        phx-submit={@on_change}
      >
        <.filter_fields :let={i} form={@form} fields={@fields}>
          <.input
            field={i.field}
            label={i.label}
            type={i.type}
            phx-debounce={120}
            {i.rest}
          />
        </.filter_fields>
    
        <button name="reset">reset</button>
      </.form>
      """
    end
    
    # Usage in HEEx
    <.filter_form
      fields={[:name, :email]}
      meta={@meta}
      id="user-filter-form"
    />
    
    # LiveView event handler
    @impl true
    def handle_event("update-filter", params, socket) do
      params = Map.delete(params, "_target")
      {:noreply, push_patch(socket, to: ~p"/pets?#{params}")}
    end
  3. Style the Flop.Phoenix.pagination component

    main

    The Flop.Phoenix.pagination component does not include default styles. You can style it by passing a class attribute to the component, which will be applied to the wrapping <nav> element.

    To style the component effectively, you can target the following internal elements:

    • The wrapper: The <nav> element (targeted via the class you provided).
    • Page numbers: An unordered list (<ul>) that is a direct descendant of the <nav> element, containing list items (<li>) for each page.
    • Ellipsis: A classless <span> inside an <li> (e.g., li > span).
    • Navigation controls: <a> or <button> elements used for the 'previous' and 'next' controls, as well as the page numbers. Note that Flop.Phoenix uses <a> if a path is provided, otherwise it uses <button>.
    • Current page: The active page link/button is identified by the [aria-current="page"] attribute.
    • Disabled states: Disabled controls are identified by the [disabled] attribute (for buttons) or [aria-disabled="true"] attribute (for links).
    <Flop.Phoenix.pagination class="pagination" ...>
      <%!-- ... --%>
    </Flop.Phoenix.pagination>
  4. Use LiveView streams with Flop.Phoenix.table

    main

    To use LiveView streams with the table component:

    1. In handle_params/3, use stream/3 to assign your data to the socket (e.g., stream(:pets, pets, reset: true)).
    2. Pass @streams.pets to the items attribute of Flop.Phoenix.table.
    3. In the :col slot, match on the stream tuple format {{id, item}} using the :let attribute.
    # In LiveView
    def handle_params(params, _, socket) do
      {pets, meta} = Pets.list_pets(params)
      {:noreply, socket |> assign(:meta, meta) |> stream(:pets, pets, reset: true)}
    end
    
    # In HEEx
    <Flop.Phoenix.table items={@streams.pets} meta={@meta} path={~p"/pets"}>
      <:col :let={{_, pet}} label="Name" field={:name}>{pet.name}</:col>
      <:col :let={{_, pet}} label="Age" field={:age}>{pet.age}</:col>
    </Flop.Phoenix.table>
  5. Implement Infinite Scroll using IntersectionObserver and LiveView hooks

    main

    To add infinite scroll to a Flop-powered view, follow these steps:

    1. JavaScript Hook: Create an InfiniteScroll hook using IntersectionObserver. The hook should observe an element identified by a data-anchor-id attribute. When the observer detects intersection, it calls this.pushEvent("load-more", {}).
    2. LiveView Socket: Ensure the Hooks object is passed to your LiveSocket configuration in app.js.
    3. Template Structure: Wrap your table and the 'Load More' link in a div that has phx-hook="InfiniteScroll" and a data-anchor-id matching the ID of your anchor element.
    4. Server-side Event Handling: Implement handle_event("load-more", ...) in your LiveView. This function should check if socket.assigns.meta.end_cursor exists, and if so, use push_patch to update the URL with the after parameter.
    // 1. JavaScript Hook
    Hooks.InfiniteScroll = {
      mounted() {
        observer = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                this.pushEvent("load-more", {});
              }
            });
          },
          { root: null, rootMargin: "0px", threshold: 1.0 }
        );
    
        const anchorId = this.el.dataset.anchorId;
        const anchor = document.getElementById(anchorId);
        if (anchor) observer.observe(anchor);
      },
    };
    
    // 2. LiveSocket setup
    let liveSocket = new LiveSocket("/live", Socket, {
      hooks: Hooks,
    });
    // 4. LiveView handle_event
    def handle_event("load-more", _, socket) do
      if end_cursor = socket.assigns.meta.end_cursor do
        {:noreply, push_patch(socket, to: ~p"/pets?after=#{end_cursor}")}
      else
        {:noreply, socket}
      }
    end
    <%!-- 3. Template --%
    <div id="main-content" phx-hook="InfiniteScroll" data-anchor-id="load-more-anchor">
      <Flop.Phoenix.table ... />
    
      <p :if={@meta.has_next_page?} id="load-more-anchor">
        <.link patch={~p"/pets?after=#{@meta.end_cursor}"}>Load More</.link>
      </p>
    </div>
  6. Implement user-controlled page size in LiveView

    main

    To allow users to change the number of items displayed per page, you can implement a custom component that sends a phx-click event to your LiveView. This event should update the page_size and reset the limit in your Flop metadata, then navigate to the updated path using push_patch.

    1. Create the UI Component

    Define a component (e.g., page_size_links/1) that iterates over a list of desired sizes and highlights the currently active size.

    2. Render the Component

    Pass the current page size from your Flop metadata to the component: <.page_size_links current_size={@meta.page_size} />

    3. Handle the Event

    In your LiveView, catch the set-page-size event, update the flop struct within your metadata, and use Flop.Phoenix.build_path/2 to generate the new URL for patching.

    # 1. Component Definition
    attr :current_size, :integer, required: true
    
    def page_size_links(assigns) do
      ~H"""
      <ul class="page-size-links">
        <li
          :for={page_size <- [10, 20, 40, 60]}
          class={page_size == @current_size && "is-active"}
        >
          <.link phx-click="set-page-size" phx-value-size={page_size}>
            {page_size}
          </.link>
        </li>
      </ul>
      """
    end
    
    # 2. Rendering in Template
    <.page_size_links current_size={@meta.page_size} />
    
    # 3. Event Handler in LiveView
    def handle_event("set-page-size", %{"size" => page_size}, socket) do
      flop = %{socket.assigns.meta.flop | page_size: page_size, limit: nil}
      path = Flop.Phoenix.build_path(~p"/pets", flop)
    
      {:noreply, push_patch(socket, to: path)}
    end
  7. Add visible inputs for meta parameters in filter forms

    main

    If you want to render visible inputs for meta parameters (like page_size) instead of relying on the hidden inputs generated by filter_fields, you can manually add them to your form component. Ensure the name attribute matches the expected parameter name (e.g., name="page_size").

    <.form
      for={@form}
      id={@id}
      phx-target={@target}
      phx-change={@on_change}
      phx-submit={@on_change}
    >
      <%!-- ... --%>
    
      <label for="filter-form-page-size">Page size</label>
      <input
        id="filter-form-page-size"
        type="text"
        name="page_size"
        value={@meta.page_size}
      />
    
      <button name="reset">reset</button>
    </.form>
  8. Change the visual order of pagination elements

    main

    The Flop.Phoenix.pagination component always renders markup in this order:

    1. Previous/Next controls (links or buttons)
    2. A list (<ul>) containing page number links/buttons.

    To change the visual order without changing the HTML structure, use the CSS order property on the flex items within the <nav> element.

    Example: Page numbers first, then Previous/Next

    > a:first-child, 
    > button:first-child {
      order: 2;
    }
    
    > a:last-of-type, 
    > button:last-of-type {
      order: 3;
    }
    
    > ul {
      order: 1;
    }

    Example: Previous, then Page numbers, then Next

    > a:first-child, 
    > button:first-child {
      order: 1;
    }
    
    > a:last-of-type, 
    > button:last-of-type {
      order: 3;
    }
    
    > ul {
      order: 2;
    }
    /* To put the page number links/buttons first and the previous/next links/buttons last */
    > a:first-child,
    > button:first-child {
      order: 2;
    }
    
    > a:last-of-type,
    > button:last-of-type {
      order: 3;
    }
    
    > ul {
      order: 1;
    }
  9. Implement a Load More button with Flop cursor pagination

    main

    To implement a 'Load More' button, use Flop's cursor pagination.

    1. Configure the Schema: Derive Flop.Schema and set default_pagination_type: :first (or another cursor-based type) to ensure a response with cursors is returned even without parameters. Set a default_limit and a sortable column (e.g., id).
    2. Define the List Function: Use Flop.validate_and_run! with default_pagination_type: :first and pagination_types: [:first]. This ensures the function returns cursors.
    3. LiveView Setup: In handle_params, call the list function and assign the meta struct to the socket. Use stream to manage the items.
    4. Template Implementation: Render a link that uses patch to navigate to the base URL with the after parameter set to @meta.end_cursor. Only render this link if @meta.has_next_page? is true.
    # 1. Schema configuration
    @derive {Flop.Schema, 
             filterable: [:name], 
             sortable: [:id], 
             default_order: %{order_by: [:id], order_directions: [:desc]}, 
             default_limit: 50}
    
    # 2. List function
    def list_pets(params) do
      Flop.validate_and_run!(Pet, params,
        for: Pet,
        default_pagination_type: :first,
        pagination_types: [:first],
        replace_invalid_params: true,
        filtering: false,
        ordering: false
      )
    end
    
    # 3. LiveView handle_params
    @impl true
    def handle_params(params, _url, socket) do
      {pets, meta} = Pets.list_pets(params)
      {:noreply, socket |> stream(:pets, pets) |> assign(:meta, meta)}
    end
    <%!-- 4. Template link --%
    <p :if={@meta.has_next_page?}>
      <.link patch={~p"/pets?after=#{@meta.end_cursor}"}>Load More</.link>
    </p>
  10. Integrate Flop with LiveView handle_params

    main

    In your LiveView's handle_params/3 callback, call your context function with the incoming params. Assign both the returned data and the meta struct to the socket.

    defmodule MyAppWeb.PetLive.Index do
      use MyAppWeb, :live_view
    
      alias MyApp.Pets
    
      @impl Phoenix.LiveView
      def handle_params(params, _, socket) do
        {pets, meta} = Pets.list_pets(params)
        {:noreply, assign(socket, pets: pets, meta: meta)}
      end
    end
  11. Configure business logic with Flop

    main

    In your context module, define a function that performs a list query using Flop.validate_and_run!/3. It is recommended to use the replace_invalid_params: true option to allow Flop to ignore invalid parameters instead of raising an error.

    defmodule MyApp.Pets do
      alias MyApp.Pet
    
      def list_pets(params) do
        Flop.validate_and_run!(Pet, params, for: Pet, replace_invalid_params: true)
      end
    end