Skip to main content

priv/registry/data_table.json

{
  "files": [
    {
      "content": "defmodule Shadix.Components.DataTable do\n  @moduledoc \"\"\"\n  A styling and sorting **recipe** layered over `Shadix.Components.Table`.\n\n  The shadcn \"Data Table\" example is not a standalone component: it is a guide\n  for wiring [TanStack Table](https://tanstack.com/table) (column defs, sorting,\n  filtering, pagination, row selection, visibility) into the presentational\n  `Table` primitives. This v1 is deliberately **not** a TanStack port. It is a\n  thin set of helpers that render the *chrome* — a bordered container, sortable\n  column-header buttons, and an optional pagination bar — on top of the existing\n  `Table` components.\n\n  ## Scope and simplifications\n\n    * **The consumer owns the data and all state.** Your LiveView holds the rows,\n      the current sort field/direction, and the current page; it does the actual\n      sorting and slicing. These helpers only render the surface. There is **no**\n      LiveView hook and no client-side state.\n    * **You build the table yourself.** `data_table/1` is just a bordered wrapper:\n      put a `<Shadix.Components.Table.table>` (with its own header/body) inside it.\n      The recipe does not generate rows from column definitions.\n    * **Sorting is \"controlled\".** `data_table_column_header/1` renders a button\n      that shows the current sort direction for that column (via `:sort`,\n      one of `\"asc\"`, `\"desc\"`, or `nil`) and runs the `:on_sort` JS you pass.\n      Your handler decides the next state and re-renders with an updated `:sort`.\n    * **Pagination is \"controlled\".** `data_table_pagination/1` renders prev/next\n      buttons and a \"Page X of Y\" label. It enforces nothing; your `:on_prev` /\n      `:on_next` handlers update the page and you pass `:page` / `:total_pages`\n      back in. Buttons can be disabled via `:prev_disabled` / `:next_disabled`.\n\n  ## data-slots\n\n    * `data-table` — the bordered container `<div>`.\n    * `data-table-column-header` — the sortable header `<button>`.\n    * `data-table-pagination` — the pagination bar `<div>` (a labelled `navigation` landmark).\n\n  ## Accessibility\n\n  The sortable header is a plain `<button>` and intentionally does **not** carry\n  `aria-sort` — that attribute is not allowed on `<button>`; it belongs on the\n  `columnheader` (the `<th>`). If you want to expose the sort state to assistive\n  technology via `aria-sort`, set it on the consuming\n  `<Shadix.Components.Table.table_head>` (e.g. `aria-sort=\"ascending\"`). The\n  button instead conveys the sort control through an `aria-label`.\n\n  ## Example\n\n      <Shadix.Components.DataTable.data_table>\n        <Shadix.Components.Table.table>\n          <Shadix.Components.Table.table_header>\n            <Shadix.Components.Table.table_row>\n              <Shadix.Components.Table.table_head>\n                <Shadix.Components.DataTable.data_table_column_header\n                  label=\"Name\"\n                  sort={@sort_dir_for_name}\n                  on_sort={JS.push(\"sort\", value: %{field: \"name\"})}\n                />\n              </Shadix.Components.Table.table_head>\n            </Shadix.Components.Table.table_row>\n          </Shadix.Components.Table.table_header>\n          <Shadix.Components.Table.table_body>\n            <Shadix.Components.Table.table_row :for={row <- @rows}>\n              <Shadix.Components.Table.table_cell>{row.name}</Shadix.Components.Table.table_cell>\n            </Shadix.Components.Table.table_row>\n          </Shadix.Components.Table.table_body>\n        </Shadix.Components.Table.table>\n      </Shadix.Components.DataTable.data_table>\n\n      <Shadix.Components.DataTable.data_table_pagination\n        page={@page}\n        total_pages={@total_pages}\n        prev_disabled={@page <= 1}\n        next_disabled={@page >= @total_pages}\n        on_prev={JS.push(\"prev_page\")}\n        on_next={JS.push(\"next_page\")}\n      />\n  \"\"\"\n  use Phoenix.Component\n\n  alias Phoenix.LiveView.JS\n\n  import Shadix.Cn\n\n  @doc \"\"\"\n  A bordered container for a data table.\n\n  Renders a `rounded-md border` wrapper carrying `data-slot=\"data-table\"`. Place a\n  `<Shadix.Components.Table.table>` inside via the inner block; the table's own\n  scroll container nests cleanly within the rounded border.\n  \"\"\"\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def data_table(assigns) do\n    ~H\"\"\"\n    <div\n      data-slot=\"data-table\"\n      class={cn([\"overflow-hidden rounded-md border\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  A sortable column header button for use inside a `table_head`.\n\n  Renders a `<button data-slot=\"data-table-column-header\">` showing `:label` and a\n  chevron reflecting `:sort`:\n\n    * `\"asc\"` — up chevron\n    * `\"desc\"` — down chevron\n    * `nil` (default) — neutral up/down chevrons\n\n  Clicking runs the caller's `:on_sort` JS. The component is \"controlled\": it\n  renders the sort state you give it and does not toggle anything itself — your\n  handler computes the next direction and passes back an updated `:sort`.\n\n  The button carries an `aria-label` describing the sort control (and the current\n  direction). `aria-sort` is **not** placed on the button — it is not a valid\n  attribute there; set it on the consuming `<th>`/`table_head` instead.\n  \"\"\"\n  attr(:label, :string, required: true)\n\n  attr(:sort, :string,\n    default: nil,\n    values: [\"asc\", \"desc\", nil],\n    doc: ~s(Current sort direction for this column: \"asc\", \"desc\", or nil.)\n  )\n\n  attr(:on_sort, JS, default: %JS{}, doc: \"JS run when the header is clicked.\")\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n\n  def data_table_column_header(assigns) do\n    ~H\"\"\"\n    <button\n      type=\"button\"\n      data-slot=\"data-table-column-header\"\n      data-sort={@sort}\n      aria-label={\n        case @sort do\n          \"asc\" -> \"Sort by \" <> @label <> \", current sort ascending\"\n          \"desc\" -> \"Sort by \" <> @label <> \", current sort descending\"\n          _ -> \"Sort by \" <> @label\n        end\n      }\n      phx-click={@on_sort}\n      class={\n        cn([\n          \"-ml-3 inline-flex h-8 select-none items-center gap-2 rounded-md px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[sort]:text-accent-foreground\",\n          @class\n        ])\n      }\n      {@rest}\n    >\n      {@label}\n      <.sort_chevron sort={@sort} />\n    </button>\n    \"\"\"\n  end\n\n  # Up chevron for \"asc\", down chevron for \"desc\", neutral up/down for unsorted.\n  attr(:sort, :string, default: nil)\n\n  defp sort_chevron(%{sort: \"asc\"} = assigns) do\n    ~H\"\"\"\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class=\"size-4\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m18 15-6-6-6 6\" />\n    </svg>\n    \"\"\"\n  end\n\n  defp sort_chevron(%{sort: \"desc\"} = assigns) do\n    ~H\"\"\"\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class=\"size-4\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>\n    \"\"\"\n  end\n\n  defp sort_chevron(assigns) do\n    ~H\"\"\"\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      stroke-width=\"2\"\n      stroke-linecap=\"round\"\n      stroke-linejoin=\"round\"\n      class=\"size-4 text-muted-foreground\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m7 15 5 5 5-5\" />\n      <path d=\"m7 9 5-5 5 5\" />\n    </svg>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  An optional pagination bar with prev/next buttons and a \"Page X of Y\" label.\n\n  Purely presentational and \"controlled\": it renders `:page` / `:total_pages` and\n  runs `:on_prev` / `:on_next`. Disable the edges with `:prev_disabled` /\n  `:next_disabled`; your handlers update the page state you pass back in.\n  \"\"\"\n  attr(:page, :integer, default: 1)\n  attr(:total_pages, :integer, default: 1)\n  attr(:prev_disabled, :boolean, default: false)\n  attr(:next_disabled, :boolean, default: false)\n  attr(:on_prev, JS, default: %JS{}, doc: \"JS run when the previous-page button is clicked.\")\n  attr(:on_next, JS, default: %JS{}, doc: \"JS run when the next-page button is clicked.\")\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n\n  def data_table_pagination(assigns) do\n    ~H\"\"\"\n    <div\n      data-slot=\"data-table-pagination\"\n      role=\"navigation\"\n      aria-label=\"Pagination\"\n      class={cn([\"flex items-center justify-end gap-2 py-4\", @class])}\n      {@rest}\n    >\n      <div\n        data-slot=\"data-table-pagination-info\"\n        aria-live=\"polite\"\n        class=\"text-sm text-muted-foreground\"\n      >\n        Page {@page} of {@total_pages}\n      </div>\n      <button\n        type=\"button\"\n        data-slot=\"data-table-pagination-prev\"\n        disabled={@prev_disabled}\n        phx-click={@on_prev}\n        class=\"inline-flex h-8 select-none items-center justify-center rounded-md border bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\"\n      >\n        Previous\n      </button>\n      <button\n        type=\"button\"\n        data-slot=\"data-table-pagination-next\"\n        disabled={@next_disabled}\n        phx-click={@on_next}\n        class=\"inline-flex h-8 select-none items-center justify-center rounded-md border bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\"\n      >\n        Next\n      </button>\n    </div>\n    \"\"\"\n  end\nend\n",
      "path": "data_table.ex"
    }
  ],
  "hooks": [],
  "name": "data_table",
  "npm_deps": [],
  "registry_deps": [
    "cn"
  ]
}