README.md

# LiveViewBus

[![Hex version](https://img.shields.io/hexpm/v/live_view_bus.svg)](https://hex.pm/packages/live_view_bus)
[![Hex downloads](https://img.shields.io/hexpm/dt/live_view_bus.svg)](https://hex.pm/packages/live_view_bus)
[![CI](https://github.com/half-blood-labs/live_view_bus/actions/workflows/ci.yml/badge.svg)](https://github.com/half-blood-labs/live_view_bus/actions/workflows/ci.yml)

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`:

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

## Quick start

### 1. Subscribe in mount

```elixir
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

```elixir
# 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:

```elixir
# 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:

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

### Message format

All messages are delivered as:

```elixir
{:live_view_bus, id, event, payload}
```

- `id` – the topic ID you subscribed to
- `event` – the event name (string or atom)
- `payload` – the payload map (defaults to `%{}`)

### 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):

```elixir
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):

```elixir
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:

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

**Widget B and C** subscribe and react:

```elixir
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

```elixir
# 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