Home > Software engineering >  Adding an eventlistener on not yet generated DOM
Adding an eventlistener on not yet generated DOM

Time:01-26

I have a bunch of

<div  data-location-id="123">
  <img src="img_url" />
</div>

Loaded into my .locations div. I want that whenever you click on a .location-box that the clicked div gets a highlighted class on it. And the attribute value gets added to a hidden input. When you click on another one, the class from the previous one gets removed. And so on and so on.

I've tried it before when those divs where static, and it worked fine. But now I'm appending these divs out of pure Javascript from an api call.

I also know that not yet generated DOM can't be manupilated by event listeners etc.

I've looked into mutation observers, and tried some simple stuff from the docs. But I could make this code work with it

let locations = document.querySelectorAll(".location-box");

locations.forEach( el => 
    el.addEventListener('click', function() {
        locations.forEach( els => els.classList.remove('active-location'));
        document.getElementById('location_id').value = this.getAttribute('data-location-id');
        this.classList.add("active-location");
    })
);

Does anyone know how to make this work? Maybe not only this time, but in multiple cases. Cause in the near future I'd probably have more not yet generated DOM.

CodePudding user response:

You can do that with MutationObserver, the code it's something like below, it doesn't have the piece to get the attribute, but you can add that, another way of doing it would be like @scara9 is saying, on the code you use to render each .location-box you can assign the click handler.

in the code below i used jquery to "add" new location-box, you don't need jquery for this

// select the parent node: .locations, i switched to ID for test
var target = document.getElementById("locations");

// create an observer instance
var observer = new MutationObserver(function (mutations) {
  //loop through the detected mutations(added controls)
  mutations.forEach(function (mutation) {
    //addedNodes contains all detected new controls
    if (mutation && mutation.addedNodes) {
      mutation.addedNodes.forEach(function (elm) {
        if (elm && elm.className=="location-box") {
          elm.addEventListener("click", function () {
            elm.classList.add("active-location");

            var locations = document.querySelectorAll(".location-box");

            locations.forEach((e) => {
              if (e != elm) {
                //ignore clicked element
                e.classList.remove("active-location");
              }
            });
          });
        }
      });
    }
  });
});

// pass in the target node, as well as the observer options
observer.observe(target, {
  childList: true
});

//you don't need this, it's only to simulate dynamic location-box
$(function () {$("button").on("click", function () {var count = $(".location-box").length   1;$(".locations").append($("<div class='location-box' data-attribute-id='"  count  "'>location box:"  count  "</div>"));});});
.active-location{
  background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div  id="locations">


</div>

<!-- This button it's only to add the controls, not needed in your case-->
<button type="button" id="add">
Add
</button>

CodePudding user response:

From my above comment ...

"@Coolguy31 ... A MutationObserver based approach most likely is overkill. Event Delegation might be the technique of choice. But in order to implement it somehow correctly it was nice to know whether all the later rendered stuff is always inserted/appended below a common and also known root node, cause document.body as event listening root is not the most elegant/performant choice either."

function uuid(a) {
  // [https://gist.github.com/jed/982883] - Jed Schmidt
  return a
    ? (a^Math.random()*16>>a/4).toString(16)
    : ([1e7] -1e3 -4e3 -8e3 -1e11).replace(/[018]/g,uuid);
}

function addLocations(evt) {
  evt.preventDefault();

  const allLocationsRoot = 
    document.querySelector('.locations');

  allLocationsRoot.innerHTML = `
    <div >
      <img src="https://picsum.photos/133/100?grayscale" />
    </div>
    <div >
      <img src="https://picsum.photos/100/75?grayscale" />
    </div>
    <div >
      <img src="https://picsum.photos/120/90?grayscale" />
    </div>
  `;
  allLocationsRoot
    .querySelectorAll('.location-box')
    .forEach(locationNode => locationNode.dataset.locationId = uuid());

  allLocationsRoot
    .closest('form[name="location-data"]')
    .elements['location']
    .value = '';
}
function initializeAddLocations() {
  document
    .querySelector('button')
    .addEventListener('click', addLocations);
}

function handleLocationSelect(evt) {
  const locationItemRoot = evt
    .target
    .closest('.location-box');

  if (locationItemRoot) {

    const allLocationsRoot = locationItemRoot
      .closest('.locations');

    const locationControl = allLocationsRoot
      .closest('form[name="location-data"]')
      .elements['location'];

    // console.log({
    //   target: evt.target,
    //   locationItemRoot,
    //   allLocationsRoot,
    //   locationControl,
    // });
    allLocationsRoot
      .querySelectorAll('.location-box')
      .forEach(locationNode => locationNode.classList.remove('selected'));

    locationItemRoot.classList.add('selected');
    locationControl.value = locationItemRoot.dataset.locationId;
  }
}
function initializeLocationHandling() {
  document
    .querySelector('.locations')
    .addEventListener('click', handleLocationSelect)
}

function main() {
  initializeLocationHandling();

  initializeAddLocations();
}
main();
body { margin: 0; }
[type="text"][name="location"] { width: 23em; }
.locations:after { display: block; content: ''; clear: both; }
.location-box { float: left; margin: 4px; padding: 10px; min-height: 104px; background-color: #eee; }
.location-box.selected { outline: 2px solid #fc0; }
<form name="location-data">

  <div >
  </div>
  <button>update locations</button>

  <!--
  <input type="hidden" name="location" />
  //-->
  <input type="text" name="location" disabled />

</form>

  •  Tags:  
  • Related