{"id":13490978,"url":"https://github.com/woylie/flop_phoenix","last_synced_at":"2025-05-14T09:13:22.417Z","repository":{"id":39639937,"uuid":"272301904","full_name":"woylie/flop_phoenix","owner":"woylie","description":"Components for pagination, sortable tables and filter forms using Phoenix, Ecto and Flop","archived":false,"fork":false,"pushed_at":"2025-05-09T22:53:46.000Z","size":1509,"stargazers_count":462,"open_issues_count":13,"forks_count":47,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-05-09T23:29:11.795Z","etag":null,"topics":["ecto","elixir","filter-form","pagination","phoenix-framework","phoenix-liveview","sortable-table"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/woylie.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"woylie","liberapay":"woylie"}},"created_at":"2020-06-14T23:41:28.000Z","updated_at":"2025-05-09T22:53:49.000Z","dependencies_parsed_at":"2024-01-06T05:22:40.394Z","dependency_job_id":"1502bd74-903d-462b-95c5-3d07a2061882","html_url":"https://github.com/woylie/flop_phoenix","commit_stats":{"total_commits":703,"total_committers":22,"mean_commits":"31.954545454545453","dds":"0.25035561877667145","last_synced_commit":"02af703fe3bb91c5d25c41b1b705fd3edd1c8a5e"},"previous_names":[],"tags_count":54,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woylie%2Fflop_phoenix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woylie%2Fflop_phoenix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woylie%2Fflop_phoenix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woylie%2Fflop_phoenix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/woylie","download_url":"https://codeload.github.com/woylie/flop_phoenix/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254110377,"owners_count":22016391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","elixir","filter-form","pagination","phoenix-framework","phoenix-liveview","sortable-table"],"created_at":"2024-07-31T19:00:52.553Z","updated_at":"2025-05-14T09:13:22.410Z","avatar_url":"https://github.com/woylie.png","language":"Elixir","funding_links":["https://github.com/sponsors/woylie","https://liberapay.com/woylie"],"categories":["Elixir"],"sub_categories":[],"readme":"# Flop Phoenix\n\n![CI](https://github.com/woylie/flop_phoenix/workflows/CI/badge.svg) [![Hex](https://img.shields.io/hexpm/v/flop_phoenix)](https://hex.pm/packages/flop_phoenix) [![Coverage Status](https://coveralls.io/repos/github/woylie/flop_phoenix/badge.svg)](https://coveralls.io/github/woylie/flop_phoenix)\n\nFlop Phoenix provides Phoenix components for pagination, sortable tables, and\nfilter forms with [Flop](https://hex.pm/packages/flop) and\n[Ecto](https://hex.pm/packages/ecto).\n\n## Installation\n\nAdd `flop_phoenix` to your list of dependencies in the `mix.exs` of your Phoenix\napplication.\n\n```elixir\ndef deps do\n  [\n    {:flop_phoenix, \"~\u003e 0.25.1\"}\n  ]\nend\n```\n\nNext, set up your business logic following the\n[Flop documentation](https://hex.pm/packages/flop).\n\n## Context\n\nDefine a context function that performs a list query using Flop.\n\n```elixir\ndefmodule MyApp.Pets do\n  alias MyApp.Pet\n\n  def list_pets(params) do\n    Flop.validate_and_run!(Pet, params, for: Pet, replace_invalid_params: true)\n  end\nend\n```\n\nNote the usage of the `replace_invalid_params` option, which lets Flop ignore\ninvalid parameters instead of producing an error.\n\n## LiveView\n\nIn the `handle_params` function of your LiveView module, pass the parameters\nto the list function to fetch the data and assign both the data and the meta\nstruct to the socket.\n\n```elixir\ndefmodule MyAppWeb.PetLive.Index do\n  use MyAppWeb, :live_view\n\n  alias MyApp.Pets\n\n  @impl Phoenix.LiveView\n  def handle_params(params, _, socket) do\n    {pets, meta} = Pets.list_pets(params)\n    {:noreply, assign(socket, pets: pets, meta: meta)}\n  end\nend\n```\n\n## Sortable table and pagination\n\nAdd a sortable table and pagination to your HEEx template:\n\n```heex\n\u003cFlop.Phoenix.table items={@pets} meta={@meta} path={~p\"/pets\"}\u003e\n  \u003c:col :let={pet} label=\"Name\" field={:name}\u003e{pet.name}\u003c/:col\u003e\n  \u003c:col :let={pet} label=\"Age\" field={:age}\u003e{pet.age}\u003c/:col\u003e\n\u003c/Flop.Phoenix.table\u003e\n\n\u003cFlop.Phoenix.pagination meta={@meta} path={~p\"/pets\"} /\u003e\n```\n\nThe `path` attribute points to the current route, and Flop Phoenix appends\npagination, filtering, and sorting parameters to it. You can use verified\nroutes, route helpers, or custom path builder functions. For a\ndescription of the different formats, refer to the documentation of\n`Flop.Phoenix.build_path/3`.\n\nThe `field` attribute in the `:col` slot is optional. If set and the field\nis defined as sortable in the Ecto schema, the table header for\nthat column will be interactive, allowing users to sort by that column. However,\nif the field isn't defined as sortable, or if the field attribute is omitted, or\nset to `nil` or `false`, the table header will not be clickable.\n\nBy using the `for` option in your Flop query, Flop Phoenix can identify which\ntable columns are sortable. Additionally, it omits the `order` and `page_size`\nparameters if they match the default values defined with `Flop.Schema`.\n\nYou also have the option to pass a `Phoenix.LiveView.JS` command instead of or\nin addition to a path. For more details, please refer to the component\ndocumentation.\n\nThe pagination component can be used for both page-based pagination and\ncursor-based pagination. It chooses the pagination type based on the information\nfrom the `Flop.Meta` struct.\n\n## Event-based pagination and sorting\n\nIn the example above, the pagination, sorting, and filtering parameters are\nappended to the URL as query parameters. Most of the time, this provides a\nbetter user experience, since users are able to bookmark or share a URL that\nleads to the exact view.\n\nIn some cases, though, you may prefer not to handle the parameters via the URL.\nFor example, you may have multiple pageable areas in a single view, or a\npageable widget that is not part of the main content.\n\nIn that case, you can set the `on_paginate` and `on_sort` attributes instead of\nthe `path` attribute and handle these events with the `handle_event` callback.\n\nRefer to the \"Using JS commands\" section in the `Flop.Phoenix` module\ndocumentation for an example.\n\n## Filter forms\n\nFlop Phoenix implements the `Phoenix.HTML.FormData` for the `Flop.Meta` struct.\nAs such, you can easily pass the struct to Phoenix form functions. One\nstraightforward way to render a filter form is through the\n`Flop.Phoenix.filter_fields/1` component, as shown below:\n\n```elixir\nattr :fields, :list, required: true\nattr :meta, Flop.Meta, required: true\nattr :id, :string, default: nil\nattr :on_change, :string, default: \"update-filter\"\nattr :target, :string, default: nil\n\ndef filter_form(%{meta: meta} = assigns) do\n  assigns = assign(assigns, form: Phoenix.Component.to_form(meta), meta: nil)\n\n  ~H\"\"\"\n  \u003c.form\n    for={@form}\n    id={@id}\n    phx-target={@target}\n    phx-change={@on_change}\n    phx-submit={@on_change}\n  \u003e\n    \u003c.filter_fields :let={i} form={@form} fields={@fields}\u003e\n      \u003c.input\n        field={i.field}\n        label={i.label}\n        type={i.type}\n        phx-debounce={120}\n        {i.rest}\n      /\u003e\n    \u003c/.filter_fields\u003e\n\n    \u003cbutton name=\"reset\"\u003ereset\u003c/button\u003e\n  \u003c/.form\u003e\n  \"\"\"\nend\n```\n\nNow you can render a filter form like this:\n\n```heex\n\u003c.filter_form\n  fields={[:name, :email]}\n  meta={@meta}\n  id=\"user-filter-form\"\n/\u003e\n```\n\nYou will need to handle the `update-filter` event with the `handle_event/3`\ncallback function of your LiveView.\n\n```elixir\n@impl true\ndef handle_event(\"update-filter\", params, socket) do\n  params = Map.delete(params, \"_target\")\n  {:noreply, push_patch(socket, to: ~p\"/pets?#{params}\")}\nend\n```\n\nNote that while the `filter_fields` component produces all necessary hidden\ninputs, it doesn't automatically render inputs for filter values. Instead, it\npasses the necessary details to the inner block, allowing you to customize the\nfilter inputs with your own input component.\n\nYou can pass additional options for each field. Refer to the\n`Flop.Phoenix.filter_fields/1` documentation for details.\n\n### Adding visible inputs for meta parameters\n\nIf you want to render visible inputs instead of relying on the hidden inputs\nthat are automatically added to the form, you can just add them to the form\ncomponent:\n\n```heex\n\u003c.form\n  for={@form}\n  id={@id}\n  phx-target={@target}\n  phx-change={@on_change}\n  phx-submit={@on_change}\n\u003e\n  \u003c%!-- ... --%\u003e\n\n  \u003clabel for=\"filter-form-page-size\"\u003ePage size\u003c/label\u003e\n  \u003cinput\n    id=\"filter-form-page-size\"\n    type=\"text\"\n    name=\"page_size\"\n    value={@meta.page_size}\n  /\u003e\n\n  \u003cbutton name=\"reset\"\u003ereset\u003c/button\u003e\n\u003c/.form\u003e\n```\n\n`Phoenix.LiveView.JS` command as an attribute to the components in that case.\n\n## LiveView streams\n\nTo use LiveView streams, you can change your `handle_params/3` function as\nfollows:\n\n```elixir\ndef handle_params(params, _, socket) do\n  {pets, meta} = Pets.list_pets(params)\n  {:noreply, socket |\u003e assign(:meta, meta) |\u003e stream(:pets, pets, reset: true)}\nend\n```\n\nWhen using LiveView streams, you need to pass `@streams.pets` instead of `@pets`\nto the table component.\n\nThe stream values are tuples, with the DOM ID as the first element and the items\n(in this case, Pets) as the second element. You need to match on these tuples\nwithin the `:let` attributes of the table component.\n\n```heex\n\u003cFlop.Phoenix.table items={@streams.pets} meta={@meta} path={~p\"/pets\"}\u003e\n  \u003c:col :let={{_, pet}} label=\"Name\" field={:name}\u003e{pet.name}\u003c/:col\u003e\n  \u003c:col :let={{_, pet}} label=\"Age\" field={:age}\u003e{pet.age}\u003c/:col\u003e\n\u003c/Flop.Phoenix.table\u003e\n```\n\n## Customization\n\nFor customizing the components, it is recommend to define wrapper components\nthat set the necessary attributes. Refer to the\n[module documentation](https://hexdocs.pm/flop_phoenix/Flop.Phoenix.html#module-customization) for examples.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoylie%2Fflop_phoenix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwoylie%2Fflop_phoenix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoylie%2Fflop_phoenix/lists"}