Live Table
View Source 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.
Makes use of Oban
, NimbleCSV
and Typst
to handle exports.
You can find a table with 1 Million rows here
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
:
def deps do
[
{:live_table, "~> 0.2.0"}
]
Configuration
Configure LiveTable in your config/config.exs
:
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
:
import { TableHooks } from "../../deps/live_table/priv/static/live-table.js"
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: TableHooks
})
CSS Setup
Add the following to your assets/tailwind.config.js
:
content: [
// Other paths
"../deps/live_table/priv/static/*.js",
"../deps/live_table/**/*.*ex"
]
And add the following to your assets/css/app.css
:
@import "../../deps/live_table/priv/static/live-table.css";
Oban
Configure your Oban instance and queues in your config/config.exs
:
# config/config.exs
config :live_table, Oban,
repo: YourApp.Repo,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
plugins: [Oban.Plugins.Pruner],
queues: [exports: 10]
# the queue named `exports` will be used for export jobs
Oban Web: Optional
You can configure oban web in your router to monitor the background jobs.
# lib/your_app_web/router.ex
import Oban.Web.Router
scope "/", YouAppWeb do
# your other routes
oban_dashboard("/oban")
end
Note: Remember to add exports to your list of allowed
static_paths
inlib/app_web.ex
def static_paths, do: ~w(assets fonts images favicon.ico exports robots.txt)
Basic Usage
In your LiveView add the line use LiveTable.LiveResource
:
Define your fields and filters as required.
#/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
#/app_web/live/user_live/index.html.heex
# in your view:
<.live_table
fields={fields()}
filters={filters()}
options={@options}
streams={@streams}
/>
Note: Using
shift
+ click on a column header will sort by multiple columns.
Filter Types
Boolean Filter
Boolean.new(:active, "active_filter", %{
label: "Show Active Only",
condition: dynamic([p], p.active == true)
})
Range Filter
Range.new(:price, "price_range", %{
type: :number,
label: "Price Range",
min: 0,
max: 1000,
step: 10
})
Select Filter
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,
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,
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,
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,
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
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request