View Source SaladUI Communication Guide
This document explains how communication works between different parts of the SaladUI system: client-to-server, server-to-client, and client-to-client.
Overview
SaladUI uses a hybrid architecture with multiple communication channels:
- Client → Server: JavaScript components send events to LiveView
- Server → Client: LiveView sends commands to JavaScript components
- Client → Client: JavaScript components communicate directly with each other
Client → Server Communication
JavaScript components send events to the Phoenix LiveView server.
1. Event Mapping Configuration
In your Elixir component, map client events to server handlers:
defmodule SaladUI.Dialog do
def dialog(assigns) do
event_map = %{}
|> add_event_mapping(assigns, "opened", :"on-open")
|> add_event_mapping(assigns, "closed", :"on-close")
|> add_event_mapping(assigns, "item-selected", :"on-item-selected")
assigns = assign(assigns, :event_map, json(event_map))
~H"""
<div
data-component="dialog"
data-event-mappings={@event_map}
phx-hook="SaladUI"
>
<!-- component content -->
</div>
"""
end
end
2. Sending Events from JavaScript
In your JavaScript component, use pushEvent()
to send data to the server:
class DialogComponent extends Component {
onOpenEnter() {
// Send simple event
this.pushEvent("opened");
// Send event with data
this.pushEvent("opened", {
dialogId: this.el.id,
timestamp: Date.now()
});
}
selectItem(item) {
// Send structured data
this.pushEvent("item-selected", {
value: item.value,
label: item.textContent,
index: this.items.indexOf(item)
});
}
}
3. Handling Events in LiveView
Handle the events in your LiveView module:
defmodule MyAppWeb.PageLive do
use MyAppWeb, :live_view
# Event handler matching the event mapping
def handle_event("dialog_opened", params, socket) do
%{"dialogId" => dialog_id, "timestamp" => timestamp} = params
IO.puts("Dialog #{dialog_id} opened at #{timestamp}")
{:noreply, socket}
end
def handle_event("item_selected", params, socket) do
%{"value" => value, "label" => label} = params
socket =
socket
|> assign(:selected_item, value)
|> put_flash(:info, "Selected: #{label}")
{:noreply, socket}
end
end
4. Using Phoenix.LiveView.JS
For simple interactions, use JS commands directly in templates:
<.dialog on-open={JS.push("dialog_opened")} on-close={JS.push("dialog_closed")}>
<.dialog_trigger>
<.button>Open Dialog</.button>
</.dialog_trigger>
<.dialog_content>
<.button phx-click={JS.push("action_clicked", value: %{action: "save"})}>
Save
</.button>
</.dialog_content>
</.dialog>
Server → Client Communication
The server sends commands to JavaScript components to control their behavior.
1. Using SaladUI.LiveView.send_command/4
Send commands from LiveView to specific components:
defmodule MyAppWeb.PageLive do
def handle_event("open_dialog", _params, socket) do
# Send command to component
socket = SaladUI.LiveView.send_command(socket, "user-dialog", "open")
{:noreply, socket}
end
def handle_event("update_chart", _params, socket) do
new_data = get_chart_data()
# Send command with parameters
socket = SaladUI.LiveView.send_command(socket, "sales-chart", "update", %{
data: new_data,
options: %{animation: true}
})
{:noreply, socket}
end
def handle_event("close_all_dialogs", _params, socket) do
# Send commands to multiple components
socket =
socket
|> SaladUI.LiveView.send_command("dialog-1", "close")
|> SaladUI.LiveView.send_command("dialog-2", "close")
|> SaladUI.LiveView.send_command("dialog-3", "close")
{:noreply, socket}
end
end
2. Handling Commands in JavaScript
Components handle commands in the handleCommand()
method:
class ChartComponent extends Component {
handleCommand(command, params) {
switch (command) {
case "update":
this.updateChart(params.data, params.options);
return true;
case "reset":
this.resetChart();
return true;
case "highlight":
this.highlightDataPoint(params.index);
return true;
default:
return super.handleCommand(command, params);
}
}
updateChart(data, options = {}) {
this.chart.data = data;
if (options.animation) {
this.chart.update('active');
} else {
this.chart.update('none');
}
}
}
Client → Client Communication
Components can communicate directly with each other using JavaScript commands without server involvement.
Using SaladUI.JS.dispatch_command/3
Send commands directly between components using JavaScript actions in templates:
<!-- Button that opens a dialog -->
<.button phx-click={SaladUI.JS.dispatch_command("open", to: "#user-dialog")}>
Open Dialog
</.button>
<!-- Button that updates a chart -->
<.button phx-click={SaladUI.JS.dispatch_command("update",
to: "#sales-chart",
detail: %{data: @new_chart_data})}>
Refresh Chart
</.button>
<!-- Toggle multiple components -->
<.button phx-click={
%JS{}
|> SaladUI.JS.dispatch_command("toggle", to: "#sidebar")
|> SaladUI.JS.dispatch_command("close", to: "#dropdown")
}>
Toggle Layout
</.button>
How Client-to-Client Works
When you use SaladUI.JS.dispatch_command/3
, here's what happens:
- JavaScript Generation: The function generates JavaScript that dispatches a custom DOM event
- Event Dispatch: The browser fires a
salad_ui:command
event with the command details - Component Reception: The target component automatically receives and processes the command
- No Server Round-trip: Everything happens in the browser without contacting the server
Under the Hood
// What SaladUI.JS.dispatch_command generates
document.dispatchEvent(new CustomEvent('salad_ui:command', {
detail: {
command: 'open',
params: { data: newData },
target: 'user-dialog'
}
}));
// Components automatically listen for this event from both client and server
class Component {
setupEvents() {
document.addEventListener('salad_ui:command', (event) => {
const { command, params, target } = event.detail;
if (target === this.el.id) {
this.handleCommand(command, params);
}
});
}
}
Command Parameters
You can pass data between components using the detail
parameter:
<!-- Pass simple data -->
<.button phx-click={SaladUI.JS.dispatch_command("highlight",
to: "#data-table",
detail: %{row_id: @selected_row})}>
Highlight Row
</.button>
<!-- Pass complex data -->
<.button phx-click={SaladUI.JS.dispatch_command("filter",
to: "#product-list",
detail: %{
filters: %{
category: @category,
price_range: %{min: @min_price, max: @max_price},
in_stock: true
}
})}>
Apply Filters
</.button>
Chaining Commands
Execute multiple commands in sequence:
<.button phx-click={
%JS{}
|> SaladUI.JS.dispatch_command("close", to: "#main-dialog")
|> SaladUI.JS.dispatch_command("open", to: "#confirmation-dialog")
|> SaladUI.JS.dispatch_command("focus", to: "#cancel-button")
}>
Show Confirmation
</.button>
Handling Commands in Components
Components receive commands through their handleCommand
method the same way it handle server command:
class DataTableComponent extends Component {
handleCommand(command, params) {
switch (command) {
case "highlight":
this.highlightRow(params.row_id);
return true;
case "filter":
this.applyFilters(params.filters);
return true;
case "reset":
this.clearFilters();
this.clearHighlight();
return true;
default:
return super.handleCommand(command, params);
}
}
highlightRow(rowId) {
// Remove previous highlights
this.el.querySelectorAll('.highlighted').forEach(row => {
row.classList.remove('highlighted');
});
// Highlight specific row
const targetRow = this.el.querySelector(`[data-row-id="${rowId}"]`);
if (targetRow) {
targetRow.classList.add('highlighted');
}
}
applyFilters(filters) {
this.currentFilters = filters;
this.filterRows();
this.updateUI();
}
}
Communication Patterns Summary
When to Use Each Pattern
Client → Server:
- User actions that need server processing
- Form submissions and validation
- Data persistence
- Navigation changes
- Real-time updates to other users
Server → Client:
- Update component state based on server events
- Show/hide components based on permissions
- Update data displays after server processing
- Coordinate multiple components from server logic
Client → Client:
- Immediate UI feedback without server round-trip
- Filter/search interactions
- Component coordination and synchronization
- Local state management