How to achieve phx-target="<%= @myself %>" in HEEx?

I’m currently going through Pragmatic Studio’s LiveView 0.15 course using Phoenix 1.7.7. and I’m struggle to convert the following component to a HEEx component. I’ll tell you right now, I have no shame in my game so I’m leading with action and posting toxic code below (the second code block). I’ve looked all over and can’t seem to find how to set phx-target={@myself}. Needless to say, currently it always returns an ArgumentError “assign @myself not available in template.” In a week, hopefully I’ll look back at this and laugh, but folks, I’m struggling with this one. I looked through the docs and couldn’t find it, now I’m thinking it’s gone on to tunnel vision.

defmodule LiveViewStudioWeb.DeliveryChargeComponent do
  use LiveViewStudioWeb, :live_component

  alias LiveViewStudioWeb.SandboxCalculator
  import Number.Currency

  def mount(socket) do
    {:ok, assign(socket, zip: nil, charge: 0)}
  end

  def render(assigns) do
    ~L"""
    <form phx-change="calculate"
          **phx-target="<%= @myself %>">**
      <div class="field">
        <label for="zip">Zip Code:</label>
        <input type="text" name="zip" value="<%= @zip %>" />
        <span class="unit"><%= number_to_currency(@charge) %></span>
      </div>
    </form>
    """
  end

  def handle_event("calculate", %{"zip" => zip}, socket) do
    charge = SandboxCalculator.calculate_delivery_charge(zip)

    socket = assign(socket, zip: zip, charge: charge)

    send(self(), {:delivery_charge, charge})

    {:noreply, socket}
  end
end

This is what I’ve been able to compile

defmodule LiveViewStudioWeb.DeliveryChargeComponent do
  use Phoenix.Component

  import Number.Currency

  alias LiveViewStudio.SandboxCalculator

  def delivery_charge(assigns) do
    # compiles but is definitely wrong
    assigns = assign_new(assigns, :charge, fn -> 0 end)
    assigns = assign_new(assigns, :zip, fn -> nil end)

    ~H"""
    <div>
      <form phx-change="calculate" >
        <div class="field">
          <label for="zip">Zip Code:</label>
          <input type="text" name="zip" value={@zip} />
          <span class="unit"><%= number_to_currency(@charge) %></span>
        </div>
      </form>
    </div>
    """
  end

  def handle_event("calculate", %{"zip" => zip}, socket) do
    charge = SandboxCalculator.calculate_delivery_charge(zip)

    socket = assign(socket, zip: zip, charge: charge)

    send(self(), {__MODULE__, :delivery_charge, charge})

    {:noreply, socket}
  end
end

Hi @KevinGenus. There is a difference between a Component and a LiveComponent. You need to use the latter to get the @myself assign set.

Edit: Relevant docs here: Phoenix.LiveComponent — Phoenix LiveView v0.20.0

No shame whatsoever – we’ve all been there!

What does the live_component/0 function in your LiveViewStudioWeb module look like? It should include use Phoenix.LiveComponent and look something like this:

  def live_component do
    quote do
      use Phoenix.LiveComponent

      unquote(html_helpers())
    end
  end

Im confused by your issue.

<form phx-change="calculate" 

             phx-target={@myself} >
   
<div class="field">
      <label for="zip">Zip Code:</label>
      <input type="text" name="zip" value={@zip} />
      <span class="unit"><%= number_to_currency(@charge) %></span>
   </div>
</form>

Are you saying you have already tried setting the target here, but its not working?

If you ever see

something=<%= @something %>

It should just be straight up replaceable, in the same location as the below for most of the older guides

something={@something}

Appreciate the response. I didn’t realized I’d typed use Phoenix.Component, however, I did change it to use Phoenix.LiveComponent and the results were the same:

ArgumentError at GET /sandbox
assign @myself not available in template.

Please make sure all proper assigns have been set. If you are
calling a component, make sure you are passing all required
assigns as arguments.

Available assigns: [:zip, :__changed__, :charge]

Can you show the full code you have for that component now as well as how it is being called?

Yes.

I initially used a LEEx form with use LiveViewStudio, :live_component, mount/3 and render/1 as placed in the first code block. <form phx-change="calculate" phx-target="<%= @myself %>" had no issues.

I refactored to HEEx. The refactor included instantiating the component with the line<DeliveryChargeComponenet.delivery_charge zip={nil charge={0} />. In the component file, I did incorrectly use Phoenix.Component (and have since changed that to use Phoenix.LiveComponent) and removed mount/3 and render/1, and set <form> to <form phx-change="calculate" phx-target={@myself}>.

I’m just realizing I did not mention the console logs:

warning: render/1 was not implemented for LiveViewStudioWeb.DeliveryChargeComponent.

Make sure to either explicitly define a render/1 clause with a LiveView template:

It’s using HEEx, but I changed the function name anyway, from delivery_charge to render to remove that from the equation.

Hey, Ben, sure thing - it’s pasted below

sandbox_live.ex

defmodule LiveViewStudioWeb.SandboxLive do
  use LiveViewStudioWeb, :live_view

  alias LiveViewStudioWeb.SandboxCalculatorComponent
  alias LiveViewStudioWeb.QuoteComponent
  alias LiveViewStudioWeb.DeliveryChargeComponent

  def mount(_params, _session, socket) do
    {:ok, assign(socket, weight: nil, price: nil, delivery_charge: 0)}
  end

  def render(assigns) do
    ~H"""
    <h1>Build a Sandbox</h1>


    <div id="sandbox">
      <%= live_component @socket, SandboxCalculatorComponent,
                          id: 1,
                          coupon: 10.00 %>

      <%= if @weight do %>
        <DeliveryChargeComponent.render zip={nil} charge={0} />

        <QuoteComponent.render
          material="sand"
          weight={@weight}
          price={@price}
          hrs_until_expires="4"
          delivery_charge={@delivery_charge} />
      <% end %>
    </div>

    """
  end

  def handle_info({SandboxCalculatorComponent, :totals, weight, price}, socket) do
    socket = assign(socket, weight: weight, price: price)
    {:noreply, socket}
  end

  def handle_info({DeliveryChargeComponent, :delivery_charge, charge}, socket) do
    socket = assign(socket, :delivery_charge, charge)
    {:noreply, socket}
  end
end

components/deliver_charge_component.ex

defmodule LiveViewStudioWeb.DeliveryChargeComponent do
  use Phoenix.LiveComponent

  alias LiveViewStudio.SandboxCalculator
  import Number.Currency

  def render(assigns) do
    ~H"""

    <form phx-change="calculate" phx-target={@myself}>
      <div class="field">
        <label for="zip">Zip Code:</label>
        <input type="text" name="zip" value={@zip} phx-debounce={500} />
        <span class="unit"><%= number_to_currency(@charge) %></span>
      </div>
    </form>
    """
  end

  def handle_event("calculate", %{"zip" => zip}, socket) do
    charge = SandboxCalculator.calculate_delivery_charge(zip)

    socket = assign(socket, zip: zip, charge: charge)

    send(self(), {__MODULE__, :delivery_charge, charge})

    {:noreply, socket}
  end
end

You’re calling the live component as if it were a function component. You need to use live_component, just like you’re doing with SandboxCalculatorComponent.

3 Likes

The previously linked doc explains why:

“Note that @myself is not set for stateless components, as they cannot receive events.”

1 Like