Livex.LivexComponent (livex v0.1.2)
A module that enhances Phoenix LiveComponent with automatic state management and lifecycle improvements.
Features
- Declarative State Definition: Define component state properties with type safety
- Component Properties (attr): Define expected properties passed from parent
- Simplified Lifecycle: Consolidate data derivation logic in the
pre_render
callback - Dependency-Aware Assignments: Use
assign_new/4
to compute values only when dependencies change - Simplified State Updates: Use
JSX.assign_state
for direct state updates from templates - Component Events: Emit events to parent views with
push_emit
orJSX.emit
- PubSub Integration: Subscribe to topics with dependency-aware
assign_topic
State and Properties
Livex components use two key concepts for managing data:
- state - Internal component state that persists across renders
- attr - Properties passed down from parent components/views
Component State
# Client-side state, survives reconnects but not refreshes
state :is_expanded, :boolean
state :selected_tab, :string
state :current_count, :integer
Component Properties (attr)
# Properties passed from parent
attr :id, :string, required: true
attr :item_id, :string
attr :initial_value, :string
attr :on_save, :any
Lifecycle Management
Livex simplifies the component lifecycle by consolidating data derivation and initial
assignment logic into a pre_render/1
callback. This is where you can initialize state
based on attr values or compute derived data.
def pre_render(socket) do
{:noreply,
socket
|> assign_new(:is_expanded, fn -> false end)
|> assign_new(:pending_selection, [:selected_value], &(&1.selected_value))
|> assign_new(:has_changes, fn -> false end)
|> then(fn socket ->
# Compute derived state
assign(socket, :has_changes,
socket.assigns.pending_selection != socket.assigns.selected_value)
end)}
end
Simplified State Updates
Livex introduces JSX.assign_state
for updating component state directly from templates:
# Update a single value
<button phx-click={JSX.assign_state(:is_expanded, true)}>Expand</button>
# Update multiple values
<button phx-click={JSX.assign_state(is_expanded: false, selected_tab: "details")}>
Close
</button>
# Conditional updates
<button phx-click={
if @is_expanded do
JSX.assign_state(is_expanded: false, pending_value: @initial_value)
else
JSX.assign_state(is_expanded: true)
end
}>
{if @is_expanded, do: "Close", else: "Expand"}
</button>
Component Events
Livex enhances event handling for components, allowing them to emit custom events that parent LiveViews or LiveComponents can listen for:
Emitting from templates:
<button phx-click={JSX.emit(:save, value: %{id: @id, data: @form_data})}>
Save Changes
</button>
Emitting from Elixir code:
def handle_event("save_changes", _, socket) do
# Process the data...
# Emit an event to the parent
socket = push_emit(socket, :saved, %{id: socket.assigns.id})
{:noreply, assign(socket, :is_saving, false)}
end
Handling in parent:
<.live_component
module={MyApp.FormComponent}
id="my-form"
phx-saved="handle_form_saved"
/>
# In the parent's handle_event
def handle_event("handle_form_saved", %{"id" => id}, socket) do
# Handle the saved event
{:noreply, socket}
end
PubSub Integration
Livex makes it easier for components to subscribe to PubSub topics with assign_topic
:
def pre_render(socket) do
{:noreply,
socket
|> assign_new(:status_message, fn -> "Connecting..." end)
|> assign_topic(:doc_updates, [:document_id], fn assigns ->
"document_updates:#{assigns.document_id}"
end)}
end
# Handle PubSub messages
def handle_info({:doc_updates, %{message: msg}}, socket) do
{:noreply, assign(socket, :status_message, msg)}
end
Complete Example
defmodule MyApp.FormComponent do
use Livex.LivexComponent
attr :id, :string, required: true
attr :item_id, :string
attr :on_save, :any
state :is_expanded, :boolean
state :form_data, :map
state :is_valid, :boolean
def pre_render(socket) do
{:noreply,
socket
|> assign_new(:is_expanded, fn -> false end)
|> assign_new(:form_data, [:item_id], fn assigns ->
if assigns.item_id, do: load_item(assigns.item_id), else: %{}
end)
|> assign_new(:is_valid, [:form_data], fn assigns ->
validate_form(assigns.form_data)
end)}
end
def render(assigns) do
~H"""
<div id={@id} class="form-component">
<h3>Edit Item</h3>
<div class="form-fields">
<!-- Form fields here -->
</div>
<div class="actions">
<button
disabled={!@is_valid}
phx-click="save"
phx-target={@myself}>
Save
</button>
<button phx-click={JSX.emit(:cancel)}>
Cancel
</button>
</div>
</div>
"""
end
def handle_event("save", _, socket) do
# Process the save...
if socket.assigns.on_save do
socket.assigns.on_save.(socket.assigns.form_data)
end
# Emit an event to the parent
socket = push_emit(socket, :saved, %{id: socket.assigns.id})
{:noreply, socket}
end
defp load_item(id) do
# Load item data
end
defp validate_form(data) do
# Validate form data
end
end