README.md

# Live Table

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