Pantrypoints Pantrypoints

Components

April 22, 2022 10 minutes  • 2013 words
Table of contents

@impl true

Tells LiveView to implement the callback

@impl true 
def render(assigns) do
  ~L"""
  whatever
  """

Stateful by adding id and pointing to the Component with @myself

Add id to the live_component to make it ‘stateful’

<%= live_component @socket, MessageComponent, id: 1 %>

Add @myself to the stateful form to make the Component handle the event

<form phx-submit="send-message" phx-target="<%= @myself %>" %>

Card Component with Form

defmodule CardComponent do

  def render(assigns) do
    ~H"\""
    <form phx-submit="..." phx-target={@myself}>
      <input name="title"><%= @card.title %></input>
      ...
    </form>
    "\""
  end

Listing of cards

<%= for card <- @cards do %>
  <%= live_component CardComponent, card: card, id: card.id, board_id: @id %>
<% end %>

Form submission triggers CardComponent.handle_event/3 which must update the card.

LiveView as the Source: self()

The component and the view run in the same process. So, sending an internal message from the LiveComponent to the parent LiveView is done by sending a message to self():

send self(), {:message_name, %{content_of_message}}
defmodule CardComponent do
  ...
  def handle_event("update_title", %{"title" => title}, socket) do
    send self(), {:updated_card, %{socket.assigns.card | title: title}}
    {:noreply, socket}

LiveView then receives this event using c:Phoenix.LiveView.handle_info/2:

defmodule BoardView do
  ...
  def handle_info({:updated_card, card}, socket) do
    # update the list of cards in the socket
    {:noreply, updated_socket}

The LiveView will send the updated card to the component.

Alternatively, the could be updated via broadcast by using Phoenix.PubSub to all users subscribed to it.

defmodule CardComponent do
  ...
  def handle_event("update_title", %{"title" => title}, socket) do
    message = {:updated_card, %{socket.assigns.card | title: title}}
    Phoenix.PubSub.broadcast(MyApp.PubSub, board_topic(socket), message)
    {:noreply, socket}
  end

  defp board_topic(socket) do
    "board:" <> socket.assigns.board_id

LiveComponent as the Source

In this case, LiveView must only fetch the card ids, then render each component only by passing an ID:

<%= for card_id <- @card_ids do %>
  <%= live_component CardComponent, id: card_id, board_id: @id %>
<% end %>

Each CardComponent will load its own card making expensive N queries, where N is the number of cards. So we use the c:preload/1 callback to make it efficient.

Once the card components are started, they can each manage their own card, without concern for the parent LiveView.

Components do not have a c:Phoenix.LiveView.handle_info/2 callback. Therefore, if you want to track distributed changes on a card, you must have LiveView receive those events and redirect them to the appropriate card.

For example, if card updates are sent to the “board:ID” topic, and that the board LiveView is subscribed to the said topic, one could do:

def handle_info({:updated_card, card}, socket) do
  send_update CardComponent, id: card.id, board_id: socket.assigns.id
  {:noreply, socket}

With Phoenix.LiveView.send_update/3, the CardComponent given by id will be invoked, triggering both preload and update callbacks, which will load the most up to date data from the database.

LiveComponent blocks

When live_component/3 (Phoenix.LiveView.Helpers.live_component/3) is invoked, it is also possible to pass a do/end block:

<%= live_component GridComponent, entries: @entries do %>
  <% entry -> %>New entry: <%= entry %>
<% end %>

The do/end will be available in an assign named @inner_block.

You can render its contents by calling render_block with the assign itself and a keyword list of assigns to inject into the rendered content. For example, the grid component above could be implemented as:

defmodule GridComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"\""
    <div class="grid">
      <%= for entry <- @entries do %>
        <div class="column">
          <%= render_block(@inner_block, entry) %>
        </div>
      <% end %>
    </div>
    "\""
  end
end

Where the entry variable was injected into the do/end block.

Note the @inner_block assign is also passed to c:update/2 along all other assigns. So if you have a custom update/2 implementation, make sure to assign it to the socket like so:

def update(%{inner_block: inner_block}, socket) do
  {:ok, assign(socket, inner_block: inner_block)}
end

Components run inside the LiveView process, but may have their own state and event handling. They are either stateless or stateful.

Setting

Phoenix.LiveComponent

Calling

Phoenix.LiveView.Helpers.live_component/3` in a parent LiveView

Simplest Stateless Component: render/1

defmodule HeroComponent do
use MyAppWeb, :live_component

def render(assigns) do
  ~L\"""
  <div><%= @content %></div>
  \"""
end
end

Passing Data to Component from Parent LiveView

<%= live_component HeroComponent, content: @content %>

Stateless components

live_component/3 calls three things:

mount(socket) -> update(assigns, socket) -> render(assigns)
  • mount/1 sets the initial state
  • update/2 sets all of the assigns given to live_component/3. If c:update/2 is not defined, all assigns are simply merged into the socket
  • render/1 renders all assigns

A stateless component is always mounted, updated, and rendered whenever the parent template changes.

Stateful Components via:id

Components become stateful by passing an :id assign.

<%= live_component HeroComponent, id: :hero, content: @content %>

They are identified by the component module and their ID. Therefore, two different component modules with the same ID are different components. This means we can often tie the component ID to some application based ID:

<%= live_component UserComponent, id: @user.id, user: @user %>

The given :id is not necessarily used as the DOM ID. If you want to set a DOM ID, it is your responsibility to set it when rendering:

defmodule UserComponent do
use Phoenix.LiveComponent

def render(assigns) do
  ~L\"""
  <div id="user-<%= @id %>" class="user"><%= @user.name %></div>
  \"""
end
end

Stateful components should only a single root element in the HTML template.

In stateful components, c:mount/1 is called only once when it is first rendered. For each rendering, the optional c:preload/1 and c:update/2 callbacks are called before c:render/1.

So on first render, the following callbacks will be invoked:

preload(list_of_assigns) -> mount(socket) -> update(assigns, socket) -> render(assigns)

On subsequent renders, these callbacks will be invoked:

preload(list_of_assigns) -> update(assigns, socket) -> render(assigns)

Component Events

For a client event to reach a component, the tag must have phx-target. Use @myself to send it to the component assign.

<a href="#" phx-click="say_hello" phx-target="<%= @myself %>">
Say hello!
</a>

@myself is not set for stateless components. To target another component, pass an ID or a class selector to any element inside the targeted component. Only the diff of the component is sent to the client, making them extremely efficient.

<a href="#" phx-click="say_hello" phx-target="#user-13">
Say hello!
</a>

Any valid query selector for phx-target is supported, provided that the matched nodes are children of a LiveView or LiveComponent, for example to send the close event to multiple components:

<a href="#" phx-click="close" phx-target="#modal, #sidebar">
Dismiss
</a>

Managing state

The parent LiveView and its LiveComponent should not work on 2 different copies of the state. Only one is the truth.

In this scenario, each LiveView Card has a form to update the card title directly:

defmodule CardComponent do
use Phoenix.LiveComponent

def render(assigns) do
  ~L\"""
  <form phx-submit="..." phx-target="<%= @myself %>">
    <input name="title"><%= @card.title %></input>
    ...
  </form>
  \"""
end

...
end

LiveView as the source of truth

The board LiveView will fetch all the cards in a board, calling live_component/3 for each card, passing the card struct as argument to CardComponent:

<%= for card <- @cards do %>
<%= live_component CardComponent, card: card, id: card.id, board_id: @id %>
<% end %>

When the user submits the form, CardComponent.handle_event/3 the Component sends an internal message to its parent Liveview via self():

defmodule CardComponent do
...
def handle_event("update_title", %{"title" => title}, socket) do
  send self(), {:updated_card, %{socket.assigns.card | title: title}}
  {:noreply, socket}
end
end

The LiveView receives this event using Phoenix.LiveView.handle_info/2:

defmodule BoardView do
...
def handle_info({:updated_card, card}, socket) do
  # update the list of cards in the socket
  {:noreply, updated_socket}
end
end

The parent LiveView will be re-rendered, sending the updated card to the Component, or by broadcasting via Phoenix.PubSub. This needs the LiveView to subscribe to the board:<ID> topic

defmodule CardComponent do
...
def handle_event("update_title", %{"title" => title}, socket) do
  message = {:updated_card, %{socket.assigns.card | title: title}}
  Phoenix.PubSub.broadcast(MyApp.PubSub, board_topic(socket), message)
  {:noreply, socket}
end

defp board_topic(socket) do
  "board:" <> socket.assigns.board_id
end
end

LiveComponent as the source of truth

The board LiveView no longer fetches the card structs from the database. Instead, it only fetches the card ids, then render each component by passing an ID:

<%= for card_id <- @card_ids do %>
  <%= live_component CardComponent, id: card_id, board_id: @id %>
<% end %>

Each CardComponent will load its own card. You should use preload/1

To broadcast changes on a card, the parent LiveView must receive those events and redirect them to the appropriate card.

def handle_info({:updated_card, card}, socket) do
send_update CardComponent, id: card.id, board_id: socket.assigns.id
{:noreply, socket}
end

Phoenix.LiveView.send_update/3 invokes the CardComponent given by id. This triggers preload and update callbacks.

LiveComponent blocks

do/end block is possible with live_component/3

<%= live_component GridComponent, entries: @entries do %>
New entry: <%= @entry %>
<% end %>

The do/end will be available in an assign named @inner_block. You can render its contents by calling render_block with the assign itself and a keyword list of assigns to inject into the rendered content. For example, the grid component above could be implemented as:

defmodule GridComponent do
use Phoenix.LiveComponent

def render(assigns) do
  ~L\"""
  <div class="grid">
    <%= for entry <- @entries do %>
      <div class="column">
        <%= render_block(@inner_block, entry: entry) %>
      </div>
    <% end %>
  </div>
  \"""
end
end

Where the :entry assign was injected into the do/end block.

The @inner_block assign is also passed to c:update/2 along all other assigns. So if you have a custom update/2 implementation, make sure to assign it to the socket like so:

def update(%{inner_block: inner_block}, socket) do
{:ok, assign(socket, inner_block: inner_block)}
end

The above approach is the preferred one when passing blocks to do/end. However, if you are outside of a .leex template and you want to invoke a component passing a do/end block, you will have to explicitly handle the assigns by giving it a -> clause:

live_component GridComponent, entries: @entries do
  new_assigns -> "New entry: " <> new_assigns[:entry]
end

Live patches and live redirects

A template rendered inside a component can use:

  • Phoenix.LiveView.Helpers.live_patch/2 : this is always handled by the parentLiveView, as components do not provide handle_params
  • Phoenix.LiveView.Helpers.live_redirect/2

Cost of stateful components

Keep only the assigns necessary in each component.

Avoid passing all of LiveView’s assigns when rendering a component:

    <%= live_component MyComponent, assigns %>

Instead pass only the keys that you need:

<%= live_component MyComponent, user: @user, org: @org %>

The view and the component will share the same copies of the @user and @org assigns.

Avoid using stateful components to provide abstract DOM components

A good LiveComponent encapsulates application concerns and not DOM functionality. If your page shows products, you can encapsulate those products in a component. This component may have many buttons and events inside.

Do not write a component that is simply encapsulating generic DOM components:

defmodule MyButton do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L\"""
    <button class="css-framework-class" phx-click="click">
      <%= @text %>
    </button>
    \"""
  end

  def handle_event("click", _, socket) do
    _ = socket.assigns.on_click.()
    {:noreply, socket}
  end
end

Instead, create a function:

def my_button(text, click) do
  assigns = %{text: text, click: click}

  ~L\"""
  <button class="css-framework-class" phx-click="<%= @click %>">
      <%= @text %>
  </button>
  \"""
end

Limitations

  1. Components require at least one HTML tag

Components must only contain HTML tags at their root. At least one HTML tag must be present. It is not possible to have components that render only text or text mixed with tags at the root.

  1. Components must always be change tracked

If you render a component inside form_for the component ends up enclosed by the form markup, where LiveView cannot track it.

<%= form_for @changeset, "#", fn f -> %>
  <%= live_component SomeComponent, f: f %>
<% end %>

This causes an error:

    ** (ArgumentError) cannot convert component SomeComponent to HTML.
    A component must always be returned directly as part of a LiveView template

Solve this without anonymous functions:

<%= f = form_for @changeset, "#" %>
  <%= live_component SomeComponent, f: f %>
</form>

This issue can also happen with other helpers, such as content_tag:

<%= content_tag :div do %>
  <%= live_component SomeComponent, f: f %>
<% end %>

So do not use content_tag. Instead, use LiveEEx to build the markup.

Form Bindings

These are in forms that intercept JS events.

Phoenix.LiveView.Socket is a server-side struct that has all the data. It has an assigns key for current data.

The template exposes the data of the Socket to the browser.

Data is set to the Socket with Phoenix.Component.assign/2 and Phoenix.Component.assign/3

The browser accesses the Socket data in 2 ways:

  • With LiveView as socket.assigns.name_of_assign_data
  • With Heex as @name_of_assign_data
Follow Us! →

We're creating a new Economic System from a new Economic Science! Please support us by leaving your email or leaving a comment above.