Script can't get elements on page load JS

Script can't get elements on page load JS



I have a problem with this code:




var coll = document.getElementsByClassName("collapsible");
var i;

for (i = 0; i < coll.length; i++)
coll[i].addEventListener("click", function()
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block")
content.style.display = "none";
else
content.style.display = "block";

);

/



when page is fully loaded, the class name ("collapsible") is not yet present in the page code, its been loading from remote source only upon user request, so, long story short - its not working.



Lazy solution is to include this code in every page with custom class names, but I don't want it to be this way



Also if its possible I want to stick with this JS code, not jQuery



Can somebody help me to modify this code to work in this case?



Loader for remote content (Update 1):




$.get('t.html', , function(data)
var $response = $('<div />').html(data);
var $div1 = $response.find('#div1');
$('#div-for-remote-content').append($div1);
,'html');



// Update 2



Thank You everyone for your help, I know exactly what I need to solve this problem, but I need your help on that:)



I need a script that checks if all .get() request on page is successfully completed and all content are loaded in main document, if it does, load a script files to the main document.



I have a follow up here, does the scripts in this files will be automatically working on main document?





Use event delegation.
– Xufox
Sep 2 at 15:47





I'm new to JS, can you please elaborate, or show me how it needed to be done? @Xufox
– Andrew K
Sep 2 at 15:50






Have you assigned this class to any element in your page, and if yes, where does this code reside in relation to the first occurrence of an element of class "collapsible"?
– Robidu
Sep 2 at 15:50





Make sure your <script> element is located just prior to </body> so that it doesn't execute until after the DOM is ready.
– Scott Marcus
Sep 2 at 15:52



<script>


</body>





@AndrewK In your case, event delegation would basically look like this: document.addEventListener("click", function(e) if(e.target.hasClass("collapsible")) your event listener code );, but replace each this by e.target. This way, it doesn’t matter when the elements appear, and you’re only binding a single event listener.
– Xufox
Sep 2 at 15:57


document.addEventListener("click", function(e) if(e.target.hasClass("collapsible"))


);


this


e.target




5 Answers
5



If you have no idea regarding when and how the class 'collapsible' is added to your DOM, you might want to consider using a MutationObserver interface, which provides the ability to watch exactly for changes being made to the DOM tree.



For example:


// Callback function to execute when mutations are observed
var callback = function(mutationsList)
if (document.getElementsByClassName('collapsible').length > 0)
// YOUR CODE HERE !!!
this.disconnect();

;

// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);

// Select the node that will be observed for mutations
// this is targeting the whole html tree, you can narrow it if you want
var targetNode = document.documentElement;

// Options for the observer (which mutations to observe)
var config = attributes: true, childList: true, subtree: true ;

// Start observing the target node for configured mutations
observer.observe(targetNode, config);



};



In this way, your code will actually wait for the class 'collapsible' to be added to your DOM before being executed.



BTW, I would not produce a code which is relying on someone else's code: if you are not in control of the class 'collapsible' you are preparing your code for failure ...



There is a solution to get things done. This method will do the trick providing that any resources to be loaded are returned as HTML.


function load_content(p_rsrc)

var l_request = new XMLHttpRequest();

l_request.addEventListener('readystatechanged', function(p_event)
if(this.readystate == 4)
// We wind up here once AJAX reports the request to be completed (either because of a timeout or because of a reaction from the server)
if(this.status == 200)

let l_col;
let l_target = document.getElementById('div-for-remote-content');
let l_this;

if(l_target == null)

console.error('FATAL: Could not find container for remote content!');
throw "internal error";


// Assign the received contents to the container
// This clears out any previous content, including any event handlers, before adding the new content.
l_target.innerHTML = l_request.responseText;
// Once we are here we can attach event handlers to any element that brings along the "collapsible" class...
// The following statement digs up all elements of the collapsible class that are present in their container.
l_col = document.querySelectorAll('#div-for-remote-content .collapsible');

// Now dig through the entire list of items...
for(l_this of l_col)

l_this.addEventListener("click", function(p_event)
p_event.target.classList.toggle("active");
if (p_event.target.style.display === "block")
p_event.target.style.display = "none";
else
p_event.target.style.display = "block";
, false);


// You can do some additional status handling here, like dealing with any errors
, false);
l_request.open('GET', p_rsrc, true);
l_request.send();



As you can see the event listeners are attached outside of the scope of the loaded new content if and only if the AJAX request returned with a success (that is, status code 200). Any other status code won't allow the script to modify the contents of your container, and I have also added some rudimentary sanity checks.



Please note that assigning the contents (which have to be HTML to work properly) to .innerHTML clears out the DOM subtree of your container so that you can assign new content to it without having to worry about any event handlers or outdated DOM nodes. This avoids duplicate invocation of your event handler and also allows for attaching this function to a button as an event handler (or invocation from another script block). Please note that you have to pass the path to the desired resource to this function.


.innerHTML



EDIT:



Corrected a bug in my code snippet



EDIT 2:



When you import this code into your document you need to make sure that it gets invoked whenever someone loads extra content into the document. You can do that by attaching a wrapper function to whatever mechanism is required to facilitate this (i. e. a button that triggers an event handler which in turn calls this function and also provides the URL of the resource to be loaded).



Once load_content() is called, the following sequence of events is set off:


load_content()


readystatechange


readystatechange


this.readyState == 4


this.status == 200


.innerHTML


collapsible



Any resources loaded from the remote source are not involved in this process at all so you don't risk multiple runs of this function, and every time that new content is loaded into the container, the old one is effectively overwritten.





I appreciated your help, the problem is bigger then I think at the beginning, and the main problem is - duplication, the original question was not been asked the right way, after rethinking my situation with this particular case and other possible future dev I came to a possible solution. Is it possible to check and run specific scripts again in the main script file after every successfully completed get request?
– Andrew K
Sep 2 at 18:43






It is, and without incurring the issue of duplication. It's similar to what I have done here by attaching the code that sets any event handlers to the result of the AJAX request. Since it doesn't require you to load the script along with your remote resources, you don't risk that things would be duplicated.
– Robidu
Sep 2 at 18:48





I didn’t get you last comment, original problem from the beginning was that script from external file on main page didn’t run on remote content after loading, I just want to scripts from external file on main page works on newly added content to that page, if the previous answer is solving this somehow - I didn’t get it
– Andrew K
Sep 2 at 19:54





O.k., then let me edit in the sequence of events - maybe that clarifies things.
– Robidu
Sep 2 at 19:56





The best thing to see whether it works for you is putting this code into your external JS file and hook the function up to any elements that load your external resources. The only thing that you have to ensure is passing the URL of the desired resource to the function.
– Robidu
Sep 2 at 20:28



This function will keep looking for a specific class on a target element:


function checkElement(selector, cb)
var raf;
var found = false;
(function check()
const el = document.querySelector(selector);
if (el)
found = true;
cancelAnimationFrame(raf);
// Do something here
console.log('Found it');
// Include your logic in the callBack
cb();
else
raf = requestAnimationFrame(check);

)();
return found;



How to use it:


checkElement('collapsibile', elementReady);

function elementReady()
var coll = document.getElementsByClassName("collapsible");
var i;

for (i = 0; i < coll.length; i++)
coll[i].addEventListener("click", function()
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block")
content.style.display = "none";
else
content.style.display = "block";

);

;



It acts like a "watcher". You can modify the check function of course as you see fit. Using document.querySelector(selector); will match only the first element in the DOM.


check


document.querySelector(selector);



I'd use EventDelegation probably in your case but my understanding is that you are after something else.



Hope it helps anyway!


function addCollapsible(container /* $div1 in your code */ )
[...container.querySelectorAll(".collapsible")].forEach(el =>
el.addEventListener("click", (event) =>
const content = el.nextElementSibling;
el.textContent = el.textContent.trim() === "collapse" ? "uncolapse" : "collapse";
toggleDisplay(content);
)
)




[...document.querySelectorAll(`[data-js="fetch-html"]`)].forEach(el =>
el.addEventListener("click", async function(event)
const target = event.currentTarget;
const url = target.getAttribute("data-url");
const html = await fakeFetch(url);
target.textContent = target.textContent.slice(target.textContent.indexOf("content"));
const container = document.createElement("div");
container.innerHTML = html;
addCollapsible(container);
target.insertAdjacentElement('afterend', container)
,
once: true
);
);

function addCollapsible(container)
[...container.querySelectorAll(".collapsible")].forEach(el =>
el.addEventListener("click", (event) =>
const content = el.nextElementSibling;
el.textContent = el.textContent.trim() === "collapse" ? "uncolapse" : "collapse";
toggleDisplay(content);
)
)


function toggleDisplay(el)
if (!el.style.display

async function fakeFetch(url)
const urlMap =
"content1.html": content1Html,
"content2.html": content2Html


await wait(200)
return urlMap[url];


async function wait(waitTime)
return new Promise((resolve) =>
setTimeout(resolve, waitTime)
)


const content1Html = `
<div class="collapsible">
collapse
</div>
<div>
visible 1
</div>
<div class="collapsible">
collapse
</div>
<div>
visible 2
</div>
`;

const content2Html = `
<div class="collapsible">
collapse
</div>
<div>
visible 3
</div>
<div class="collapsible">
collapse
</div>
<div>
visible 4
</div>
`;


body
font-family: sans-serif;


.collapsible
background-color: mediumseagreen;


<h2 data-js="fetch-html" data-url="content1.html">fetch content 1</h2>
<h2 data-js="fetch-html" data-url="content2.html">fetch content 2</h2>


/* use on document.body or even better the closets common ancestor: '#div-for-remote-content' */
document.body.addEventListener("click", (event) =>
const target = event.target;
const el = target.classList.contains("collapsible") ? target : target.closest(".collapsible");
if (el)
const content = el.nextElementSibling;
el.textContent = el.textContent.trim() === "collapse" ? "uncolapse" : "collapse";
toggleDisplay(content);

)




[...document.querySelectorAll(`[data-js="fetch-html"]`)].forEach(el =>
el.addEventListener("click", async function(event)
const target = event.currentTarget;
const url = target.getAttribute("data-url");
const html = await fakeFetch(url);
target.textContent = target.textContent.slice(target.textContent.indexOf("content"));
const container = document.createElement("div");
container.innerHTML = html;
target.insertAdjacentElement('afterend', container)
,
once: true
);
);

document.body.addEventListener("click", (event) =>
const target = event.target;
const el = target.classList.contains("collapsible") ? target : target.closest(".collapsible");
if (el)
const content = el.nextElementSibling;
el.textContent = el.textContent.trim() === "collapse" ? "uncolapse" : "collapse";
toggleDisplay(content);

)

function toggleDisplay(el)
if (!el.style.display

async function fakeFetch(url)
const urlMap =
"content1.html": content1Html,
"content2.html": content2Html


await wait(200)
return urlMap[url];


async function wait(waitTime)
return new Promise((resolve) =>
setTimeout(resolve, waitTime)
)


const content1Html = `
<div class="collapsible">
collapse
</div>
<div>
visible 1
</div>
<div class="collapsible">
collapse
</div>
<div>
visible 2
</div>
`;

const content2Html = `
<div class="collapsible">
collapse
</div>
<div>
visible 3
</div>
<div class="collapsible">
collapse
</div>
<div>
visible 4
</div>
`;


body
font-family: sans-serif;


.collapsible
background-color: mediumseagreen;


<h2 data-js="fetch-html" data-url="content1.html">fetch content 1</h2>
<h2 data-js="fetch-html" data-url="content2.html">fetch content 2</h2>



To check how many listeners an element has, you can run getEventListeners on a given element in chrome console.


getEventListeners


[...document.querySelectorAll("*")].map(el =>
[el, getEventListeners(el)]
).filter(([_,o]) => Object.keys(o).length)



Long answer short:



1) Don't be afraid of using jQuery, it's still one tiny good old legacy way for manipulating DOMs like what you are trying to achieve in your question.



2) You need to execute your snippet after "loading from remote source" completed.


$.get('t.html', , function (data)
var $response = $('<div />').html(data);
var $div1 = $response.find('#div1');
$('#div-for-remote-content').append($div1);
, 'html').then(function ()
var coll = document.getElementsByClassName("collapsible");
var i;

for (i = 0; i < coll.length; i++)
coll[i].addEventListener("click", function ()
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block")
content.style.display = "none";
else
content.style.display = "block";

);

);



Not sure if this is what you are looking for as I can't fully understand the issue you are having.



And thanks to the down voter who I guess hate jQuery, but I still insist jQuery is a useful library in 2018 even I didn't actually use it in the recent several projects.





Even if I put the script into every get call it still be causing duplicate problem
– Andrew K
Sep 2 at 16:45



Thanks for contributing an answer to Stack Overflow!



But avoid



To learn more, see our tips on writing great answers.



Some of your past answers have not been well-received, and you're in danger of being blocked from answering.



Please pay close attention to the following guidance:



But avoid



To learn more, see our tips on writing great answers.



Required, but never shown



Required, but never shown




By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)