An open API service indexing awesome lists of open source software.

https://github.com/cpursley/livefilter

A flexible and composable filtering library for Phoenix LiveView applications
https://github.com/cpursley/livefilter

admin-dashboard elixir liveview

Last synced: 2 months ago
JSON representation

A flexible and composable filtering library for Phoenix LiveView applications

Awesome Lists containing this project

README

          

# LiveFilter

A flexible and composable filtering library for Phoenix LiveView applications, inspired by Linear, Notion and Airtable. Provides filtering, sorting, and pagination with clean URL state management and a modern UI built on [SaladUI](https://salad-storybook.fly.dev/) and inspired by [shadcn/ui data tables](https://tablecn.com/).

[![Hex.pm](https://img.shields.io/hexpm/v/livefilter.svg)](https://hex.pm/packages/livefilter)
[![Documentation](https://img.shields.io/badge/documentation-hexdocs-blue.svg)](https://hexdocs.pm/livefilter)

**Demo**: [https://livefilter.fly.dev](https://livefilter.fly.dev/) - source: [https://github.com/cpursley/livefilter-demo](https://github.com/cpursley/livefilter-demo)

## Features

- Filter operators for all data types (string, numeric, boolean, date, enum, array)
- URL state management with shareable filter links
- Autogeneration of database-efficient queries
- Sorting and column management
- Components built on shadcn-inspired [SaladUI](https://salad-storybook.fly.dev/)

## Installation

Add to your `mix.exs`:

```elixir
def deps do
[
{:livefilter, "~> 0.1.7"}
]
end
```

Install JavaScript assets:

```bash
mix live_filter.install.assets
```

Add to your `app.js`:

```javascript
import LiveFilter from "./hooks/live_filter/live_filter"

let liveSocket = new LiveSocket("/live", Socket, {
hooks: { LiveFilter }
})
```

## Basic Usage

### 1. Minimal Setup

```elixir
defmodule MyAppWeb.ProductLive do
use MyAppWeb, :live_view
alias LiveFilter.{Filter, FilterGroup, QueryBuilder}

def mount(_params, _session, socket) do
{:ok, assign(socket, :products, load_products())}
end

defp load_products(filter_group \\ %FilterGroup{}) do
from(p in Product)
|> QueryBuilder.build_query(filter_group)
|> Repo.all()
end
end
```

### 2. URL-Persisted Filters

```elixir
defmodule MyAppWeb.ProductLive do
use MyAppWeb, :live_view
use LiveFilter.Mountable # Adds filter helpers

def mount(_params, _session, socket) do
socket = mount_filters(socket)
{:ok, assign(socket, :products, [])}
end

def handle_params(params, _url, socket) do
socket = handle_filter_params(socket, params)
{:noreply, assign(socket, :products, load_products(socket.assigns.filter_group))}
end

defp load_products(filter_group) do
from(p in Product)
|> QueryBuilder.build_query(filter_group)
|> Repo.all()
end
end
```

## Advanced Example

Here's a complete implementation with UI components (similar to our [demo app](https://livefilter.fly.dev/)):

```elixir
defmodule MyAppWeb.TodoLive do
use MyAppWeb, :live_view
use LiveFilter.Mountable

def mount(_params, _session, socket) do
socket =
socket
|> mount_filters(registry: field_registry())
|> assign(:todos, [])

{:ok, socket}
end

def handle_params(params, _url, socket) do
socket = handle_filter_params(socket, params)
{:noreply, assign(socket, :todos, load_todos(socket.assigns.filter_group))}
end

def render(assigns) do
~H"""








<.live_component
module={LiveFilter.Components.SearchSelect}
id="status-filter"
field={:status}
value={get_filter_value(@filter_group, :status)}
options={[
{"pending", "Pending"},
{"in_progress", "In Progress"},
{"completed", "Completed"}
]}
/>


<.live_component
module={LiveFilter.Components.FilterBuilder}
id="filter-builder"
filter_group={@filter_group}
field_options={field_options()}
/>





<.live_component
module={LiveFilter.Components.SortableHeader}
id="sort-title"
field={:title}
label="Title"
current_sort={@current_sort}
/>
Status
Due Date




<%= todo.title %>
<%= todo.status %>
<%= todo.due_date %>




"""
end

def handle_event("search", %{"q" => query}, socket) do
filter = %Filter{field: :title, operator: :contains, value: query, type: :string}
filter_group = %FilterGroup{filters: [filter]}
socket = apply_filters_and_reload(socket, filter_group)
{:noreply, assign(socket, :search, query)}
end

defp field_registry do
LiveFilter.FieldRegistry.new()
|> LiveFilter.FieldRegistry.put(:title, :string, "Title")
|> LiveFilter.FieldRegistry.put(:status, :enum, "Status",
options: [{"pending", "Pending"}, {"in_progress", "In Progress"}, {"completed", "Completed"}])
|> LiveFilter.FieldRegistry.put(:due_date, :date, "Due Date")
|> LiveFilter.FieldRegistry.put(:tags, :array, "Tags")
end

defp load_todos(filter_group) do
from(t in Todo)
|> QueryBuilder.build_query(filter_group)
|> QueryBuilder.apply_sort(socket.assigns.current_sort)
|> Repo.all()
end
end
```

## Documentation

- [API Documentation](https://hexdocs.pm/livefilter)
- [Demo Application](https://github.com/cpursley/livefilter-demo)

## License

MIT