sb logoToday I Learned

Using Phoenix hooks to control parent DOM elements

I’m building a scrollable modal that overlays a screen that’s also scrollable. I find it to be a bit of an awkward UX if both the foreground and background are scrollable in this case, so I want to disable the background scrolling when the modal opens.

The problem is that the document body can’t see the state of my LiveView. Fortunately, LiveView (combined with Tailwind CSS in our case) can handle this in another way. Using hooks, we can tell our app to add a CSS class when our modal opens, and then remove the class on modal close.

<!-- root.html.eex -->
<body id="app">
  ...
  <%= @inner_content %>
</body>

And now we add our hook:

// assets/js/hooks/index.js
const hooks = {}

hooks.ToggleAppScroll = {
  mounted: () => {
    document.getElementById("app").classList.toggle('overflow-hidden');
  },
  destroyed: () => {
    document.getElementById("app").classList.toggle('overflow-hidden');
  }
}

export default hooks

And then in our modal component:

  def render(assigns) do
    ~L"""
      <div phx-hook="ToggleAppScroll" class="bg-gray-300 bg-opacity-50 fixed top-0">
          <!-- Modal content goes here -->
      </div>
     """
  end

And now any new component that wants to disable scrolling of the app simply has to add phx-hook="ToggleAppScroll" to its attributes. The phx-hook lifecycle will handle the rest.