defmodule LiveTable do @moduledoc """ LiveTable is a powerful Phoenix LiveView component library that provides dynamic, interactive tables with built-in support for sorting, filtering, pagination, and data export capabilities. ## Features - **Advanced Filtering System** - Text search across multiple fields - Range filters for numbers, dates, and datetimes - Boolean filters with custom conditions - Select filters with static and dynamic options - Multi-column filtering support - **Smart Sorting** - Multi-column sorting - Sortable associated fields - Customizable sort directions - Shift-click support for multi-column sorting - **Flexible Pagination** - Configurable page sizes - Dynamic page navigation - Efficient database querying - **Export Capabilities** - CSV export with background processing - PDF export using Typst - Custom file naming and formatting - Progress tracking for large exports - **Real-time Updates** - LiveView integration - Instant filter feedback - Background job status updates ## Installation Add `live_table` to your list of dependencies in `mix.exs`: ```elixir {:live_table, "~> 0.1.0"} ``` ## Configuration Configure LiveTable in your `config.exs`: ```elixir config :live_table, repo: YourApp.Repo, pubsub: YourApp.PubSub, components: YourApp.Components # Optional, defaults to LiveTable.Components ``` ### JavaScript Setup Add the following to your `assets/js/app.js`: ```js import { TableHooks } from "./hooks/hooks" let liveSocket = new LiveSocket("/live", Socket, { params: {_csrf_token: csrfToken}, hooks: TableHooks }) ``` ## Basic Usage In your LiveView add the line `use LiveTable.LiveResource`: Define your fields and filters as required. ```elixir #/app_web/live/user_live/index.ex defmodule MyAppWeb.UserLive.Index do use MyAppWeb, :live_view use LiveTable.LiveResource, resource: "users", schema: User # Add this line # Define fields def fields do [ id: %{label: "ID", sortable: true}, name: %{label: "Name", sortable: true, searchable: true}, email: %{label: "Email", sortable: true, searchable: true}, # Include fields from associations supplier: %{ label: "Supplier", sortable: true, searchable: true, assoc: {:supplier, :name} } ] end # Define filters def filters do [ # Boolean filter active: Boolean.new(:active, "active", %{ label: "Active Users", condition: dynamic([q], q.active == true) }), # Range filter age: Range.new(:age, "age", %{ type: :number, label: "Age Range", min: 0, max: 100 }), # Select filter with dynamic options supplier: Select.new({:suppliers, :name}, "supplier", %{ label: "Supplier", options_source: {YourApp.Suppliers, :search_suppliers, []} }) ] end ``` ```elixir #/app_web/live/user_live/index.html.heex # in your view: <.live_table fields={fields()} filters={filters()} options={@options} streams={@streams} /> ``` ## Filter Types ### Boolean Filter ```elixir Boolean.new(:active, "active_filter", %{ label: "Show Active Only", condition: dynamic([p], p.active == true) }) ``` ### Range Filter ```elixir Range.new(:price, "price_range", %{ type: :number, label: "Price Range", min: 0, max: 1000, step: 10 }) ``` ### Select Filter ```elixir Select.new(:category, "category_select", %{ label: "Category", options: [ %{label: "Electronics", value: [1, "Electronics"]}, %{label: "Books", value: [2, "Books"]} ] }) ``` ## Defining your fields ### Normal Fields The fields you want should be defined under the fields() function. This function needs to be passed to the live_table component in the template. A basic guide to defining fields is as follows: Define them as a keyword list, with the key being the name of the field (which will appear in the url and be used to reference the field) and a map of options which will contain more data about the field. For eg, ```elixir def fields() do [ id: %{label: "ID", sortable: true}, name: %{label: "Name", sortable: true, searchable: true}, email: %{label: "Email", sortable: true, searchable: true}, ] end ``` The map contains the label, and config for sort and search. The label will be the column header in the table and the exported CSV/PDF. **Only fields with `sortable: true` will have a sortable link generated as the column header**. **All fields with `searchable: true` will be searched from the search bar using the `ILIKE` operator**. ### Associated Fields For associated fields, you can use the `assoc` key to specify the association, with a tuple containing the table name and the field. For eg, ```elixir def fields() do [ id: %{label: "ID", sortable: true}, supplier: %{ label: "Supplier", sortable: true, searchable: true, assoc: {:supplier, :name} }, supplier_description: %{ label: "Supplier Email", assoc: {:suppliers, :contact_info}, searchable: false, sortable: true }, category_name: %{ label: "Category Name", assoc: {:category, :name}, searchable: false, sortable: false }, image: %{ label: "Image", sortable: false, searchable: false, assoc: {:image, :url} }, ] end ``` Be it any type of association, you can join using the `assoc` key. ### Computed Fields You can also define computed fields, which are fields that are not present in the database but are computed using a function. This is useful in cases like calculating the total price of a product based on the quantity and price. Such fields require a `computed:` key, which should get a dynamic query expression. Since it is a dynamic query, you can use it to alias associated fields and use them inside the fragment. For eg, ```elixir def fields() do [ amount: %{ label: "Amount", sortable: true, searchable: false, computed: dynamic([resource: r, suppliers: s, categories: c], fragment("? * ?", r.price, r.stock_quantity)) } ] end ``` If the field has not already been joined by a previous field, you can join it in the computed field itself. For eg, ```elixir def fields() do [ amount: %{ label: "Amount", sortable: true, searchable: false, assoc: {:image, :url} computed: dynamic([resource: r, images: i], fragment("? * ?", r.price, r.stock_quantity)), } ] end ``` ## Defining your filters Your filters should be defined under the filters() function. This function needs to be passed to the live_table component in the template. A basic guide to defining them is as follows: Define them as a keyword list, with the key being the name of the filter (which will appear in the url and be used to reference the filter) and a map of options which will contain more info about the filter. Each filter is defined by a struct of the corresponding filter type. The struct should be created using the new() function of the filter type. The struct takes 3 arguments, the field the filter should act on, a key referencing the filter(to be used in the url), and a map of options which will contain more info about the filter. As a general rule of thumb, the field should be the name of the field as an atom(in case of a normal field) or a tuple containing the table name and the field name(in case of an associated field). A detailed guide for defining each type of filter has been provided in its corresponding module. ## License MIT License. See LICENSE file for details. ## Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request """ end