On site: http://jonirautiainen.net/code_edit/ there on left is a panel which has a filetree.
.load('scandDir.php') loads new <ul><li></li></ul> elements from php-file.
The problem is that because this script is a loop it executes loadNew(); multiple times.
Please check the problem live on page mentioned before. Click on those folders to open them and you will see those files showing up multiple times.
Script needs to be looped because .load is asynchronous. $(".file-tree a").on( won't let me select those new elements created by executing .load.
Is there any other way to do this?
function loadNew()
{
$(".file-tree a").on("click",function ()
{
var path = $(this).attr("title");
var folder = $(this).text();
var name = $(this).attr("name");
var id = $(this).parent().attr("id");
$("#testi").html(path + folder + name + id);
var liHtml = $(this).parent().html();
if(liHtml.match(/<ul /))
{
}else
{
if(name=="dir")
{
$("#hiddenDiv1").load("scanDir.php?path=" + path + "/" + folder, function() {
var hiddenDiv = $("#hiddenDiv1").html();
$("#" + id).append(hiddenDiv);
loadNew();
});
}
}
});
}
loadNew();
You keep piling on new click handlers. I understand that you're loading new elements and they need to have their events handled too, but in the process you're adding more handlers to the existing elements. So after you expand several directories, the older elements will have a couple of handlers on them; and when you click those, all of those handlers will get invoked (which gets you additional elements).
One approach is to take care what you add your click handler to. Only add it to the newly loaded elements.
Another, easier approach is to take advantage of the power of jQuery's on() method. If you write it like this:
$('#fileNavi').on('click', '.file-tree a', function() {
...
});
and only once, the handler will work for current and any future elements that appear in your hierarchy. Then inside the handler just do the AJAX call and add the new data to the document, trusting jQuery to handle the events for you. Get rid of the loadNew function altogether, especially the deviant recursion you have going there.
Your loadNew function is binding a new click handler to $(".file-tree a") every time you call it.
1 click, one load, 3 clicks.. 3 clcik handlers, 3 loads. Delegate the click handler to parent of the list.
$("#fileNavi ").on("click",'.file-tree a',function ()
loadNew();
})
EDIT: will require some reformatting of loadNew , you could pass "this" into load new as an argument so you can get all the variables from the element clicked
It looks to me like every time a .file-tree link is clicked it will be attaching a new click handler to all of the .file-tree elements. You just want to attach to the new elements. So perhaps change to something like this:
var expand = function() {
var path = $(this).attr("title");
var folder = $(this).text();
var name = $(this).attr("name");
var id = $(this).parent().attr("id");
$("#testi").html(path + folder + name + id);
var liHtml = $(this).parent().html();
if(liHtml.match(/<ul /)) {
}
else
{
if(name=="dir")
{
$("#hiddenDiv1").load("scanDir.php?path=" + path + "/" + folder, function() {
var hiddenDiv = $("#hiddenDiv1").html();
$("#" + id).append(hiddenDiv);
});
}
}
};
$('#fileNavi').on('click', '.file-tree a', expand);
EDIT:
on will effect all elements added to the DOM as well as existing elements.
you probably just need to add some parameters to "loadNew()" so it knows exactly what to do and not repeat itself.
Related
Should i create only one DOMContentLoaded event listener and gather all the stuff for all my elements inside, or i can write multiple DOMContentLoaded listeners for every my element?
This:
window.document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, {edge:'right'});
});
window.document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.collapsible');
var instances = M.Collapsible.init(elems, {"accordion" : false});
});
Or this:
window.document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, {edge:'right'});
var elems2 = document.querySelectorAll('.collapsible');
var instances2 = M.Collapsible.init(elems2, {"accordion" : false});
});
What is more correct? Are there any caveats using multiple functions for DOMContentLoaded event? Do one overwrites the other or they both get saved and fired at their time later?
Right now i have them separately in different functions, it's more convenient for me, but i do not know if it's correct or not. Thanks.
By putting it in one listener function, you're reducing the fragmentation of the code, helping readability, and avoiding the situation where the next developer "fixes" missing calls in the first listener that were actually already present in the second listener.
If you have multiple source files for a single displayed page (such as with MVC shared Views loaded into a parent View), you may find that it's logical and necessary to have a listener in more than one file.
This question already has answers here:
How do I attach events to dynamic HTML elements with jQuery? [duplicate]
(8 answers)
Closed 3 years ago.
I'm trying to figure how I can use the each() method to loop through dynamic elements. The method does not seem to be working if the element is not present. I do not wish to use setTimeout either and would prefer something that would not delay the method or event.
So far all of the research and searching I've seen is using an event handler to trigger on dynamic objects.
$('.task').each(function() {
let that = this;
// let startTask = $('.start-task', that);
let mc = new Hammer(this);
mc.on("panright", function(e){
if(e.deltaX < 100 ) {
$(that).css('transform', 'translateX(' + e.deltaX + 'px' + ')');
$(that).addClass('pan-task');
} else {
$(that).css('transform', 'translateX(100px)');
$('.start-task', that).trigger('click');
}
});
mc.on('panend', function(e){
$(that).css('transform', 'translateX(' + '0' + 'px' + ')');
$(that).removeClass('pan-task');
});
});
This will work with dynamic elements. It just depends when you run it. First you do what you have copied here but you isolate what is inside the “each” in a function. Then when you create a new task, you run the same function as a callback.
Really do a function where the task is the argument. Otherwise you risk attaching the same event multiple times.
function initTaskEvents(task) {
let mc = new Hammer(task.get(0));
mc.on("panright", function(e){
if (e.deltaX < 100 ) {
task.css('transform', 'translateX(' + e.deltaX + 'px' + ')');
task.addClass('pan-task');
} else {
task.css('transform', 'translateX(100px)');
$('.start-task', task).trigger('click');
}
});
mc.on('panend', function(e){
task.css('transform', 'translateX(' + '0' + 'px' + ')');
task.removeClass('pan-task');
});
}
As you see the argument is assumed to be already a jQuery object. This is why I used .get(0) for your constructor. Remove it if it accepts jQuery objects. By the way, it is not needed anymore, but avoid creating the same jQuery object $(that) many times because $ is actually an expensive function. You want to save it in a variable if possible and reuse it.
Then you use it on existing tasks if you have tasks when loading the page already.
$(function() { // When ready
$('.task').each(function() {
initTaskEvents($(this));
});
});
So far it does the same as what you already had. But then when you insert a new task in the DOM, you run it through the function as well.
// ...
tasklist.append(myNewTask);
initTaskEvents(myNewTask);
// ...
Does that make sense? It is the common way to do it.
Now if you only attach events (not your case since you are creating Hammer in the middle), you can use this in jQuery.
$('body').on('.task', 'click', function(event) {
// ...
});
It works on things which don't exist yet because the event is attached to body and delegates. I have seen this mentioned in the comments, but again this does not work in your case because you are creating a Hammer object from your task.
You can achieve this behavior with MutationObserver
as the name implies, it is used to observe mutations on DOM elements. It's the most efficient way to watch DOM changes and the callback is invoked after the DOM updated.
const taskList = $('#task-list');
const observer = new MutationObserver((mutation, observer) => {
// Looping only on added nodes
mutation[0].addedNodes.forEach(node => {
// Do something with added node
})
})
const observerOts: MutationObserverInit = { attributes: true, childList: true, subtree: true };
observer.observe(taskList[0], observerOts);
here's a working example
Not sure if I am understanding the problem exactly, but everytime I have to loop a list of dynamic elements I use an event delegation approach, and it looks like this is what you need here.
Basically, it consists in attaching the event listener to the parent and then use event.target to get the element you need to work with.
Here is a great explanation by David Walsh: https://davidwalsh.name/event-delegate
from what I understand from your question is that you want to addEventsListener to elements which returned from Hammer class right? so can you post your Hammer class too ?
you can try:
$(mc).on('panright', function() { ... })
been struggling with this for a while now. Really hope someone can help me out.
Im trying to make divs which are removable when you click onto them.
They have a css-class 'tag-show' which is added and removed (this works), so the selector seems to be fine i guess..?
Why is the $(this).remove() not working?
$(document).ready(function() {
// selectors
var module = $(".divCreate");
var list = module.find(".listTag");
var button = module.find(".divButton");
// the actual issue
button.click(function() {
list.append("<div class='tag'>Tag</div>");
setTimeout(function() {
list.find(".tag").last().addClass("tag-show").on("click", function() {
$(this).removeClass("tag-show");
setTimeout(function() {
$(this).remove();
},190);
});
},40);
})
});
"this" loses it's context in your setTimeout.
add a var containing this and use that instead.
like so
var self = $(this);
setTimeout(function() {
$(self).remove();
},190);
I think that would fix it.
You should use the jQuery .on() function instead of .click(), as it works for dynamically created elements. Then you also don't have to apply it to the element each time it is dynamically added.
Somewhere else in your code within $(document).ready(function() {}); add this event handler:
$('.listTag').on('click', 'div.tag', function() {
$(this).remove();
});
and it should work for any element that is matched by the div.tag selector.
this inside the setTimeout callback is not referring to the div anymore. You could bind the callback to the div or do something like this:
var $this = $(this);
setTimeout(function() {
$this.remove();
},190);
I have made some improvments to your code, adding comments where it was needed. Just check the code below and its comments.
Your problem with remove is that the this in your case is "triggering" the closest parent, which is setTimeout, not clicked element.
For a better understanding just try to call console.log(this); inside timeout function and click function, you will see the difference.
I have made an fiddle which can help you understand better (open developer tools to see the console result)
$(document).ready(function() {
// when you keep DOM elements in variables is better to put $ in the beginning
var $module = $(".divCreate");
// faster than .find()
var $list = $(".listTag", $module);
// can not call 'on' with variable
//var $button = $(".divButton", $module);
// called through document since we need to handle dynamic added elements - check event delegation
$(document).on("click", ".divButton", function() {
$list.append("<div class='tag'>Tag</div>");
setTimeout(function() {
// some improvments
$(".tag", $list).last().addClass("tag-show");
}, 40);
// Just keep this if you have divButton attached to an anchor element
// Useful for preventing default behvaiour - in this case adding "#" to url
return false;
});
// Do not need to create the event inside that event
$(document).on("click", ".tag-show", function() {
// Since we use an element more than once is better to
// add it into a variable to avoid performance issues - js caches it and call the variable
var $el = $(this); // our needed 'this'
$el.removeClass("tag-show");
setTimeout(function() {
// 'this' here returns some properties of window (where setTimeout belongs), we need to call element cached above
//(the 'this' above contains what we need)
$el.remove();
}, 190);
});
});
Note : still do not understand why you need these timeouts but it s up to you, maybe you need them with a bigger interval :D
Not sure what you're trying to accomplish, but perhaps this is a better way to accomplish the same task?
// selectors
var module = $(".divCreate");
var list = module.find(".listTag");
var button = module.find(".divButton");
// the actual issue
button.click(function() {
list.append("<div class='tag'>Tag</div>");
})
$(document).on('click','.tag', function(){
$(this).remove();
});
https://jsfiddle.net/7daffjh8/
I have a function, which at the end of its task assigns a button to a new id.
function altChecker() {
var doc = document,
out = [],
appButton = doc.getElementById('appButton'),
//re = /click-me/gi,
output = doc.createElement('p');
output.setAttribute('id', 'output');
EventUtility.addHandler(appButton, 'click', function(e) {
//I have not included all the function details to concentrate on the question
appButton.id = 'appButtonNextChecker';
var appButtonNextChecker = doc.getElementById('appButtonNextChecker');
nextChecker(appButtonNextChecker);
});
}
function nextChecker(newBtnName) {
EventUtility.addHandler(newBtnName, 'click', function(e) {
$('#output').innerHTML = "";
console.log('next Checker, button!')
});
}
So basically there is one button in the DOM assigned to appButton ID initially, and then I change it doing:
appButton.id = 'appButtonNextChecker';
when the altChecker function fires...
Then I assign the button to a new variable, and pass in the variable to the next function...
var appButtonNextChecker = doc.getElementById('appButtonNextChecker');
nextChecker(appButtonNextChecker);
While I can see the buttons' ID change in the DOM, and I see the console.log fire in the nextChecker function,
$('#output').innerHTML = ""; //doesn't fire
AND the altChecker function fires as well (again)?! Haven't I severed the connection to the click function when I reassigned the new ID?
Any help would be appreciated!
Javascript doesn't remember that you initially attached the event through it's id. The event is attached to the element itself, not the ID. It's not like CSS that way.
In fact your variables are still holding the same element as well, so there's no need to create a new variable after changing the ID, either. Since you're using jQuery you can just type $(appButton).unbind(); to remove the event handler. You may also want to look into .on() and .off()
The problem is that you're trying to use the innerHTML property in a jQuery's object.
That property belongs to Element, and it will not work in the way you're using it.
You can use the document.getElementById method, and it will work fine:
document.getElementById('output').innerHTML = '';
Or you can use jQuery's html method:
$('#output').html('');
And you can even use the first element of the jQuery's array, and use innerHTML again:
$('#output')[0].innerHTML = '';
It's up to you, but the first option will be faster, for sure.
I have a function that uses jQuery to add in an additional file upload button when a user adds a file. My problem is that I cannot seem to have it either add in proper format or add every time. My current function simply tries added the string directly:
jQuery(document).ready(function($) {
$(function() {
$("input:file").change(function(){
$("input:file").after("</td></tr><tr><td class=\"field_name span4\"><strong></strong></td><td class=\"field_option\"><input type=\"file\" name=\"pictures\">");
});
});
});
You can see a live version of this here: http://1ro.co/salem/?module=insert
The issue with the method shown above is it does not add the first two closing tags: </td></tr>.
I've tried methods such as setting $("input:file"); to a variable, however that doesn't set for every value after the first. For example:
var count = 0;
jQuery(document).ready(function($) {
$(function() {
var input = $("input:file");
input.change(function(){
input.after(input.get(count++));
});
});
});
However this doesn't append at all (in theory would probably append every element), and I cannot use .after() on input.get(count).
In simple terms, I'm looking to improve this and make it so it will append the a new file upload button, and not being formatted improperly. If possible, I would rather use method 2 but at the moment I would like for it to just work.
You'll need to re-add the handler to each new file input, so best to isolate that in a function:
var addfile = function() {
var newRow = $("<tr><td class=\"field_name span4\"><strong></strong></td>" +
"<td class=\"field_option\"><input type=\"file\" name=\"pictures\">").
insertAfter($(this).closest('tr'));
newRow.find('input:file').change(addfile);
};
$("input:file").change( addfile );
I just tried out your live demo and it is inserting something. But it only happens once of course as your event handler is bound only to the first object.
I would try your change function using something like the following:
$('input:file').change(function() {
$(this).closest('tr').after($(this).closest('tr').clone());
});
but your change handler will have to be rebuilt as well.