<.modal> conflicts with JS when closed?

I rarely touch JS, but I’ve come into a problem I’m confused about.

I was originally using the below to create a modal for new posts and but I’ve noticed that when I apply JS within the Modal, it stops working if I close and reopen the Modal.

For example I have some JS that will convert text to bold when a button is clicked. This JS works perfectly if I refresh the page or when new_post is first opened. If however I close the modal by either the X or by clicking away, then reopen the modal by either patch or navigate my JS completely stops working. Other handle_events, text entry and form submissions work perfectly, but the JS I’m using to save markdown does nothing. Any button on the modal doesn’t recieve inputs when clicked.

Is there something in the .Modal I’m not aware of that could be causing this conflict? I have no errors in the browser console or VScode terminal to go on. If I remove the Modal and make posts/new it’s own page everything works fine.

I feel like it’s going to come down to things being shown/hidden that are effectively covering my buttons with an invisible layer, but not sure. Totally lost to be honest.

<.modal
  :if={@live_action in [:new_post, :edit]}
  id="post-modal"
  show
  on_cancel={JS.navigate(~p"/user/#{@user.username}/posts")}>

  <.live_component
    module={APP.PostLive.FormComponent}
    id={@post.id || :new}
    title={@page_title}
    action={@live_action}
    current_user={@current_user}
    post={@post}
    patch={~p"/user/#{@user.username}/posts"}
  />
</.modal>

Can you show how your JS is implemented? In order for JS to play well with LiveView you need to make sure it’s integrated with a hook.

Assuming JS.navigate works like push_navigate, this will shut down the current LiveView and remount a new one, which would wipe away any client side changes made via custom JS.

So I fixed it by converting it to a hook. Broke my JS, but for test purposes it worked.

How do you know when to use hooks and when not to?

I have some basic JS like this which works if I just leave it floating:

const backButton = document.getElementById('back');

if (backButton !== null) {
  backButton.addEventListener("click", goBack);
}

function goBack() {
  window.history.back();
}

The code that was failing was this but not in a hook. Are there scenarios in which hooks are required and scenarios in which they aren’t, or is it just easier to always use hooks?

const Hooks = {};
    
Hooks.bold = {
  mounted() {

    const textarea = document.getElementById("myTextarea");
    const boldButton = document.getElementById("bold-btn");
    const cursorStart = textarea.selectionStart;
    const cursorEnd = textarea.selectionEnd;
    const text = textarea.value;
    
    boldButton.addEventListener("click", () => {
      if (cursorStart !== cursorEnd) {
        const highlightedText = text.substring(cursorStart, cursorEnd);
        const boldText = `**${highlightedText}**`;
    
        const isBold =
          text.slice(cursorStart - 2, cursorStart) === "**" &&
          text.slice(cursorEnd, cursorEnd + 2) === "**";
    
        if (isBold) {
          // Remove Bold from highlighted text
          const removeBold = `${text.substring(
            0,
            cursorStart - 2
          )}${highlightedText}${text.substring(cursorEnd + 2)}`;
          textarea.value = removeBold;
          // Adjust cursor position
          textarea.selectionStart = cursorStart - 2;
          textarea.selectionEnd = cursorEnd - 2;
        } else {
          // Adds Bold to highlighted text
          const highlightBold = `${text.substring(
            0,
            cursorStart
          )}${boldText}${text.substring(cursorEnd)}`;
          textarea.value = highlightBold;
    
          // Adjust cursor position
          textarea.selectionStart = cursorStart + 2;
          textarea.selectionEnd = cursorEnd + 2;
        }
      }
    
      if (cursorStart === cursorEnd) {
        // Removes Bold if condition is **CURSOR**
        if (
          text.slice(cursorStart - 2, cursorStart) === "**" &&
          text.slice(cursorEnd, cursorEnd + 2) === "**"
        ) {
          const removeBold =
            text.slice(0, cursorStart - 2) +
            text.slice(cursorStart, cursorEnd) +
            text.slice(cursorEnd + 2);
          textarea.value = removeBold;
          textarea.setSelectionRange(cursorStart - 2, cursorEnd - 2);
          // Ends Bold if condition is charCURSOR**
        } else if (
          text.slice(cursorStart - 1, cursorStart) !== "*" &&
          text.slice(cursorEnd, cursorEnd + 2) === "**"
        ) {
          textarea.setSelectionRange(cursorEnd + 2, cursorEnd + 2);
        } else {
          // Adds Bold if condition is CURSOR
          const newBold =
            text.slice(0, cursorStart) + "****" + text.slice(cursorEnd);
          textarea.value = newBold;
          textarea.setSelectionRange(cursorStart + 2, cursorEnd + 2);
        }
      }
    
      // Set focus to textarea
      textarea.focus();
    });
  }
};

Thanks for the suggestion, it was because I wasn’t using hooks apparently.

I suspect what might be happening is that when this is outside the mounted hook, the event listener gets added only once on page load/refresh. So when a live navigation event happens that results in re-mounting e.g. on_cancel={JS.navigate(...)}>, this event listener won’t get re-added to the new button. But by adding this code inside the mounted lifecycle callback of a hook, the event listener will get re-added to the new button upon re-mounting.

Just some speculation, but if your backButton is outside the modal, it won’t get re-mounted and therefore the floating JS addEventListener would work as expected. On the other hand, if your boldButton is within the form live component that gets remounted or modal that gets rendered after initial page load, then floating JS addEventListener would not be sufficient.