Phoenix Hook in layout, how to handle globally?

I have a JavaScript hook in my Phoenix LiveView layout that detects timezone changes and sends an “update_timezone” event via pushEvent . Currently I’m copy-pasting the same handle_event("update_timezone", ...) function across all my LiveViews.

What’s the best way to handle this event globally across all LiveViews without duplicating the handler code? The hook is in the layout template but layouts aren’t LiveViews themselves, so I can’t put the handler there.

# layouts.ex
<main class="min-h-screen">
  <%= if @current_scope do %>
    <div
      id="timezone-detector"
      phx-hook="TimezoneDetector"
      data-authenticated="true"
      data-current-timezone={@current_scope.user.timezone}
      class="hidden"
    >
    </div>
  <% end %>
  ...

My hook:

const TimezoneDetector = {
  mounted() {
    // Only update if user is authenticated
    if (this.el.dataset.authenticated === "true") {
      const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
      const currentTimezone = this.el.dataset.currentTimezone;

      // Only send update if timezone is different
      if (timezone && timezone !== currentTimezone) {
        this.pushEvent("update_timezone", { timezone: timezone });
      }
    }
  }
};

Where do I put the handle_event so it’s global?

I believe you will want to use attach_hook/4 to handle the event globally.

1 Like

Thank you completely slipped my mind!

Alternate method below if you’re interested. Allows you to write events or info etc within dedicated modules and applies them globally to all of your LiveViews.

If you’re app has lots of LiveViews that share handle_event/info/param logic it can keep everything totally DRY.

defmodule PhxieWeb.LiveViewHandler do
  @moduledoc """

    This module defines common implementations for `handle_info`, `handle_params`, and `handle_event` callbacks. 
    By using this module, you can streamline and centralize event-handling functionality that is common across 
    multiple AppWeb.Live.Index modules.

    Usage:

    defmodule PhxieWeb.HomeLive.Index do
      use PhxieWeb, :live_view
      use PhxieWeb.LiveViewHandler
    
  """

  import PhxieWeb.{LiveviewEvents, LiveviewInfo}

  defmacro __using__(_) do
    quote do

      @doc """
      Handles incoming parameters from the LiveView route so the correct socket can be assigned.

      @spec handle_params(map(), any(), Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()}
      """
      @impl true
      def handle_params(params, _, socket) do
        {:noreply, apply_action(socket, socket.assigns.live_action, params)}
      end

      @doc """
      Converts info/3 functions from PhxieWeb.LiveviewInfo into handle_info/3

      @spec handle_info({atom, any}, Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()}
      """

      @impl true
      def handle_info({atom, payload}, socket) do
        info(atom, payload, socket)
      end

      @impl true
      def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
        {:noreply, socket}
      end

      @doc """
      Converts event/3 functions from PhxieWeb.LiveviewEvents into handle_event/3
            
      @spec handle_event(string(), map(), Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()}
      """
      
      @impl true
      def handle_event(event, params, socket) do
        event(event, params, socket)
      end
    end
  end
end

The events/info etc then look like this:

defmodule PhxieWeb.LiveviewEvents do
  use PhxieWeb, :live_view

  ##############################
  ####  Modal Map : Update  ####
  ##############################

  def event("update modal map", params, socket) do 
    modal_map = Map.put(modal_map(), atomize(params["option"]), true)

    {:noreply, assign(socket, modal_map: modal_map)}
  end

  ##############################
  ####  Modal Map : Reset  ####
  ##############################
  
  def event("reset modal map", _params, socket) do
    {:noreply, 
      socket
      |> assign(:quote_post, nil)
      |> assign(:channel_create, %{})
      |> assign(modal_map: modal_map())
    }
  end

I would feel super uneasy about this. You are basically going down the path of inheritance with this approach.

Instead, I would use defdelegate to delete to some module that implements shared functionality.

Depends on the apps scale and rate of repetition though.

Defdelegate is fine if you have a couple of LiveViews sharing a couple of events, but as the number of LiveViews and shared events increases, the more you can benefit from creating a shared handler.

My app currently has 6 LiveViews with 30+ fully or semi shared events, hence why I’m using the handler method.

Edit: Forgot to add there’s also just a level of preference for creating isolated modules on my part as well. I like having a dedicated module for events rather than storing events everywhere and anywhere as it makes it significantly easier to find what I’m looking for. I can just go into the “events” module and find whatever event I’m looking for rather than having to sift through all my LiveViews. It also keeps the actual LiveView modules cleaner, as you can strip the handle_x code out of them and treat them as dedicated routing modules.

All of those things can be done with attach_hook as well though without needing to deal with the complexities around macros.

Its hardly complex lol…It’s just one macro/module to make things global. As I said originally, it’s just an alternative method if anyones interested in making things truly global without having to attach hooks everywhere. It’s a personal preference alternative at the end of the day.

With macros you can easily run into issues like stracktraces pointing to the wrong line or errors around unused functions, question on the order of functions, … With hooks someone coming to your projects gets an explicit hint that there’s additional functionality spread in and there’s documentation on how it works. With macros things are not so obvious.

2 Likes

You could apply the same argument and say that Import should never be used over Alias.

It’s personal preference as both methods achieve the same result with different trade offs. I would rather import global events and info via use then explicitly mount them via attach_hook. There is a negligible performance difference, that likely favours macros anyways and in terms of new people viewing code its pretty much the same process as attach_hook. You still have to find the attach_hook module to understand the events, which is the same as the macro method.

I’m just going to end my part in this discussion saying it’s an alternative method I like to use and figured I’d share. Not bothered on trying to convert people, just sharing it.