Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve docs. #2

Merged
merged 4 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright (c) 2023

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
113 changes: 106 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# LiveViewEvents

![Elixir CI](https://github.com/DockYard/live_view_events/actions/workflows/elixir-ci.yml/badge.svg)

LiveViewEvents provides a set of tools to send events between components.

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `live_view_events` to your list of dependencies in `mix.exs`:
Add `live_view_events` to your list of dependencies in `mix.exs` like:

```elixir
def deps do
Expand All @@ -15,11 +16,109 @@ def deps do
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/live_view_events>.

## Usage

TBD
Add `use LiveViewEvents` whenever you want to use any of the features of the libraries.

### Sending events to LiveView components

You can send events to LiveView components by using `notify_to/2` or `notify_to/3` (the only
difference being that the latter sends some extra params). These functions accept a target as
first argument and a message name as second. Targets can be any of:

- `:self` to send to `self()`.
- A PID.
- A tuple of the form `{Module, "id"}` to send a message to a `LiveView.Component` in the same process.
- A tuple of the form `{pid, Module, "id"}` to send a message to a `LiveView.Component` in a different process.

You an handle these messages on the `handle_info/2` callback on
your LiveView components. For being able to do so, you need to
use the `handle_info_or_assign/2` macro on the `update/3` callback of your component.

## Example


In our view, we are going to have two components. The first one is a `Sender`
that can send an event to whatever is being passed. Let's dig into the
implementation:

```elixir
defmodule MyAppWeb.Components.Sender do
use MyAppWeb, :live_component
use LiveViewEvents

def mount(socket) do
socket = assign(socket, :notify_to, :self)

{:ok, socket}
end

def render(assigns) do
~H[<button type="button" phx-click="clicked" phx-target={@myself}>Send event</button>]
end

def handle_event("clicked", _params, socket) do
notify_to(socket.assigns.notify_to, :sender_event, :rand.uniform(100))
{:noreply, socket}
end
end
```

This component will send a `:sender_event` message to whatever we pass to the `notify_to`
attribute. It sends a random number between 0 and up to 100 as parameter.

Let's dig into the receiver:

```elixir
defmodule MyAppWeb.Components.Receiver do
use MyAppWeb, :live_component
use LiveViewEvents

def mount(socket) do
socket = assign(socket, :messages, [])

{:ok, socket}
end

def update(assigns, socket) do
socket = handle_info_or_assign(socket, assigns)

{:ok, socket}
end

def render(assigns),
do: ~H[<ul><li :for={m <- @messages}><%= m %></li></ul>]

def handle_info({:sender_event, num}, socket) do
{:noreply, update(socket, :messages, &[num | &1])}
end
end
```

This component will receive messages and handle them in `handle_info/2` because
it is using `handle_info_or_assign/2` in their `LiveComponent.update/2`.
It will add the received messages to `socket.assigns.messages` and display
them. As the reader can see, it is pattern matching against `:sender_event` messages.
When `notify_to/3` is used, the message sent is a tuple containing the event name
as first element, and the params as second the second element.

Finally, let's take a look at what the live view template would need to look like
for this to work:

```heex
<div class="contents">
<.live_component
module={MyAppWeb.Components.Sender}
id="sender"
notify_to={{MyAppWeb.Components.Receiver, "receiver"}}
/>
<.live_component module={MyAppWeb.Components.Receiver} id="receiver" />
</div>
```

In this template, we set `notify_to` to the tuple `{MyAppWeb.Components.Receiver, "receiver"}`.
The first element of the tuple is the live component module and the second is the id.
Optionally, the tuple can contain an extra first element that needs to be a PID. Though
this might not be useful in the application code (there are way better ways to send events
between processes), it is quite useful when testing. When testing a `LiveView`
it creates a new process for it. Its PID can be accessed through `view.pid`.
5 changes: 4 additions & 1 deletion lib/live_view_events.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
defmodule LiveViewEvents do
@moduledoc """
Documentation for `LiveViewEvents`.
Add `use LiveViewEvents` to the module you want to use any
of the features of `LiveViewEvents` in.

For more info about sending and receiving events, see `LiveViewEvents.Notify`.
"""

defmacro __using__(_opts) do
Expand Down
105 changes: 95 additions & 10 deletions lib/live_view_events/notify.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,88 @@
defmodule LiveViewEvents.Notify do
@moduledoc false
@moduledoc """
Functions to send messages in the server including to live components
and handle them.

## Example

In our view, we are going to have two components. The first one is a `Sender`
that can send an event to whatever is being passed. Let's dig into the
implementation:

defmodule MyAppWeb.Components.Sender do
use MyAppWeb, :live_component
use LiveViewEvents

def mount(socket) do
socket = assign(socket, :notify_to, :self)

{:ok, socket}
end

def render(assigns) do
~H[<button type="button" phx-click="clicked" phx-target={@myself}>Send event</button>]
end

def handle_event("clicked", _params, socket) do
notify_to(socket.assigns.notify_to, :sender_event, :rand.uniform(100))
{:noreply, socket}
end
end

This component will send a `:sender_event` message to whatever we pass to the `notify_to`
attribute. It sends a random number between 0 and up to 100 as parameter.

Let's dig into the receiver:

defmodule MyAppWeb.Components.Receiver do
use MyAppWeb, :live_component
use LiveViewEvents

def mount(socket) do
socket = assign(socket, :messages, [])

{:ok, socket}
end

def update(assigns, socket) do
socket = handle_info_or_assign(socket, assigns)

{:ok, socket}
end

def render(assigns), do: ~H[<ul><li :for={m <- @messages}><%= m %></li></ul>]

def handle_info({:sender_event, num}, socket) do
{:noreply, update(socket, :messages, &[num | &1])}
end
end

This component will receive messages and handle them in `handle_info/2` because
it is using `handle_info_or_assign/2` in their [`LiveComponent.update/2`](`c:Phoenix.LiveComponent.update/2`).
It will add the received messages to `socket.assigns.messages` and display
them. As the reader can see, it is pattern matching against `:sender_event` messages.
When `notify_to/3` is used, the message sent is a tuple containing the event name
as first element, and the params as second the second element.

Finally, let's take a look at what the live view template would need to look like
for this to work:

<div class="contents">
<.live_component
module={MyAppWeb.Components.Sender}
id="sender"
notify_to={{MyAppWeb.Components.Receiver, "receiver"}}
/>
<.live_component module={MyAppWeb.Components.Receiver} id="receiver" />
</div>

In this template, we set `notify_to` to the tuple `{MyAppWeb.Components.Receiver, "receiver"}`.
The first element of the tuple is the live component module and the second is the id.
Optionally, the tuple can contain an extra first element that needs to be a PID. Though
this might not be useful in the application code (there are way better ways to send events
between processes), it is quite useful when testing. When testing a [`LiveView`](`Phoenix.LiveView`),
it creates a new process for it. Its PID can be accessed through `view.pid`.
"""

@assign_name_for_event "__live_view_events__assign_event__"

Expand All @@ -22,14 +105,15 @@ defmodule LiveViewEvents.Notify do
## Why using `c:Phoenix.LiveView.handle_info/2` in components?

In one word: consistency. Messages coming from the client are
handled by `c:Phoenix.LiveView.handle_event/3` or by
`c:Phoenix.LiveComponent.handle_event/3`. For messages sent from
the server are currently being handled by `c:Phoenix.LiveView.handle_info/2`
in live views, with not official way to do this but the hack this
library is based on.

The hack is basically send an update with `Phoenix.LiveView.send_update/3`
and handle it in `c:Phoenix.LiveComponent.update/2`
handled by [LiveView.handle_event/3](`c:Phoenix.LiveView.handle_event/3`)
in live views or by [`LiveComponent.handle_event/3`](`c:Phoenix.LiveComponent.handle_event/3`)
in live components.
Messages sent from the server are currently being handled by
[`LiveView.handle_info/2`](`c:Phoenix.LiveView.handle_info/2`) in live views,
with no official way to do this but the __hack__ this library is based on.

The hack is basically send an update with [`LiveView.send_update/3`](`Phoenix.LiveView.send_update/3`)
and handle it in [`LiveComponent.update/2`](`c:Phoenix.LiveComponent.update/2`).
"""
defmacro handle_info_or_assign(socket, assigns) do
quote do
Expand Down Expand Up @@ -67,7 +151,8 @@ defmodule LiveViewEvents.Notify do

- `:self` to send to `self()`.
- A PID.
- A tuple of the form `{Module, "id"}` to send a message to a LiveView component.
- A tuple of the form `{Module, "id"}` to send a message to a [`LiveView.Component`](`Phoenix.LiveView.Component`) in the same process.
- A tuple of the form `{pid, Module, "id"}` to send a message to a [`LiveView.Component`](`Phoenix.LiveView.Component`) in a different process.
"""
def notify_to(:self, message), do: notify_to(self(), message)
def notify_to(pid, message) when is_pid(pid), do: send(pid, message)
Expand Down
30 changes: 28 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
defmodule LiveViewEvents.MixProject do
use Mix.Project

@source_url "https://github.com/DockYard/live_view_events"
@version "0.1.0"

def project do
[
app: :live_view_events,
version: "0.1.0",
version: @version,
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
deps: deps(),
docs: docs(),
license: "MIT",
package: package()
]
end

Expand All @@ -18,6 +24,26 @@ defmodule LiveViewEvents.MixProject do
]
end

def docs do
[
extras: [{:"README.md", [title: "Overview"]}],
main: "readme",
source_url: @source_url,
source_ref: "v#{@version}"
]
end

def package do
[
maintainers: ["Sergio Arbeo"],
licenses: ["MIT"],
links: %{"GitHub" => @source_url},
files: ~w(lib LICENSE.md mix.exs README.md),
description:
"A library to unify and simplify sending messages between components and views in the server for Phoenix LiveView."
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
Expand Down
10 changes: 10 additions & 0 deletions test_app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
test_app-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

5 changes: 5 additions & 0 deletions test_app/assets/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* This file is for your main application CSS */
41 changes: 41 additions & 0 deletions test_app/assets/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
// import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
// import "some-package"
//

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

Loading
Loading