Introducing `live_view_events`

A table of computers in a corner of a library with bookshelves in the background.

Whether you’re part of a startup or a large-scale enterprise development team, you need to save time, money, and resources. Elixir and LiveView Native do that for you. Learn all the ways Elixir can help you launch a better digital product faster and more efficiently by reading our free Ebook, “The Business Value of Elixir”.

What does This Library Do and Why Should I Use It?

In Phoenix LiveView, we can send messages from the current component to the current view easily using send/2, but sending messages from one component to another is not possible out of the box.

There has been a known hack, which is the official way of handling it, using send_update/3 and pattern-match in the c:update/2 callback for some special property names.

This hack relies on the fact that the assigns argument in the callback only contains the changed properties, and not all of them.

live_view_events provides some macros to simplify sending messages to both views and other components and to handle those messages in the components.

Why Not Just Use the Workaround?

There are two main reasons to use the live_view_events instead of the current workaround: consistency and efficiency.

First, consistency:

  • c:update/2 only deals with updating the assigns, and does not deal with messages.
  • All logic that deals with messages sent from the server lives inside a handle_info callback, be it in a live view or in a live component.

Secondly, because we’ve already written something not-generalized, simpler, hackier, and harder to maintain in the library as part of some of our projects, live_view_events is more efficient than the existing workaround.

A Quick Introduction to the First Release

This first release includes everything you need to send and handle messages between live views and components or between components and components. This feature already brings value to the team by simplifying their workflow and the mental model of the messages, but it is also the first release.

The next planned feature will provide a way for a component to easily subscribe to PubSub events. We are really excited to land that feature soon-ish.

Sending Events

The library provides a couple of macros (notify_to/2 and notify_to/3) to send messages to a variety of targets:

  • :self is the same as sending it to self() but, in some scenarios, it’s a useful way to tell your component to notify to its parent live view.
  • A PID
  • A tuple of the form {ComponentModule, "id"} to notify a component.
  • A tuple of the form {PID, ComponentModule, "id"} to notify a component in a different live view.

Receiving Events

Events, like normal messages, are handled in a handle_info callback. If you are in a live view, it is the normal handle_info you are used to. If you are in a component, you need to do some simple wiring. (Don’t worry, we mean it when we say it’s simple). Just add a use LiveViewEvents to your module and then override the update callback with the following implementation:

defmodule MyAppWeb.MyComponent do
  use MyAppWeb, :live_component
  use LiveViewEvents
  
  def update(assigns, socket) do
    socket = handle_info_or_assign(socket, assigns)
    
    {:ok, socket}
  end
end

Then, you can add a handle_info that deals with the messages you are ready to receive.

If you have an existing callback like the following:

def update(assigns, socket) do
  socket =
    socket
    |> assign(assigns)
    |> assign(:something, value_from(assigns))

  {:ok, socket}
end

The adaptation takes two steps:

  1. First, replace assign(assigns) with handle_info_or_assign(assigns).
  2. Refactor the second assign to take the value from socket.assigns and check if the dependant values changed with Phoenix.Component.changed?/2. This refactor will prevent potentially expensive code from running unless needed and prevent hard-to-find bugs in the code. See the note above about assigns only containing the properties whose values changed, and not all of them.

That Looks Cool, but is it Useful IRL?

Though it won’t be a full example, let’s explore one of the multiple cases where this can be handy. We might be building a complex form in which some fields require a non-standard input. That input would open a modal that would display several options. It can be anything, from an asset picker like WordPress does for images, or a contact selector, or anything custom you need for your app.

For this case, we’d need two components: one for handling the input and opening the modal with the selector, and the selector itself. These two components will send messages to each other.

defmodule MyAppWeb.InputWithModal do
  use MyAppWeb, :live_component

  def mount(socket) do
    socket =
      socket
      |> assign(:modal_opened?, false)
      |> assign(:selected_value, nil)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <div id={@id}>
      <button phx-click="open_modal" phx-target={@myself}>
        Open modal
      </button>
      <%= hidden_input @form, @field, value: @selected_value %>

      <.modal :if={@modal_opened?}>
        <.live_component module={MyAppWeb.Picker} id={"#{@id}__modal"} />
      </.modal>
    </div>
    """
  end

  def handle_event("open_modal", _params, socket) do
    socket = assign(socket, :modal_opened?, true)

    {:noreply, socket}
  end
end

The component above is barely functional: It can only open the modal and display the picker. Let’s take a look at the picker.

defmodule MyAppWeb.Picker do
  use MyAppWeb, :live_component
  use LiveViewEvents

  def handle_event("select", params, socket) do
    notify_to(socket.assigns.notify_to, socket.assigns.on_select, get_value_from_params(params))
  end
end

The exact implementation of the picker does not matter, we just care that we are sending an event back to the component. Let’s go back to the InputWithModal component and add the following code:

  1. Add use LiveViewEvents.
  2. Add custom update/2. Using handle_info_or_assign would run the handle_info/2 if needed.
  3. Modify the live_component/1 invocation adding the target and the event name, so the event can be sent.
  4. Add new handle_info/3 handling the event from the modal.

The final code looks like this:

defmodule MyAppWeb.InputWithModal do
  use MyAppWeb, :live_component
  use LiveViewEvents

  def mount(socket) do
    socket =
      socket
      |> assign(:modal_opened?, false)
      |> assign(:selected_value, nil)

    {:ok, socket}
  end

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

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <div id={@id}>
      <button phx-click="open_modal" phx-target={@myself}>
        Open modal
      </button>
      <%= hidden_input @form, @field, value: @selected_value %>

      <%= if @modal_opened? do %>
        <.modal>
          <.live_component module={MyAppWeb.Picker} id={"#{@id}__modal"} notify_to={{__MODULE__, @id}} event_name="value_selected" />
        </.modal>
      <% end %>
    </div>
    """
  end

  def handle_event("open_modal", _params, socket) do
    socket = assign(socket, :modal_opened?, true)

    {:noreply, socket}
  end

  def handle_info({"value_selected", value}, socket) do
    socket = assign(socket, modal_opened?: false, selected_value: value)

    {:noreply, socket}
  end
end

Now, when the Picker sends the event, the InputWithModal correctly handles it!

Get Started

This blog post is a short introduction to the new library. You can find more in-depth information in the README in live_view_events and its documentation.

Hope you enjoy it and simplifies your work!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box