Stimulus Discourse

Missing target element when appending a modal to a root div

I have an open modal function that opens bootstrap modal for me. We do some extra work to basically display the inside of a modal based on the source. The issue is that when they are inside other elements that have a separate z-index, they appear under the backdrop. This is why I am moving to a root div called modal_test with append.

This works fine, but after that it can no longer target the the contentTarget that is inside the modal.
Is there anything I can do to try and find the contentTarget again after it is appended?

 open(event) {
    document.body.classList.add("modal-open");
    this.containerTarget.style.display = "block";
    this.containerTarget.classList.add("show");

    let modal_test = document.getElementById('modal_test')
    modal_test.append(this.containerTarget)
    document.getElementById('overlay').classList.add('modal-backdrop', 'fade', 'show')

    this.isOpen = true;
    event.stopPropagation();

    if (this.data.get("source")) {

      const separator = this.data.get("source").includes("?") ? "&" : "?";
      const url = this.data.get("source") + separator + this.shiftQuery();
      fetch(url)
        .then((response) => {
          if (response.status !== 200) {
            return;
          }

          response.text().then((data) => {
            this.connect();
            this.contentTarget.innerHTML = data;
          });
        })
        .catch((err) => {
          console.log("Fetch Error", err);
        });
    }
  }

Hey @danvernon!

Welcome to the Stimulus Discourse.

Is it correct to say that the contentTarget is within the containerTarget (which gets reparented)? Once you move a target outside of a controller’s element, it’s no longer accessible as a Stimulus target. You could always query for it using native JS DOM querying at that point. However, all of this is breaking the encapsulation of the Stimulus controller, so I’d recommend not moving the containerTarget in the first place as well as not reaching out to find other DOM elements outside of the controller.

I’m happy to help figure this out, but I can’t give a recommendation at the moment as I don’t understand the motivation for the approach. Would you mind clarifying “We do some extra work to basically display the inside of a modal based on the source.”? What is “source” in this context?

Would it be possible for the Bootstrap modal HTML and Stimulus HTML be one and the same? E.g.

<div class="modal fade" id="exampleModal" data-controller="modalController" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">

hey @welearnednothing thanks for the reply!

So our modal component contains <div data-controller="modal"> along with the button to open the modal. We render different templates in the modal via source depending on the props sent to the component.

The modal component also has a <div data-target="modal.container"> which is actually the modal divs, and also a div inside <div data-target="modal.content"> where we render the source templates.

When I am moving the modal.container div to a new div i have at root <div id="modal_test" data-controller="modal"> it can no longer find the modal.content target, although its there and the move happens successfully.

Note: If I don’t append to the root, it all works as expected.

Hrmm… so are there two ModalController divs?

<div data-controller="modal"> and <div id="modal_test" data-controller="modal">?

If that’s the case, those are two separate instances of ModalController, not two parts of the same instance. A controller instance will only include DOM elements within the root container. If this is the case, the id="modal_test" ModalController actually would have a reference to contentTarget at that point… but the code is executing in the first.

You mentioned an issue with z-indexes - would it be possible just to add/remove a class that alters the z-index to always make sure it’s on top?

I feel like there’s probably a relatively straightforward approach here, but it would be helpful to include an HTML example, as well, to make sure I’m properly grasping the structure of everything. I’ve created modals in Stimulus, but I haven’t integrated Boostrap modals with Stimulus. However, there’s no reason they shouldn’t play nicely together.

Thanks @welearnednothing, the z-index issue is that the modal can be nested inside something that has a lower z-index than the backdrop, so it displays behind it. For eg. a bootstrap dropdown has a z-index of 1000, which puts it below the backdrops 1040. That’s why I was trying to drag it out to root on open. Stuck as what else to do.

Without reinventing the wheel on your current approach, you could just render the content directly into the modal.

<div data-controller="modal" data-modal-target=".modal-body">

and

open(event) {
  document.body.classList.add("modal-open");
  this.containerTarget.style.display = "block";
  this.containerTarget.classList.add("show");
  document.getElementById('overlay').classList.add('modal-backdrop', 'fade', 'show')

  this.isOpen = true;
  event.stopPropagation();

  if (this.data.get("source") && this.data.get("target") {
    const separator = this.data.get("source").includes("?") ? "&" : "?";
    const url = this.data.get("source") + separator + this.shiftQuery();
    fetch(url)
      .then((response) => {
        if (response.status !== 200) {
          return;
        }

        response.text().then((data) => {
          const target = document.querySelector(this.data.get("target"))
          if(target) {
            target.innerHTML = data
          }
        });
      })
      .catch((err) => {
        console.log("Fetch Error", err);
      });
    }
}

Here, the target data specifies a selector (in this case an ID) to use to insert the returned HTML into rather than inserting it into the content DIV. The selector should identify the modal body element of the Bootstrap modal. If you have more than one modal in your HTML, you’ll want to use something more specific than the generic modal-body class that Bootstrap uses, otherwise it’ll (as it’s written now) just pick the first modal. So you could use #example-modal, #lesson-modal, #confirmation-modal, etc. if need be for multiple modals (though in this case because you’re loading the content dynamically, I assume that’s not applicable here).

Hope this helps!

Yeah the problem here is, i need the data-controller="modal" on the parent that has the modal open button and the modal inside, and the data-modal-target=".modal-body" nested inside the actual modal - which then returns null if i log it.

These have to be on the same div right? data-controller="modal" + data-modal-target=".modal-body"

Why does the controller need to include both the button and the modal? Is that a business requirement? Is there other logic inside the controller that’s relevant?

It seems like you could have something like the following (taken straight from Bootstrap’s modal example):

<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal" data-controller="modal" data-modal-target=".modal-body">
  Launch demo modal
</button>

<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>

A similar approach would be to break apart the button/trigger and the remote loading, where the <div class="modal-body"> would also be a controller that’s responsible for only loading remote content and the button sends it a URL to load content from (via dispatching an event). That’s a little more advanced, so let’s not get into that just yet.

Would the above approach mimicking the Bootstrap example work?

Thanks I will play around and let you know how it goes.

Right on, best of luck!