LiveViewBus

Hex versionHex downloadsCI

A simple event bus for Phoenix LiveViews to send events between LiveViews and LiveComponents by ID—without manually wiring PubSub, topics, and handle_info every time.

Installation

Add live_view_bus to your list of dependencies in mix.exs:

def deps do
  [
    {:live_view_bus, "~> 0.1.0"}
  ]
end

Quick start

1. Subscribe in mount

defmodule MyAppWeb.ProductLive.Index do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    LiveViewBus.subscribe(MyApp.PubSub, "products_list")
    {:ok, assign(socket, products: [], filters: %{})}
  end

  def handle_info({:live_view_bus, "products_list", "filters_changed", payload}, socket) do
    products = load_products(payload)
    {:noreply, assign(socket, products: products)}
  end
end

2. Send from another LiveView or Component

# When filters change in a sidebar component:
LiveViewBus.send(MyApp.PubSub, "products_list", "filters_changed", %{
  category: "electronics",
  sort: "price"
})

That's it. No PubSub topic management, no boilerplate.

Usage

Using the on_mount hook

For automatic subscription when the LiveView mounts, use the on_mount hook in your router:

# In your router (lib/my_app_web/router.ex)
live_session :default, on_mount: [
  {LiveViewBus.LiveView, :subscribe, pubsub: MyApp.PubSub, ids: ["products_list"]}
] do
  live "/products", ProductLive.Index
end

For multiple IDs:

on_mount: [
  {LiveViewBus.LiveView, :subscribe, pubsub: MyApp.PubSub, ids: ["products_list", "dashboard"]}
]

Message format

All messages are delivered as:

{:live_view_bus, id, event, payload}

API reference

Function Description
LiveViewBus.subscribe(pubsub, id) Subscribe the current process to id
LiveViewBus.unsubscribe(pubsub, id) Unsubscribe from id
LiveViewBus.send(pubsub, id, event, payload \\ %{}) Send event to all subscribers of id
LiveViewBus.broadcast(pubsub, id, event, payload \\ %{}) Same as send/4
LiveViewBus.topic(id) Returns the PubSub topic string (for advanced use)

Note: Unsubscribe is optional. When the LiveView process ends, PubSub automatically removes the subscription.

Examples

Example 1: Filter sidebar + product list

Filter component (sends when user changes filters):

defmodule MyAppWeb.ProductLive.FilterComponent do
  use MyAppWeb, :live_component

  def handle_event("apply", %{"category" => category, "sort" => sort}, socket) do
    LiveViewBus.send(MyApp.PubSub, "products_list", "filters_changed", %{
      category: category,
      sort: sort
    })
    {:noreply, socket}
  end
end

Product list LiveView (receives and updates):

defmodule MyAppWeb.ProductLive.Index do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    LiveViewBus.subscribe(MyApp.PubSub, "products_list")
    {:ok, assign(socket, products: load_products(%{}))}
  end

  def handle_info({:live_view_bus, "products_list", "filters_changed", payload}, socket) do
    products = load_products(payload)
    {:noreply, assign(socket, products: products)}
  end

  defp load_products(filters) do
    # Your Ecto query here
    []
  end
end

Example 2: Dashboard with multiple widgets

Widget A broadcasts when data is refreshed:

LiveViewBus.broadcast(MyApp.PubSub, "dashboard", "data_refreshed", %{
  widget: "sales",
  timestamp: DateTime.utc_now()
})

Widget B and C subscribe and react:

def mount(_params, _session, socket) do
  LiveViewBus.subscribe(MyApp.PubSub, "dashboard")
  {:ok, socket}
end

def handle_info({:live_view_bus, "dashboard", "data_refreshed", payload}, socket) do
  # Refresh this widget's data
  {:noreply, assign(socket, data: fetch_data())}
end

Example 3: Real-time room updates

# User joins room "room_123"
def mount(%{"room_id" => room_id}, _session, socket) do
  LiveViewBus.subscribe(MyApp.PubSub, "room:#{room_id}")
  {:ok, assign(socket, room_id: room_id)}
end

# Another user sends a message:
LiveViewBus.send(MyApp.PubSub, "room:#{room_id}", "new_message", %{
  user: "alice",
  body: "Hello!"
})

# All subscribers receive it:
def handle_info({:live_view_bus, _topic, "new_message", payload}, socket) do
  {:noreply, assign(socket, messages: [payload | socket.assigns.messages])}
end

License

MIT