Function not being called on button click

Hi all!

So, in my component I pass a show_driver_avail_info? prop like so:

 prop show_driver_avail_info?, :boolean

And I have this button:

<button type="button" class="rev-Button rev-Button--secondary rev-Button--tiny" phx-click="show_driver_availability_info">See Driver Availability Info</button>

That has this click function:

  def handle_event("show_driver_availability_info", _, socket) do
    IO.puts("show_driver_availability_info being called")
    IO.inspect(socket.assigns.show_driver_avail_info?)
    socket = assign(socket, :show_driver_avail_info?, true)
    {:noreply, socket}
  end

And I have these components, each of which are rendered if show_driver_avail_info is true, and which take in a truck_load and pass it through various other functions to get the info rendered correctly:

<CardInfo :if={{ @show_driver_avail_info? }} label="Time Since Available" title={{ get_time_since_available(@truck_load) }}  small />
<CardInfo :if={{ @show_driver_avail_info? }} label="Current Status" title={{ get_driver_current_status(@truck_load) }} small />
<CardInfo :if={{ @show_driver_avail_info? }} label="Time Until Cycle Reset" title={{ get_time_until_cycle_reset(@truck_load) }} small />
  

I assign the show_driver_avail_info? prop like so in the template file:

<%= live_component @socket, TSSWeb.DispatchHomeLive.Kanban.Column.Component, truck_loads: [], orders: @scheduled, well: @well, show_driver_avail_info?: @show_driver_avail_info? %>

My issue is that whenever I click the button, I can’t see my IO.inspect, and I get this error:

[error] GenServer #PID<0.1218.0> terminating
** (UndefinedFunctionError) function TSSWeb.DispatchHomeLive.handle_event/3 is undefined or private

I am pretty certain I am defining the function in the correct file, so I’m not sure why it’s coming back as undefined or private.

I really appreciate any help!! Thanks

Going by the error you’re receiving, it could either by a typo in your handle_event/3 function name (name mismatch between your .leex and .ex files).

Another possibility, might be that you are not defining your handle_event/3 function in the correct file. For instance, is it defined in your index.ex file or your component file? My initial guess is that it is trying to look in your index.ex file or equivalent and is not finding the handle_event/3 function because it has been defined in your component (when the actual click of the button occurs in your index.ex).

So, I would check where your button click occurs and check that you have defined your handle_event/3 function there and that there aren’t any typos. Typically, the error messages are spot on.

1 Like

Try adding the phx-target to your button:

<button 
  type="button" 
  class="rev-Button rev-Button--secondary rev-Button--tiny" 
  phx-click="show_driver_availability_info" 
  phx-target={{ @myself }}
>See Driver Availability Info</button>

But, since this looks like a Surface component, you could do this instead:

<button 
  type="button" 
  class="rev-Button rev-Button--secondary rev-Button--tiny" 
  :on-click="show_driver_availability_info" 
>See Driver Availability Info</button>

And Surface will add the target for yourself.

Without a target, events will reach the main liveview, which is what the error message said. See Targeting Component Events in the LiveView docs & Events section in Surface docs.

1 Like

hmmm I tried both of these and it’s still giving the same error: [error] GenServer #PID<0.1218.0> terminating ** (UndefinedFunctionError) function TSSWeb.DispatchHomeLive.handle_event/3 is undefined or private

I checked for typos and everything looks right, and the handle_event/3 function is in the file where the button click occurs so I’m not really sure why I’m still getting the [error] GenServer #PID<0.1218.0> terminating ** (UndefinedFunctionError) function TSSWeb.DispatchHomeLive.handle_event/3 is undefined or private error…

Please show your TSSWeb.DispatchHomeLive module code.

1 Like

The full error output should also say what arguments were passed to handle_event/3 - can you post that also?

1 Like

sure thing, here it is:

[error] GenServer #PID<0.1837.0> terminating
** (UndefinedFunctionError) function TSSWeb.DispatchHomeLive.handle_event/3 is undefined or private
    (tss 0.0.1) TSSWeb.DispatchHomeLive.handle_event("show_driver_availability_info", %{"value" => ""}, #Phoenix.LiveView.Socket<assigns: %{__context__: %{}, __surface__: %{}, breadcrumb_trail: [[name: "Home", url: "/"], [name: "Wells", url: "/wells_board"], [name: "Dispatch Board - Argent Barksdale East 29-17 4LS/4WA/6LS/6WA/7LS/7WA", ...]], connected?: true, current_user: %TSS.User{location_services_network: true, device_token_type: "fcm", ...}, flash: %{}, forecast_data: %{...}, ...}, changed: %{}, endpoint: TSSWeb.Endpoint, id: "phx-FnzYcT9D2WJMWSYj", parent_pid: nil, root_pid: #PID<0.1837.0>, router: TSSWeb.Router, view: TSSWeb.DispatchHomeLive, ...>)
    (phoenix_live_view 0.15.4) lib/phoenix_live_view/channel.ex:338: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 0.4.2) /Users/amber/Desktop/TSS/TSS/web/deps/telemetry/src/telemetry.erl:262: :telemetry.span/3
    (phoenix_live_view 0.15.4) lib/phoenix_live_view/channel.ex:203: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 3.14.1) gen_server.erl:689: :gen_server.try_dispatch/4
    (stdlib 3.14.1) gen_server.erl:765: :gen_server.handle_msg/6
    (stdlib 3.14.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "4", payload: %{"event" => "show_driver_availability_info", "type" => "click", "value" => %{"value" => ""}}, ref: "8", topic: "lv:phx-FnzYcT9D2WJMWSYj"}
[error] GenServer #PID<0.1882.0> terminating

Usually when this issue comes up for me, it’s because of the targeting, like @sfusato was mentioning earlier. I haven’t used Surface, but for a Live Component, adding phx-target="<%= @myself %>" seems like it should resolve the issue if your handle_event/3 is in the same file.

This is the error I’m getting after adding that target in :sweat:

Is the name of your component DispatchHomeLive? You can see in the error message that the app is looking for the handle_event there. So if that’s not the component, then the problem is with the phx-target. My guess is there is something we are not seeing because you are only posting snippets. If you could post a full repo where the issue was happening, it would be easier to troubleshoot.

Gotcha, I was hesitant to post the full components bcs they’re pretty huge but here they are, thanks so much for the help!!

This is kanban_card_component.ex

defmodule TSSWeb.DispatchHomeLive.Kanban.Card.Component do
  @moduledoc """
  Card component for display truck loads on the dispatch kanban
  """
  use Surface.Component

  alias TSSWeb.Live.Components.{
    Grid,
    CardHeader,
    CardInfo,
    CardStat,
    CardFooterLink,
    CardStatTripTime
  }

  alias TSS.{
    Helpers.Formatters,
    Conversions.Timezone,
    Conversions.Weight,
    TruckLoads.Calls
  }

  alias TSSWeb.Router.Helpers, as: Routes
  alias TSSWeb.SharedHelpers, as: Shared

  prop truck_load, :any
  prop well, :any
  prop current_leg_eta, :map
  prop total_eta, :map
  prop trip_time, :any
  prop show_reroute_button?, :boolean
  prop show_change_pp_button?, :boolean
  prop show_start_over_button?, :boolean
  prop show_drop_trailer_button?, :boolean
  prop show_call_to_well_button?, :boolean
  prop show_unload_button?, :boolean
  prop show_trip_times?, :boolean
  prop show_driver_avail_info?, :boolean

  def mount(socket) do
    {:ok, Surface.init(socket)}
  end

  def update(%{truck_load: %{id: truck_load_id}} = assigns, socket) do
    trip_time = TSS.TripTimes.TruckLoads.get_trip_time_message(truck_load_id)
    assigns = Map.put(assigns, :trip_time, trip_time)
    {:ok, assign(socket, assigns)}
  end

  def render(assigns) do
    ~H"""
    <div class={{ dispatch_card_class(@truck_load) }}>
      <Grid type="row">
        <CardHeader
          :if={{ not is_nil(@truck_load.sand_type) }}
          background_color={{ @truck_load.sand_type_background_color }}
          text_color={{ @truck_load.sand_type_text_color }}
          title={{ @truck_load.sand_type }}
          info={{ render_measured_weight_info(@truck_load.measured_weight) }}
        />
        <Grid type="column" opts={{ [large: 7] }} class="DispatchCard-info">
          <CardInfo
            label="Truck Number"
            title={{ "#{@truck_load.driver_first_name} #{@truck_load.driver_last_name}" }}
            info={{ "##{@truck_load.truck_number}" }}
          />
          <CardInfo label="Carrier" title={{ @truck_load.carrier_name }} small />
          <CardInfo
            label="Phone"
            title={{ Formatters.format_phone_number(@truck_load.driver_phone_number) }}
            small
          />
          <CardInfo label="Trailer" title={{ Shared.get_string(@truck_load.trailer_type, :name) }} small />
          <CardInfo label="Pull Point" title={{ @truck_load.pull_point_name }} small />
          <button type="button" class="rev-Button rev-Button--secondary rev-Button--tiny"  click="show_driver_availability_info" phx-target={{ @show_driver_avail_info? }}>See Driver Availability Info</button>
          <CardInfo :if={{ @show_driver_avail_info? }} label="Time Since Available" title={{ get_time_since_available(@truck_load) }}  small />
          <CardInfo :if={{ @show_driver_avail_info? }} label="Current Status" title={{ get_driver_current_status(@truck_load) }} small />
          <CardInfo :if={{ @show_driver_avail_info? }} label="Time Until Cycle Reset" title={{ get_time_until_cycle_reset(@truck_load) }} small />
          <CardStatTripTime
            id={{ "trip_time_card_#{@truck_load.id}" }}
            trip_time={{ @trip_time }}
            has_active_fulfillment={{ active_fulfillment?(assigns) }}
          />
        </Grid>
        <Grid type="column" opts={{ [large: 5] }} class="DispatchCard-info">
          <CardStat
            title={{ TSSWeb.TruckLoadView.event_badge(@truck_load) }}
            label="Time Elapsed"
            value={{ get_time_elapsed(@truck_load) }}
            icon="clock"
          />
          <CardStat
            :if={{ show_old_eta?(assigns, :current_leg) }}
            label="Current Leg"
            icon="watch"
            value={{ eta_value(@current_leg_eta) }}
            alert={{ eta_alert(@current_leg_eta) }}
            tooltip={{ eta_tooltip(@current_leg_eta) }}
          />
          <CardStat
            :if={{ show_old_eta?(assigns, :total_eta) }}
            label="Total"
            icon="watch"
            value={{ eta_value(@total_eta) }}
            alert={{ eta_alert(@total_eta) }}
            tooltip={{ eta_tooltip(@total_eta) }}
          />
          <CardStat
            :if={{ @truck_load.bol_status }}
            icon="file-text"
            label="BOL"
            value="BOL"
            class={{ bol_class(@truck_load) }}
          />
          <CardStat
            icon="user"
            label="Driver Duty Status"
            value={{ Recase.to_title(@truck_load.driver_duty_status) }}
          />
          <CardStat
            :if={{ show_trip_break?(@truck_load.current_trip_break) }}
            icon={{ trip_break_icon(@truck_load.current_trip_break) }}
            label={{ trip_break_label(@truck_load.current_trip_break) }}
            value={{ trip_break_value(@truck_load.current_trip_break) }}
          />
        </Grid>
      </Grid>
      <div class="rev-Card-footer">
        <Grid type="row" class="DispatchCard-footer">
          <Grid type="column" opts={{ [small: 9] }} class="u-flexAlignStart">
            <CardFooterLink
              :if={{ @show_start_over_button? }}
              to={{Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive.AddTruckLoad, @well.id,
                truck_load_id: @truck_load.id
              )}}
              title="Start Over"
              icon="arrow-left-circle"
            />
            <CardFooterLink
              :if={{ @show_change_pp_button? }}
              title="Change Pull Point"
              click="change_procurement_modal:on_open"
              truck_load_id={{ @truck_load.id }}
              icon="arrow-up-circle"
              class="ProcurementModal"
            />
            <CardFooterLink
              :if={{ @show_reroute_button? && active_fulfillment?(assigns) }}
              title="Reroute"
              click="reroute_truck_load_modal:on_open"
              truck_load_id={{ @truck_load.id }}
              icon="refresh-cw"
            />
            <CardFooterLink
              :if={{ @truck_load.can_cancel? }}
              click="cancel_truck_load"
              truck_load_id={{ @truck_load.id }}
              title="Cancel"
              confirm="You cannot undo this action. Are you sure you want to CANCEL AND PERMANENTLY DELETE this truckload? Your name will be tied to the deleted truckload for tracking and user management purposes."
              icon="x-circle"
            />
            <CardFooterLink
              :if={{ get_in(@truck_load, [:current_trip_break, :type]) == "break" }}
              title="Resume"
              click="end_trip_break"
              truck_load_id={{ @truck_load.id }}
              icon="play-circle"
            />
            <CardFooterLink
              :if={{ get_in(@truck_load, [:current_trip_break, :type]) == "breakdown" }}
              title="Report Truck Back Up"
              click="end_trip_break"
              truck_load_id={{ @truck_load.id }}
              icon="play-circle"
            />
            <CardFooterLink
              :if={{ is_nil(get_in(@truck_load, [:current_trip_break, :type])) }}
              title="Pause"
              click="trip_break_modal:on_open"
              truck_load_id={{ @truck_load.id }}
              icon="pause-circle"
            />
            <CardFooterLink
              :if={{ is_nil(get_in(@truck_load, [:current_trip_break, :type])) }}
              title="Report Truck Down"
              click="report_truck_down"
              truck_load_id={{ @truck_load.id }}
              icon="alert-circle"
            />
            <CardFooterLink
              :if={{ @show_drop_trailer_button? && @truck_load.status == "trailer_dropped" }}
              to={{ Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive.RequestTrailerPickup, @truck_load.id) }}
              title="Request Trailer Pickup"
              truck_load_id={{ @truck_load.id }}
              icon="upload"
            />
            <CardFooterLink
              :if={{ @show_drop_trailer_button? && @truck_load.status == "pickup_requested" }}
              title="Trailer Picked Up"
              icon="upload"
              click="dropped_trailer:trailer_picked_up"
              confirm="Are you sure you want to mark this trailer as picked up?"
              truck_load_id={{ @truck_load.id }}
            />
            <CardFooterLink
              :if={{ @show_drop_trailer_button? && @truck_load.status not in ["pickup_requested", "trailer_dropped"] }}
              title="Drop Trailer"
              icon="download"
              click="dropped_trailer:drop_trailer"
              confirm="Are you sure you want to mark this trailer as dropped?"
              truck_load_id={{ @truck_load.id }}
            />
            <CardFooterLink
              :if={{ @show_call_to_well_button? and Calls.can_call?(@truck_load) }}
              title="Call To Well"
              confirm="Are you sure you want to call this driver to the well?"
              click="call_to_well:call"
              truck_load_id={{ @truck_load.id }}
              disabled={{ !show_call_to_well?(@truck_load) }}
              icon="phone"
            />
            <CardFooterLink
              to={{ Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive.SlipSeatModal, @truck_load.id) }}
              title="Slip Seat"
              icon="shuffle"
            />
            <CardFooterLink
              :if={{ @show_unload_button? }}
              to={{ Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive.UnloadModal, @truck_load.id) }}
              title="Unload"
              icon="check-square"
            />
          </Grid>
          <Grid type="column" opts={{ [small: 3] }}>
            <a
              href={{Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive.ViewTruckLoad, @well.id, @truck_load.id,
                referrer: Routes.live_path(TSSWeb.Endpoint, TSSWeb.DispatchHomeLive, @well.id, landing: :kanban)
              )}}
              title="View Details"
              class="DispatchCard-footerLink"
            >#{{ @truck_load.id }}</a>
          </Grid>
        </Grid>
      </div>
    </div>
    """
  end

  defp render_measured_weight_info(nil), do: ""

  defp render_measured_weight_info(weight) do
    measured_weight_lbs = Formatters.format_as_pounds(weight)
    measured_weight_tons = Weight.convert_pounds_to_tons_and_round(weight, 2)
    "#{measured_weight_lbs} lbs (#{measured_weight_tons} tons) "
  end

  defp active_fulfillment?(%{truck_load: %{active_fulfillment: nil}}), do: false
  defp active_fulfillment?(_), do: true

  defp show_call_to_well?(truck_load) do
    latest_call_status = Calls.latest_call_status(truck_load)
    is_nil(latest_call_status) || latest_call_status == "unskipped"
  end

  defp show_trip_break?(nil), do: false
  defp show_trip_break?(%{expected_duration: nil, type: "break"}), do: false
  defp show_trip_break?(_), do: true

  defp trip_break_label(%{type: "breakdown"}), do: "Time down"
  defp trip_break_label(_), do: "Time on break"

  defp trip_break_icon(%{type: "breakdown"}), do: "alert-circle"
  defp trip_break_icon(_), do: "pause-circle"

  defp trip_break_value(nil), do: nil

  defp trip_break_value(%{expected_duration: nil, type: "break"}), do: nil

  defp trip_break_value(%{type: "breakdown", started_at: started_at}) do
    started_at
    |> Timezone.diff_current_time_in_minutes()
    |> abs()
    |> format_minutes()
  end

  defp trip_break_value(%{expected_duration: expected_duration, started_at: started_at}) do
    mins_since_start = started_at |> Timezone.diff_current_time_in_minutes() |> abs()
    remaining_time = expected_duration - mins_since_start

    if remaining_time < 0 do
      format_minutes(0)
    else
      format_minutes(remaining_time)
    end
  end

  defp format_minutes(minutes) do
    Formatters.humanize_time(minutes, :minutes, :minutes) <> " (hh:mm)"
  end

  defp bol_class(%{bol_status: bol_status}) do
    "DispatchCard-iconStat--#{bol_status}"
  end

  defp show_old_eta?(%{current_leg_eta: %{eta: eta}} = assigns, :current_leg)
       when not is_nil(eta) do
    active_fulfillment?(assigns)
  end

  defp show_old_eta?(%{total_eta: %{eta: eta}} = assigns, :total_eta)
       when not is_nil(eta) do
    active_fulfillment?(assigns)
  end

  defp show_old_eta?(_, _), do: false

  defp eta_value(%{text: text}), do: "#{text} (hh:mm)"
  defp eta_value(_), do: nil

  defp eta_alert(%{driver_idle?: driver_idle?}), do: driver_idle?
  defp eta_alert(_), do: nil

  defp eta_tooltip(%{tooltip: tooltip}), do: tooltip
  defp eta_tooltip(_), do: nil

  defp get_time_elapsed(truck_load) do
    time_elapsed = TSSWeb.DispatchHome.View.get_time_elapsed_since_entered_column(truck_load)

    "#{time_elapsed} (hh:mm)"
  end

  defp get_time_since_available(truck_load) do
    driver = TSS.DriverStatuses.get_driver_by_driver_id(truck_load.driver_id)

    if !is_nil(driver) do
      time_elapsed = Formatters.decorate_time_since_available(driver)
      "#{time_elapsed}"
    else
      "driver not available"
    end
  end

  defp get_driver_current_status(truck_load) do
    driver = TSS.DriverStatuses.get_driver_status_by_driver_id(truck_load.driver_id)

    if !is_nil(driver) do
      status = Formatters.decorate_driver_current_status(driver)
      "#{status}"
    else
      "driver not available"
    end
  end

  defp get_time_until_cycle_reset(truck_load) do
    driver = TSS.DriverStatuses.get_driver_status_by_driver_id(truck_load.driver_id)
    remaining_cycle_time = Formatters.decorate_remaining_cycle_time(driver)

    "#{remaining_cycle_time}"
  end

  defp dispatch_card_class(truck_load) do
    default_class = "DispatchCard rev-Card"

    if is_nil(truck_load.current_trip_break) do
      "#{default_class} DispatchCard-status--#{truck_load.status}"
    else
      "#{default_class} DispatchCard-status--#{truck_load.current_trip_break.type}"
    end
  end
end

And this is DispatchHomeLive where the handle_event is (all the other handle_event's for components in the kanban_card_component.ex file are in here and working fine, which is why I believe this handle_event belongs here…I tried moving it to the actual DispatchHomeLive component and still got the same [error] GenServer #PID<0.1218.0> terminating ** (UndefinedFunctionError) function TSSWeb.DispatchHomeLive.handle_event/3 is undefined or private error) :

defmodule TSSWeb.DispatchHomeLive.Kanban.Component do
  @moduledoc """
  Renders the kanban board / "Active truck loads" tab on the Dispatch Board of a
  given well.

  "Active" means any truckload that has had a job request sent and is not yet
  unloaded.
  """
  use TSSWeb, :live_component

  alias TSSWeb.{
    DispatchHomeLive.ActionValidators,
    DispatchHomeLive.RerouteTruckLoadModal,
    DispatchHomeLive.ChangeProcurementModal,
    DispatchHomeLive.DroppedTrailer,
    DispatchHomeLive.CallToWell,
    DispatchHomeLive.TripBreakModal,
    DispatchHome
  }

  alias TSS.{
    DispatchOrders,
    TruckLoad,
    TruckLoads,
    TruckLoads.Calls,
    TripBreak,
    TripBreaks,
    Orders,
    Order
  }

  @init_assigns %{
    truck_loads: [],
    truck_load: nil,
    modal_assigns: nil,
    dispatched_loads: [],
    loaded_loads: [],
    on_site_loads: [],
    scheduled: [],
    # a hidden/unused field to allow for re-rendering manually:
    last_updated_timestamp: nil,
    # A map of truck_load ids => eta timestamps
    load_etas: %{},
    reroute_truck_load_modal_open?: false,
    well_options: [],
    procurement_modal_open?: false,
    trip_break_modal_open?: false,
    procurement_options: [],
    show_driver_avail_info?: false
  }

  def render(assigns) do
    Phoenix.View.render(DispatchHome.View, "kanban.html", assigns)
  end

  def update(
        %{
          data: data,
          well: well,
          show?: show?,
          current_user: current_user
        },
        socket
      ) do
    socket =
      socket
      |> init_assigns(@init_assigns)
      |> assign_data(data)
      |> assign(:well, well)
      |> assign(:show?, show?)
      |> assign(:current_user, current_user)

    {:ok, socket}
  end

  defp assign_data(socket, nil), do: socket
  defp assign_data(socket, data), do: assign(socket, data)

  def handle_event("reroute_truck_load_modal:" <> action, attrs, socket) do
    RerouteTruckLoadModal.Actions.handle_event(action, attrs, socket)
  end

  def handle_event("dropped_trailer:" <> action, attrs, socket) do
    DroppedTrailer.Actions.handle_event(action, attrs, socket)
  end

  def handle_event("call_to_well:" <> action, attrs, socket) do
    CallToWell.Actions.handle_event(action, attrs, socket)
  end

  def handle_event("change_procurement_modal:" <> action, attrs, socket) do
    ChangeProcurementModal.Actions.handle_event(action, attrs, socket)
  end

  def handle_event("trip_break_modal:" <> action, attrs, socket) do
    TripBreakModal.Actions.handle_event(action, attrs, socket)
  end

  def handle_event(
        "report_truck_down",
        %{"truck-load-id" => truck_load_id},
        %{assigns: %{current_user: user}} = socket
      ) do
    truck_load = TruckLoads.get_truck_load!(truck_load_id)

    ActionValidators.validate_user_current_location_for_trip_break(
      truck_load,
      "breakdown",
      socket,
      fn user_current_location ->
        attrs =
          TripBreaks.append_coords_to_trip_break_attrs(
            %{
              truck_load_id: truck_load.id,
              driver_id: truck_load.driver_id,
              creator_id: user.id,
              type: "breakdown",
              started_at: DateTime.utc_now()
            },
            user_current_location
          )

        case TripBreaks.create_trip_break(attrs) do
          {:error, _changeset} ->
            {:noreply,
             put_flash(socket, :error, "Error Reporting Truckload ##{truck_load_id} down!")}

          {:ok, %TripBreak{} = _trip_break} ->
            {:noreply, put_flash(socket, :success, "Truckload ##{truck_load_id} reported down!")}
        end
      end
    )
  end

  def handle_event(
        "end_trip_break",
        %{"truck-load-id" => truck_load_id},
        socket
      ) do
    trip_break = TripBreaks.get_current_trip_break(truck_load_id)

    case TripBreaks.end_trip_break(trip_break) do
      {:error, _changeset} ->
        {:noreply,
         put_flash(socket, :error, "Error Reporting Truckload ##{truck_load_id} back up!")}

      {:ok, %TripBreak{} = _trip_break} ->
        {:noreply, put_flash(socket, :success, "Truckload ##{truck_load_id} reported back up!")}
    end
  end

  def handle_event(
        "cancel_truck_load",
        %{"truck-load-id" => truck_load_id},
        %{assigns: %{current_user: user}} = socket
      ) do
    with %TruckLoad{} = truck_load <- TruckLoads.get_truck_load(truck_load_id),
         {:ok, truck_load} <- TruckLoads.cancel_truck_load(truck_load, user) do
      Calls.create_cancelled_call(truck_load, user)
      {:noreply, put_flash(socket, :success, "Truckload ##{truck_load_id} Cancelled!")}
    else
      _ ->
        {:noreply, put_flash(socket, :error, "Error Cancelling Truckload ##{truck_load_id}!")}
    end
  end

  def handle_event(
        "cancel_order",
        %{"order-id" => order_id},
        %{assigns: %{current_user: user}} = socket
      ) do
    with %Order{} = order <- Orders.get_order(order_id, [:fulfillments]),
         {:ok, %Order{}} <- DispatchOrders.cancel_order(order, user) do
      {:noreply, put_flash(socket, :success, "Truckload Cancelled!")}
    else
      _ ->
        {:noreply, put_flash(socket, :error, "Error Cancelling Truckload!")}
    end
  end

  def handle_event("show_driver_availability_info", _, socket) do
    IO.puts("show_driver_availability_info being called")
    IO.inspect(socket.assigns.show_driver_avail_info?)
    socket = assign(socket, :show_driver_avail_info?, true)
    {:noreply, socket}
  end
end

Also, this is the kanban.html.leex template file:

<%= if @show? do %>
  <div id="kanban" class="<%= Shared.suit_class("rev-Row", flex: true, stretch: true, class: "Kanban-wrapper") %>">
    <%= if render_kanban?(@scheduled, @dispatched, @loaded, @on_site) do %>
      <div class="<%= Shared.suit_class("rev-Col", medium: 3, class: "Kanban-col") %>">
        <%= render "_kanban_column_header.html",
          [
            name: "Scheduled",
            column_name: "scheduled",
            card_count: Enum.count(@scheduled),
            card_count_changed?: @count_changes.scheduled,
            error_count: 0
          ]
        %>
        <%= live_component @socket, TSSWeb.DispatchHomeLive.Kanban.Column.Component,
            show_reroute_button?: false,
            show_drop_trailer_button?: false,
            show_start_over_button?: true,
            show_change_pp_button?: false,
            show_call_to_well_button?: false,
            show_unload_button?: false,
            truck_load_etas: @load_etas,
            truck_loads: [],
            orders: @scheduled,
            well: @well,
            show_driver_avail_info?: @show_driver_avail_info?
        %>
      </div>
      <div class="<%= Shared.suit_class("rev-Col", medium: 3, class: "Kanban-col") %>">
        <%= render "_kanban_column_header.html",
          [
            name: "Dispatched",
            column_name: "dispatched",
            card_count: Enum.count(@dispatched),
            card_count_changed?: @count_changes.dispatched,
            error_count: get_kanban_column_error_count(@dispatched)
          ]
        %>
        <%= live_component @socket, TSSWeb.DispatchHomeLive.Kanban.Column.Component,
            show_reroute_button?: false,
            show_drop_trailer_button?: false,
            show_start_over_button?: false,
            show_change_pp_button?: true,
            show_call_to_well_button?: false,
            show_unload_button?: false,
            truck_load_etas: @load_etas,
            truck_loads: @dispatched,
            orders: [],
            well: @well,
            show_driver_avail_info?: @show_driver_avail_info?
        %>
      </div>
      <div class="<%= Shared.suit_class("rev-Col", medium: 3, class: "Kanban-col") %>">
        <%= render "_kanban_column_header.html",
          [
            name: "loaded",
            column_name: "loaded",
            card_count: Enum.count(@loaded),
            card_count_changed?: @count_changes.loaded,
            error_count: get_kanban_column_error_count(@loaded)
          ]
        %>
        <%= live_component @socket, TSSWeb.DispatchHomeLive.Kanban.Column.Component,
            show_reroute_button?: true,
            show_drop_trailer_button?: true,
            show_start_over_button?: false,
            show_change_pp_button?: false,
            show_call_to_well_button?: false,
            show_unload_button?: false,
            truck_load_etas: @load_etas,
            truck_loads: @loaded,
            orders: [],
            well: @well,
            show_driver_avail_info?: @show_driver_avail_info?
        %>
      </div>
      <div class="<%= Shared.suit_class("rev-Col", medium: 3, class: "Kanban-col") %>">
        <%= render "_kanban_column_header.html",
          [
            name: "On Site",
            column_name: "on_site",
            card_count: Enum.count(@on_site),
            card_count_changed?: @count_changes.on_site,
            error_count: get_kanban_column_error_count(@on_site)
          ]
        %>
        <%= live_component @socket, TSSWeb.DispatchHomeLive.Kanban.Column.Component,
            show_reroute_button?: true,
            show_drop_trailer_button?: true,
            show_start_over_button?: false,
            show_change_pp_button?: false,
            show_call_to_well_button?: true,
            show_unload_button?: true,
            truck_load_etas: @load_etas,
            truck_loads: @on_site,
            orders: [],
            well: @well,
            show_driver_avail_info?: @show_driver_avail_info?
        %>
      </div>
    <% else %>
      <div class="rev-Col">
        <div class="EmptyState EmptyState--borderless u-flexVerticalAlignCenter">
          <div><%= Shared.icon "alert-circle", class: "EmptyState-icon" %></div>
          There are no trucks currently dispatched to this well. Click 'Add Load' at the top of the page to dispatch a truck.
        </div>
      </div>
    <% end %>
    <%= if @reroute_truck_load_modal_open? do %>
      <%= render "_reroute_truck_load_modal.html", assigns %>
    <% end %>
    <%= if @procurement_modal_open? do %>
      <%= render "_change_procurement_modal.html", assigns %>
    <% end %>
    <%= if @trip_break_modal_open? do %>
      <%= render "_trip_break_modal.html", assigns %>
    <% end %>
  </div>
<% else %>
  <div id="kanban"></div>
<% end %>

In the code you posted, I’m seeing the button as

<button ... click="show_driver_availability_info" phx-target={{ @show_driver_avail_info? }}>See Driver Availability Info</button>

If this is something Surface-related that I don’t understand, then I apologize in advance, but this doesn’t seem right to me. For a Phoenix callback, you should be using phx-click, and the target should be @myself to reference the LiveView process, not a boolean value.

1 Like

So for Surface, you have to do the click a little differently, using phx-click wasn’t working.
I believe you have to set the click function like this: :on-click="show_driver_availability_info"} and this works, but I’m not sure how to set the target correctly and the Surface docs aren’t really helping…just doing target={{ @myself } like you would in LiveView doesn’t work, and I’ve tried other ways but haven’t been able to get it, I’m console logging the button’s value and it keeps coming back empty.

According to some people in the Elixir slack, the phx-value- prefix should be used to pass the value along, so, this is what I have now,

The button looks like this:

<button type="button" class="rv-Button--tiny" :on-click="show_driver_availability_info"  phx-value-show_driver_avail_info={{ @show_driver_avail_info? }}>See Driver Availability Info</button>

Here’s the event_handler:

  def handle_event(
        "show_driver_availability_info",
        %{"show_driver_avail_info" => show_driver_avail_info?},
        socket
      ) do
    IO.puts("show_driver_availability_info being called")
    socket = assign(socket, :show_driver_avail_info?, true)
    {:noreply, socket}
  end

And when the button is clicked, this is the error I’m getting:

 (FunctionClauseError) no function clause matching in TSSWeb.DispatchHomeLive.handle_event/3
    (tss 0.0.1) lib/tss_web/live/dispatch_home_live/dispatch_home_live.ex:406: TSSWeb.DispatchHomeLive.handle_event("show_driver_availability_info", %{"value" => ""}, #Phoenix.LiveView.Socket<assigns: %{__context__: %{}, __surface__: %{}, breadcrumb_trail: [[name: "Home", url: "/"], [name: "Wells", url: "/wells_board"], [name: "Dispatch Board - Argent Barksdale East 29-17 4LS/4WA/6LS/6WA/7LS/7WA", ...]], connected?: true, current_user: %TSS.User{last_signed_in_at: ~U[2021-02-22 15:45:09.535722Z], password: "$2b$12$deK4g0VNl2lJPNKlTRfoSeapIceDCWb/axPDlexmTV.iNvQL/jXDS", ...}, flash: %{}, forecast_data: %{...}, ...}, changed: %{}, endpoint: TSSWeb.Endpoint, id: "phx-Fn21s0dRpOhSdzwh", parent_pid: nil, root_pid: #PID<0.6038.0>, router: TSSWeb.Router, view: TSSWeb.DispatchHomeLive, ...>)

The “value” is still empty, am I passing it along correctly?

Hi @learning123!

The value is empty because show_driver_avail_info? is false. The thing is that HTML does not support real boolean attribute values. Any value is converted to string. The only way we have to mimic the behaviour of a boolean value in HTML is by suppressing the attribute definition itself. So whenever you pass a boolean value to an HTML attribute, that attribute is only rendered if the value is truthy. That allows you to pass boolean values directly in the template e.g. disabled={{ @disabled }}.

So you have two options:

  1. Instead of pattern matching expecting the presence of a "show_driver_avail_info" key, you accept the value without that key and check it’s defined or not.
  2. Always pass a value by converting the boolean into a string, e.g. phx-value-show_driver_avail_info={{ to_string(@show_driver_avail_info?) }}. Is this case you can keep the "show_driver_avail_info" key in the pattern but test if the the value is the string representation of the boolean value, i.e. "true" or "false" instead of true or false.
1 Like

Thank you @msaraiva!

So, if I wanted to go with the second option, this is how I have the component that should be rendered when the button is clicked and show_driver_avail_info becomes "true":

<CardInfo :if={{ @show_driver_avail_info? == "true" }} label="Time Since Available" title={{ get_time_since_available(@truck_load) }}  small />

When I do this, I don’t get an error, but the <CardInfo /> component still doesn’t show up. Is there something wrong with my handle_event function? It looks like this:

  def handle_event(
        "show_driver_availability_info",
        %{"show_driver_avail_info?" => show_driver_avail_info?},
        socket
      ) do
    IO.puts("show_driver_availability_info being called")
    socket = assign(socket, :show_driver_avail_info?, true)
    {:noreply, socket}
  end

The component doesn’t show up because @show_driver_avail_info? is still a boolean and its value is either false (on initialization) or true (after clicking), never the string "true". Pay attention that you can keep the assign as a boolean as long as you treat the received values in the event handler as strings. Liveview only accepts string values as event parameters. You cannot send boolean nor integer values to events. They’ll always be converted into strings so just handle them as such in the handle_event. The rule is simple:

  1. Event params come from the browser so they are always serialized as strings.
  2. Socket assigns are kept in the server so their values are exactly the same thing you set them to be, e.g. integer, boolean, map, list, etc.
2 Likes