How Phoenix.Component can handle events?

While searching and studying about live view I found this page of LiveComponent documentation, and reading more carefully about the “cost of a live component”, here: Phoenix.LiveComponent — Phoenix LiveView v0.18.18

Then it gives a not to do example, using a LiveComponent to wrap DOM events, like:

defmodule MyButton do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
    <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

So it preferable to use a functional component! Then, it was I did! I have this function component to wrap and style the native DOM button tag:

  attr :style, :string, values: ~w(primary secondary), required: true
  attr :submit, :boolean, default: false
  attr :icon, :atom, required: false, default: nil
  attr :class, :string, default: ""
  attr :rest, :global, doc: ~s(used for phoenix events like "phx-click" and "phx-target")

  slot :inner_block

  def button(assigns) do
    ~H"""
    <button
      type={if @submit, do: "submit", else: "button"}
      class={["btn", "btn-#{@style}", @class]}
      {@rest}
    >
      <.icon :if={@icon} name={@icon} />

      <.text :if={@style == "primary"} size="base" color="text-white-100">
        <%= render_slot(@inner_block) %>
      </.text>

      <.text :if={@style != "primary"} size="base" color="text-blue-80">
        <%= render_slot(@inner_block) %>
      </.text>
    </button>
    """
  end

To be reusable in any phoenix template with a predefined style. My question is, how this function component can handle events, in this case, phx-click?

I used the button like this: pescarte-plataforma/show.html.heex at main · peapescarte/pescarte-plataforma · GitHub

However no event is trigged as can be seen in this little video:

In short, it can’t. In order for a component to receive events it needs to be a stateful component that does use Phoenix.LiveComponent in its own module.

You generally also need to phx-target={@myself} to ensure that the events go to your component.

2 Likes

Function components do not handle events. Handle them in the parent liveview or livecomponent.

On a side note, this could be shorter:

<.text :if={@style == "primary"} size="base" color="text-white-100">
   <%= render_slot(@inner_block) %>
</.text>

<.text :if={@style != "primary"} size="base" color="text-blue-80">
  <%= render_slot(@inner_block) %>
</.text>

Could be:

<.text size="base" color={if @style == "primary", do: "text-white-100", else: "text-blue-80">
   <%= render_slot(@inner_block) %>
</.text>
1 Like

so the template needs to be a live view and not a raw html template, right? Ok, this explain a lot! I’ll try to do this!

Oh, thanks for this little tip, I implement so fast I didn’t noticed haha. Actually I really would like to se a :else and :if-else directive like vue in the future, although it’s not mandatory

HEEx templates can be both live view or not, depending the usage of the screen! Ok, I need to practice more this concept

but phx-target only works inside a live view template, right? then for a button component phx-target to @myself doesn’t make much sense as it need to handle dozens of different events in different screens, not related.

I now see the problem is I’m trying to handle an event on a “raw” html (heex) template and not a live view

I don’t really understand what this means. A functional component used by a root live view is still “in live view”. If you have a phx-click on a functional component then the event will simply go to the live view itself. There is no such thing as having a handle_event without live view at all, what would even be handling the event?

I mean: for a new phoenix project we have:

./lib/pescarte_web/
├── controllers
│   ├── app
│   │   └── researcher_controller.ex
│   ├── landing_controller.ex
│   └── login_controller.ex
├── layouts
│   ├── app.html.heex
│   └── root.html.heex
├── layouts.ex
└── templates
    ├── app
    │   ├── profile_html.ex
    │   └── researcher_html
    ├── error_html
    │   └── 404.html.heex
    ├── error_html.ex
    ├── landing_html
    │   └── show.html.heex
    ├── landing_html.ex
    └── login_html.ex

simplified. Then, those landing_html.ex files aren’t live views, are templates that can use components, but they do not handle events, right?

Example, here’s my login_html.ex file:

defmodule PescarteWeb.LoginHTML do
  use PescarteWeb, :html

  def show(assigns) do
    assigns = Map.put(assigns, :form, to_form(%{}, as: :user))

    ~H"""
    <main class="fish-bg h-full">
      <.simple_form for={@form} action={~p"/acessar"} class="login-form">
        <.text size="h3" color="text-black-80">
          Faça login para acessar a plataforma
        </.text>

        <fieldset class="login-fieldset">
          <.text_input field={@form[:cpf]} type="text" mask="999.999.999-99" label="CPF" required />
        </fieldset>

        <fieldset class="login-fieldset">
          <.text_input field={@form[:password]} type="password" label="Senha" required />
        </fieldset>

        <:actions>
          <div class="flex justify-between items-center">
            <.checkbox field={@form[:remember_me]} label="Mantanha-me conectado" id="remember" />

            <DesignSystem.link href={~p"/usuarios/recuperar_senha"} class="text-sm font-semibold">
              <.text size="sm">Esqueceu sua senha?</.text>
            </DesignSystem.link>
          </div>
        </:actions>

        <:actions>
          <.button style="primary" submit>
            <.text size="lg">Acessar</.text>
          </.button>
        </:actions>
      </.simple_form>
    </main>
    """
  end
end

This is not a live view, so cannot handle event and therefore does not define @myself. Am I wrong? To define a live view I need to use Phoenix.LiveView then implement mount/3 and handle_event/3

Hey @zoedsoupe you’re absolutely right, it’s only very recently that Phoenix started using heex for controllers and I basically totally forgot it does that now!

So back to your question: Yea you can only do handle_event and phx-target stuff on components used in a live view. If you do need to use a functional component between both live and traditional views you could probably pass in myself: nil as an assign, but also the button won’t really work.

Thanks for confirming this conception! there’s any part of the official documentation that explain this difference explicitly? I think the most important part is to understand that function components will not work with events on controllers templates.

For now i don’t need a component to trigger events on controller templates, a form action here is to save the session, so it can be safely a submit button to another route.

Not in the way you’re looking for that I see. That said if you read Components and HEEx — Phoenix v1.7.10 and the other docs it links to, you’ll notice that the word event never even appears. It’s only when you get to the live view docs do you start seeing events and handle_event.

1 Like