Skip to content

Commit

Permalink
Change the API to emit and receive arbitrary payloads
Browse files Browse the repository at this point in the history
  • Loading branch information
schrockwell committed Aug 19, 2023
1 parent bcda1af commit faef444
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 304 deletions.
33 changes: 17 additions & 16 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
# Used by "mix format"
[
line_length: 120,
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:phoenix_live_view],
locals_without_parens: [
assert_receive_event: 1,
assert_receive_event: 2,
assert_receive_event: 3,
assert_receive_event: 4,
assert_receive_event: 5,
assert_received_event: 3,
assert_received_event: 4,
assert_received_event: 1,
assert_received_event: 2,
refute_receive_event: 1,
refute_receive_event: 2,
refute_receive_event: 3,
refute_receive_event: 4,
refute_receive_event: 5,
refute_received_event: 3,
refute_received_event: 4
refute_received_event: 1,
refute_received_event: 2
],
export: [
locals_without_parens: [
assert_receive_event: 1,
assert_receive_event: 2,
assert_receive_event: 3,
assert_receive_event: 4,
assert_receive_event: 5,
assert_received_event: 3,
assert_received_event: 4,
assert_received_event: 1,
assert_received_event: 2,
refute_receive_event: 1,
refute_receive_event: 2,
refute_receive_event: 3,
refute_receive_event: 4,
refute_receive_event: 5,
refute_received_event: 3,
refute_received_event: 4
refute_received_event: 1,
refute_received_event: 2
]
]
]
212 changes: 77 additions & 135 deletions lib/live_event.ex
Original file line number Diff line number Diff line change
@@ -1,192 +1,134 @@
defmodule LiveEvent do
import LiveEvent.Internal

@moduledoc """
Standardized event handling in LiveViews and LiveComponents.
## The problem
LiveView currently has two built-in mechanisms for sending messages between views and components:
- `send/2` -> `handle_info/2` for sending to a LiveView
- `send_update/3` -> `update/2` for sending to a LiveComponent
Picking a singular approach limits component reusability, and using both approaches results in an inconsistent
event API.
## The solution
LiveEvent standardizes these systems into a singular flow:
Out of the box, LiveView has two distinct mechanisms for sending messages to LiveViews and LiveComponents:
- `emit/3` -> `c:LiveEvent.handle_emit/4` for sending to a LiveView _or_ LiveComponent
- `send/2` to send to a LiveView
- `c:Phoenix.LiveView.handle_info/2` to receive in a LiveView
- `Phoenix.LiveView.send_update/3` to send to a LiveComponent
- `c:Phoenix.LiveComponent.update/2` to receive in a LiveComponent
When `use LiveEvent.LiveComponent` or `use LiveEvent.LiveView` is invoked, it hooks into the lifecycle of the
view or component to transparently add support for the `c:LiveEvent.handle_emit/4` callback.
LiveEvent standardizes this API with a single pair of functions that work both sending and receiving
in both LiveView and LiveComponents:
## Event destinations
- `emit/2` to send to a LiveView or LiveComponent
- `c:handle_emit/2` to receive in a LiveView or LiveComponent
Imagine a LiveComponent that has an `:on_selected` event assign that is raised like so:
emit(socket, :on_selected)
To handle the event on a LiveView, pass a pid to the event assign.
```heex
<.live_component module={MyComponent} id="foo" on_selected={self()}>
```
## Example
To handle the event on a LiveComponent, pass `{module, id}` to the event assign.
Let's define a fancy button component that emits a click event with a timestamp.
```heex
<.live_component module={MyComponent} id="foo" on_selected={{__MODULE__, @id}}>
```
defmodule FancyButton do
use Phoenix.LiveComponent
use LiveEvent.LiveComponent # <-- new!
In both cases, the event is handled by the `c:LiveEvent.handle_emit/4` callback.
def handle_event("click", _, socket) do
emit(socket.assigns.on_click, {:click, DateTime.utc_now()})
{:noreply, socket}
end
# On a LiveView OR LiveComponent
def handle_emit(:on_selected, {MyComponent, "foo"}, _payload, socket), do: ...
def render(assigns) do
~H"\"\"
<button phx-click="click" phx-target={@myself}>Click me</button>
\"\"\"
end
end
# Example
And a LiveView that handles that event.
defmodule MyLiveView do
defmodule FancyView do
use Phoenix.LiveView
use LiveEvent.LiveView
use LiveEvent.LiveView # <-- new!
def handle_emit({:click, timestamp}, socket) do
# ...do something with the event...
{:ok, socket}
end
def render(assigns) do
~H\"\"\"
<.live_component module={MyLiveComponent} id="my-component" on_selected={self()} />
<.live_component module={FancyButton} id="fancy-button" on_click={self()} />
\"\"\"
end
def handle_emit(:on_selected, {MyLiveComponent, "my-component"}, %{at: at}, socket) do
IO.puts("Selected at \#{at}")
{:ok, socket}
end
end
defmodule MyLiveComponent do
Using the exact same syntax, you can also have a LiveComponent handle the same event from the FancyButton.
defmodule ContainerComponent do
use Phoenix.LiveComponent
use LiveEvent.LiveComponent
use LiveEvent.LiveComponent # <-- new!
def handle_emit({:click, timestamp}, socket) do
# ...do something with the event...
{:ok, socket}
end
def render(assigns) do
~H\"\"\"
<button phx-click="click" phx-targt={@myself}>Click me</button>
<.live_component module={FancyButton} id="fancy-button" on_click={{__MODULE__, @id}}} />
\"\"\"
end
def handle_emit("click", _, socket) do
{:noreply, emit(socket, :on_selected, %{at: DateTime.utc_now()})}
end
end
"""

@doc """
Handle an event message sent by `emit/3` or `send_event/4`.
Events sent via `emit/3` have a `source` argument of the form `{module, id}`.
## Compared to `handle_emit/3`
## Installation
This callback is distinct from LiveView's `handle_emit/3` callback in a few important ways:
Add the dependency to `mix.exs`:
- The arity is different
- The result is `{:ok, socket}`, not `{:noreply, socket}`
- LiveEvent uses atoms for event names, not strings
- LiveEvent always originate from the server, not the client
def deps do
{:live_event, "~> #{LiveEvent.MixProject.project()[:version]}"}
end
## Example
Update `MyAppWeb` to include the integrations:
def handle_emit(:on_profile_selected, {MyLiveComponent, _id}, profile_id, socket), do: ...
def MyAppWeb do
def live_view do
use LiveEvent.LiveView
end
def live_component do
use LiveEvent.LiveComponent
end
end
"""
@callback handle_emit(
name :: atom,
source :: any,
payload :: any,
socket :: LiveView.Socket.t()
) ::
{:ok, socket :: LiveView.Socket.t()}

@optional_callbacks handle_emit: 4

@doc """
Raise an event from a LiveView or LiveComponent.
Handle an event message sent by `emit/2`.
The `event_name` argument is the name of the optional socket assign whose value specifies the destination for the event.
The name of the emitted event defaults to the name of the assign.
For this callback to work, `use LiveEvent.LiveView` or `use LiveEvent.LiveComponent` must be called
in the module.
Possible assign values are:
## Example
- `nil` to not raise the event
- a pid, to send the event to a LiveView
- `{pid, event_name}` to send the event to a LiveView with a custom event name
- `{module, id}` to send the event to a LiveComponent
- `{module, id, event_name}` to send the event to a LiveComponent with a custom event name
def handle_emit({:profile_selected, profile_id}, socket), do: ...
Returns the unmodified socket.
"""
@spec emit(socket :: Phoenix.LiveView.Socket.t(), event_name :: atom, payload :: any) ::
Phoenix.LiveView.Socket.t()
def emit(socket, event_name, payload \\ nil) do
source = {get_module(socket), socket.assigns[:id]}

case socket.assigns[event_name] do
nil ->
nil

{pid, custom_name} when is_pid(pid) ->
send_event(pid, custom_name, payload, source: source)

{module, id, custom_name} ->
send_event({module, id}, custom_name, payload, source: source)

destination ->
send_event(destination, event_name, payload, source: source)
end
@callback handle_emit(payload :: any, socket :: LiveView.Socket.t()) :: {:ok, socket :: LiveView.Socket.t()}

socket
end
@optional_callbacks handle_emit: 2

@doc """
Send an event to a LiveView or LiveComponent.
Typically, `emit/3` should be used to send events, but this function can be used if more control is needed.
To send to a LiveView (or any other process), specify a pid (usually `self()`) as the `destination`.
To send to a LiveComponent, specify `{module, id}` as the `destination`.
The event can handled by the `c:LiveEvent.handle_emit/4` callback.
Send an event to either a LiveView or LiveComponent.
When sending to an arbitrary process, the message will be a `LiveEvent.Event` struct, although you
should not normally have to deal with that directly.
To send to a LiveView, specify a pid as the `target`.
## Options
To send to a LiveComponent, specify a `{module, id}` as the `target`.
- `:source` - where the event originated from; defaults to `nil`
In either case, the event can handled by the `c:LiveEvent.handle_emit/2` callback.
## Examples
send_event(self(), :on_selected, %{profile_id: 123})
# => def handle_emit(:on_selected, _source, %{profile_id: id}, socket), do: ...
## Example
send_event({MyComponent, "my-id"}, :on_selected, %{profile_id: 123})
# => def handle_emit(:on_selected, _source, %{profile_id: id}, socket), do: ...
emit(socket.assigns.on_profile_selected, {:profile_selected, socket.assigns.profile_id})
"""
@spec send_event(
destination :: LiveView.Event.destination(),
event_name :: atom,
payload :: any,
opts :: keyword
) ::
:ok
def send_event(destination, event_name, payload, opts \\ []) do
message = %LiveEvent.Event{name: event_name, source: opts[:source], payload: payload}

case destination do
pid when is_pid(pid) ->
send(pid, message)

{module, id} when is_atom(module) ->
Phoenix.LiveView.send_update(module, id: id, __message__: message)
@spec emit(target :: pid | {module, String.t() | atom}, payload :: any) :: :ok
def emit(target, payload) do
message = %LiveEvent.Event{payload: payload}

case target do
pid when is_pid(pid) -> send(pid, message)
{module, id} when is_atom(module) -> Phoenix.LiveView.send_update(module, id: id, __message__: message)
end

:ok
Expand Down
11 changes: 3 additions & 8 deletions lib/live_event/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@ defmodule LiveEvent.Event do
This is documented in the unlikely chance that the destination process or component needs to directly handle it.
This struct should not be constructed directly by the developer - use `LiveEvent.send_event/4`
and `LiveEvent.emit/3` instead.
This struct should not be constructed directly by the developer - use `LiveEvent.emit/2` instead.
"""

@type t :: %__MODULE__{
name: atom,
payload: any,
source: any
payload: any
}

@type destination :: pid | {module, String.t() | atom}

defstruct [:name, :payload, :source]
defstruct [:payload]
end
11 changes: 0 additions & 11 deletions lib/live_event/internal.ex

This file was deleted.

Loading

0 comments on commit faef444

Please sign in to comment.