#Phoenix Liveview - infinite scroll element re-renders

4 messages · Page 1 of 1 (latest)

low stump
#

Hi! I am hardstuck on a problem and would appreciate another pair of eyes.

I have a "Notifications" badge with a popover. When users hover over the badge, a list of notifications is shown. This list has infinite scroll.

next_page and prev_page both load data and this causes a re-render and the popover hides.

Thanks!

#

Badge html

  def render(assigns) do
    ~H"""
    <.live_component
      module={PlayforstakeWeb.AppBar.Components.Notifications.Popover}
      user_id={@user_id}
      id="user-notifications"
    />
    <div
      class="cursor-pointer flex hover:bg-slate-700 border border-[#22292f] rounded-xl h-10 w-10 justify-center items-center"
      phx-hook="Popover"
      id="user_notifications_button"
    >
      <.icon name="hero-bell" class="text-white h-6 w-6" />
      <span class="sr-only">Notifications</span>
      <div class="absolute inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500  border-white rounded-full -top-3 -end-2 dark:border-gray-900">
        <%= @unread_notifications %>
      </div>
    </div>
    """
  end

popover html:

 ~H"""
    <div
      id="user-notifications"
      role="tooltip"
      class="z-index-10 absolute mt-[185px] invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-[#182029] border border-[#22292f] rounded-lg shadow-sm  "
    >
      <div class="px-3 py-2 bg-[#182029] z-index-10 border-b border-[#22292f] rounded-t-lg">
        <h4 class="pb-5">Notifications</h4>
        <ul
          id={"notifications-#{UUID.uuid4()}"}
          phx-update="stream"
          phx-target={@myself}
          phx-viewport-top={@page > 1 && "prev-page"}
          phx-viewport-bottom={!@end_of_timeline? && "next-page"}
          class={[
            "flex flex-col max-h-[410px] overflow-y-auto",
            if(@end_of_timeline?, do: "pb-10", else: "pb-[calc(20vh)]"),
            if(@page == 2, do: "pt-10", else: "pt-[calc(20vh)]")
          ]}
        >
          <li
            :for={{dom_id, notification} <- @streams.notifications}
            id={dom_id}
            class="flex flex-col text-center items-center p-1 border-b border-t border-[#22292f] rounded-t-lg"
          >
            <h7 class="font-bold">New reward</h7>
            <p class="text-[14px] font-bold">
              You have been given a YYYZ that you can use in your next match.
            </p>
          </li>
        </ul>
      </div>
    </div>
    """

Popover hooks js

let currentActivePopover = null;

Hooks.Popover = {
  tooltipTimeout: null,
  delay: 100, // Delay in milliseconds before hiding the tooltip

  mounted() {
    this.initializePopover(this.el);
  },
  updated() {
    this.initializePopover(this.el);
  },
  initializePopover(el) {
    const tooltipId = el.dataset.target_id;
    const placement = el.dataset.tooltip_placement || "bottom"
    const tooltipEl = document.getElementById(tooltipId);

    const options = {
      placement: placement,
      triggerType: 'none',
      onHide: () => {
      },
      onShow: () => {
      },
      onToggle: () => {
      },
    };

    const instanceOptions = {
      id: 'tooltipContent',
      override: true
    };

    const tooltip = new Tooltip(tooltipEl, el, options, instanceOptions);

    // Hide any active popover and show the new one
    const showTooltip = () => {
      clearTimeout(this.tooltipTimeout);
      if (currentActivePopover && currentActivePopover !== tooltip) {
        currentActivePopover.hide(); // Hide the currently active popover
      }
      tooltip.show();
      currentActivePopover = tooltip; // Set the new popover as the current active popover
    };

    // Hide the popover after a delay
    const hideTooltip = () => {
      this.tooltipTimeout = setTimeout(() => {
        if (currentActivePopover === tooltip) {
          tooltip.hide();
          currentActivePopover = null;
        }
      }, this.delay);
    };

    // Setup event listeners
    el.addEventListener('mouseenter', showTooltip);
    el.addEventListener('mouseleave', hideTooltip);
    tooltipEl.addEventListener('mouseenter', () => {
      clearTimeout(this.tooltipTimeout);
    });
    tooltipEl.addEventListener('mouseleave', hideTooltip);
  }
}
low stump
#

I removed the "hidden" attribute from the element on paginate_posts 🙂

tidal storm
#

in general, you need to make sure your client-side changes are reflected server-side when the server decides to re-render, otherwise your client-side only changes get wiped out. Another way you can handle this is to use JS to make the changes client-side right-away, but still send a message to the server to let it know the state change so that if/when the server re-renders it also knows to add (or not add) the "hidden" class back