LiveView Integration
Aurora UIX leverages Phoenix LiveView to provide dynamic, real-time CRUD interfaces. The framework generates fully-functional LiveView modules at compile time, handling all the boilerplate while remaining customizable.
How It Works
Module Generation with auix_create_ui
The auix_create_ui/0 macro generates a complete set of LiveView modules for your resource. For a resource named product, it creates:
Overview.Product # Parent module (generated)
├── Overview.Product.Index # Handles list/index, create, update, delete operations
└── Overview.Product.Show # Handles detail view and show-specific operationsGenerated Module Structure
Each generated module inherits from a handler implementation that provides:
- Lifecycle callbacks -
mount/3,handle_params/3 - Event handling -
handle_event/3for form submissions and CRUD operations - Async support -
handle_info/2,handle_async/3for background operations
Index Module (Overview.Product.Index):
- Lists entities with streaming for performance
- Handles create operations (new form submission)
- Handles update operations (inline or form edits)
- Handles delete operations with confirmation
- Manages filtering, sorting, and pagination
- Provides navigation between list and detail views
Show Module (Overview.Product.Show):
- Displays entity details
- Handles show-specific edits (if enabled in layout)
- Manages section/tab switching
- Provides navigation (back, forward through routes)
Using Generated Modules with Routes
The auix_live_resources/3 macro creates all necessary routes:
import Aurora.Uix.RouteHelper
scope "/products" do
pipe_through(:browser)
auix_live_resources("/", Overview.Product)
end
# Expands to:
live "/", Overview.Product.Index, :index
live "/new", Overview.Product.Index, :new
live "/:id/edit", Overview.Product.Index, :edit
live "/:id", Overview.Product.Show, :show
live "/:id/show/edit", Overview.Product.Show, :editYou can also selectively generate routes:
# Read-only interface (no create/update)
auix_live_resources("/", Overview.Product, only: [:index, :show])
# Hide delete capability
auix_live_resources("/", Overview.Product, except: [:delete])Customizing Behavior
Handler Hooks
Aurora UIX uses a handler delegation pattern. The generated LiveView modules delegate to handler modules (called "handler hooks") that implement the actual logic. You can customize specific behaviors by providing your own handler modules.
Handler hooks are specified directly in the layout DSL as options:
handler_module- For index columns (handles list operations)edit_handler_module- For edit layout (handles form/edit operations)show_handler_module- For show layout (handles detail view operations)
Index Handler Hook
defmodule MyApp.ProductIndexHandler do
use Aurora.Uix.Templates.Basic.Handlers.IndexImpl
alias Aurora.Uix.Templates.Basic.Handlers.IndexImpl
alias Phoenix.LiveView.Socket
# Override mount to customize data loading
@impl Phoenix.LiveView
def mount(params, session, %{assigns: %{auix: auix}} = socket) do
# Apply custom query options (e.g., filtering)
new_socket =
auix.layout_tree
|> Map.get(:opts, [])
|> Keyword.put(:where, {:status, :eq, "active"})
|> then(&Map.put(auix.layout_tree, :opts, &1))
|> then(&assign_auix(socket, :layout_tree, &1))
super(params, session, new_socket)
end
# Override apply_action for custom behavior on route changes
@impl IndexImpl
def apply_action(socket, params) do
super(socket, params)
end
endForm/Edit Handler Hook
defmodule MyApp.ProductEditHandler do
use Aurora.Uix.Templates.Basic.Handlers.FormImpl
alias Aurora.Uix.Templates.Basic.Handlers.FormImpl
alias Phoenix.LiveView.Socket
# Override save_entity to customize save logic
@impl FormImpl
def save_entity(%{assigns: %{action: :edit, auix: auix}}, _entity_params) do
# Example: Skip saving on edit, just return the existing entity
{:ok, auix.entity}
end
def save_entity(socket, entity_params) do
# Use default implementation for create
super(socket, entity_params)
end
endShow Handler Hook
defmodule MyApp.ProductShowHandler do
use Aurora.Uix.Templates.Basic.Handlers.ShowImpl
import Phoenix.LiveView, only: [push_patch: 2, put_flash: 3]
alias Phoenix.LiveView.Socket
# Override handle_event for custom event handling
@impl Phoenix.LiveView
def handle_event("delete", _params, socket) do
# Custom delete logic
{:noreply,
socket
|> put_flash(:info, "Product archived instead of deleted")
|> push_patch(to: socket.assigns.auix[:_current_path])}
end
def handle_event(event, params, socket) do
super(event, params, socket)
end
endSpecifying Handler Hooks in Layout DSL
defmodule MyAppWeb.ProductViews do
use Aurora.Uix
alias MyApp.Inventory
auix_resource_metadata :product, context: Inventory, schema: Product do
field :name, required: true
field :description
field :price
end
auix_create_ui do
# Index handler for custom filtering
index_columns :product, [:name, :price],
handler_module: MyApp.ProductIndexHandler
# Edit handler for custom save logic
edit_layout :product, edit_handler_module: MyApp.ProductEditHandler do
inline [:name, :price, :description]
end
# Show handler for custom event handling
show_layout :product, show_handler_module: MyApp.ProductShowHandler do
inline [:name, :price, :description]
end
end
endEvent Handling
Aurora UIX generates handlers for standard CRUD events. You can extend or override them:
Built-in Events
Index View:
"auix_mount"- Initial mount and data loading"auix_apply_action"- Apply route action (new, edit, show)"validate"- Form validation"save"- Save entity (create/update)"delete"- Delete entity"filters-changed"- Apply filters"filters-clear"- Clear all filters"sort-changed"- Change sort column/direction"page-changed"- Navigate to page
Show View:
"auix_mount"- Load entity details"switch_section"- Switch between tabs/sections"delete"- Delete entity"auix_route_forward"- Navigate forward"auix_route_back"- Navigate back
Adding Custom Events
defmodule MyApp.ProductHandlers.Index do
use Aurora.Uix.Templates.Basic.Handlers.Index
@impl true
def handle_event("publish", %{"id" => id}, socket) do
product = Inventory.get_product(id)
Inventory.publish_product(product)
{:noreply, socket}
end
endKey Callbacks
mount/3
Initializes the LiveView socket with:
:auixassigns containing context, schema, and configuration- Stream setup for efficient list rendering
- Initial entity loading for show views
handle_params/3
Called when URL parameters change. Handles:
- Route action determination (
:new,:edit,:show) - Form component assignment
- Routing stack management for navigation
handle_event/3
Processes user events. Aurora UIX provides default implementations for:
- Form submission and validation
- CRUD operations
- Navigation
- Filtering and sorting
handle_info/2
Handles asynchronous operations and notifications from other processes.
handle_async/3
Manages async task results for long-running operations.
Form Handling
Aurora UIX automatically generates forms based on your resource metadata. Forms are handled through the "validate" and "save" events:
defmodule MyApp.ProductHandlers.Index do
use Aurora.Uix.Templates.Basic.Handlers.Index
@impl true
def handle_event("save", %{"product" => product_params}, socket) do
case Inventory.create_product(product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_navigate(to: ~p"/products/#{product.id}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
endWorking with Streams
Aurora UIX uses Phoenix LiveView streams for efficient list rendering. Streams are automatically managed for the index view and automatically created with specific naming conventions.
Stream Naming
Aurora UIX creates multiple streams for different layout types:
- Primary stream - Named after the resource key (e.g.,
:productsfor a product index) - Alternate streams - For different view types, named as
#{source_key}__#{suffix}::products__index- For table/list index view:products__card- For card-based index view:products__calendar- For calendar index view (if configured)- Additional streams based on your layout configuration
The framework automatically manages these streams, inserting, updating, or deleting entries as needed.
Accessing Streams in Handler Hooks
To access streams in a custom handler:
def handle_event("refresh", _params, %{assigns: %{streams: streams}} = socket) do
# Access all streams for the current view
{:noreply, refresh_data(socket, streams)}
end
def handle_event("custom_action", _params, %{assigns: %{auix: auix}} = socket) do
# Use the source_key to work with the primary stream
source_key = auix.source_key # :products
{:noreply, stream_insert(socket, source_key, new_product)}
endStream Operations
Standard Phoenix LiveView stream operations work with Aurora UIX streams:
# Insert a new entry
stream_insert(socket, :products, new_product)
# Update an existing entry
stream_insert(socket, :products, updated_product)
# Delete an entry
stream_delete(socket, :products, deleted_product)
# Reset the entire stream
stream(socket, :products, fetched_products)Filtering and Sorting
Aurora UIX supports sorting via configuration in both resource metadata and layout DSL. Sorting is applied automatically and can be customized per view.
Configuring Default Sort Order
Option 1: In Resource Metadata
Define a default sort order at the resource level:
auix_resource_metadata :product, context: Inventory, schema: Product,
order_by: :reference # Sort by reference field by default
do
field :name
field :reference
field :price
endOption 2: In Layout DSL
Override or specify sort order for a specific view:
auix_create_ui do
# Override metadata sort with name-based sort
index_columns :product, [:id, :reference, :name, :cost],
order_by: :name
endHow Sorting Works
The order_by option:
- Can be a single field atom (
:name) - Determines the default sort column when the index loads
- Is applied through the query layer to the database
- Can be dynamically changed by the user via column headers (if enabled)
Example: Default Sort Configuration
# Metadata defines reference as default sort
auix_resource_metadata :product, context: Inventory, schema: Product,
order_by: :reference
auix_create_ui do
# Layout overrides with name sort
index_columns :product, [:id, :reference, :name, :cost],
order_by: :name
# Show layout has no sort (N/A for detail view)
show_layout :product do
inline [:name, :reference, :price]
end
endWhen the index loads:
- Products are sorted by
:name(from layout) - Users can click sortable column headers to change sort order (if implemented)
- The sort is applied at the database level for performance
Navigation
Aurora UIX handles navigation through:
- LiveView patches - For route changes within the same view (fast)
- LiveView pushes - For full page navigation
- Routing stack - Maintains history for back/forward navigation
Navigate between views:
push_navigate(socket, to: ~p"/products/#{product.id}")
push_patch(socket, to: ~p"/products/#{product.id}/edit")Performance Considerations
Streams for Large Lists
Aurora UIX uses Phoenix LiveView streams for index views, which efficiently handle:
- Inserts/updates/deletes without full re-render
- Pagination for large datasets
- Lazy loading capabilities
Preloading Associations
Configure preloads in your resource metadata to minimize N+1 queries:
auix_resource_metadata :product, context: Inventory, schema: Product do
field :name
field :category, preload: true # Preload associated data
endAsync Operations
Use handle_async/3 for heavy operations:
def handle_event("export", _params, socket) do
{:noreply,
start_async(socket, :export, fn -> export_products() end)}
end
def handle_async(:export, {:ok, file_path}, socket) do
{:noreply, push_download(socket, :file, file_path)}
endDebugging
Inspecting Socket Assigns
The :auix assign contains all configuration and runtime state:
def mount(_params, _session, socket) do
IO.inspect(socket.assigns.auix, label: "Aurora UIX Config")
{:ok, socket}
endLiveView DevTools
Use Phoenix LiveDashboard to monitor:
- Active LiveView processes
- Socket state and assigns
- Event flow and timing
Enable in development:
# config/dev.exs
config :aurora_uix, :dev_routes, trueVisit http://localhost:4000/dev/dashboard
Best Practices
- Keep handlers focused - One concern per handler module
- Use streams - Always use streams for list views instead of assigning the full list
- Validate early - Validate inputs in
handle_eventbefore database operations - Handle errors gracefully - Provide user feedback for all operations
- Preload data - Configure preloads to avoid N+1 queries
- Test in isolation - Test handlers independently from LiveView
- Document custom events - Document any custom event handlers for team clarity