Dynamically add EventListener in JavaScript - javascript

I am populating a table with an XML file, I have a column that links to more details. Because of the way I'm running the web page (Chrome extension) I need to dynamically add an event handler when the table is populated.
I have this working...
document.addEventListener('DOMContentLoaded', function () {
document.getElementById("detailLink").addEventListener('click',
clickHandlerDetailLink); });
function clickHandlerDetailLink(e) { detailLinkPress('SHOW'); }
function detailLinkPress(str) {
alert("Message that will show more detail");
}
But how do I go about adding the event handler dynamically? I have assigned all the fields in that column the id of detailLink.

You probably need to listen for a mutation event for the table, and then check each time the target element which has fired the event. Previously it used to be these events "DOMNodeInserted", or "DOMSubtreeModified", but they were very slow so according to new specifications the listener is called MutationObserver (which is much faster than the previous ones). This is an example from some Mozilla webpage edited for my testing :
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
alert(mutation.target.id + ", " + mutation.type +
(mutation.addedNodes ? ", added nodes(" + mutation.addedNodes.length + "): " + printNodeList(mutation.addedNodes) : "") +
(mutation.removedNodes ? ", removed nodes(" + mutation.removedNodes.length + "): " + printNodeList(mutation.removedNodes) : ""));
});
});
// configuration of the observer:
var config = { attributes: false, childList: true, characterData: false };
var element = document.getElementById('TestID');
// pass in the target node, as well as the observer options
observer.observe(element, config);
function printNodeList(nodelist)
{
if(!nodelist)
return "";
var i = 0;
var str = "";
for(; i < nodelist.length; ++i)
str += nodelist[i].textContent + ",";
return str;
}

If you want to assign an event to an element that doesn't yet exist, or to a series of elements (without creating one for each element), you need a delegate. A delegate is simply a parent element that will listen for the event instead of all the children. When it handles the event, you check to see if the element that threw the event is the one you're looking for.
If the parent <table> always exits, that would be a good place to add the listener. You can also add it to body. Also, you shouldn't be using detailLink as an id for more than one element. Use class instead.
Demo:
Script:
document.body.addEventListener( 'click', function ( event ) {
if( event.srcElement.className == 'detailLink' ) {
detailLinkPress( 'SHOW' );
};
} );
function detailLinkPress( str ) {
alert("Message that will show more detail");
};
HTML:
<div class="detailLink">click me</div>

Related

How to not lose my event listener in Javascript each time I use .insertBefore?

I am attempting to move a div (which contains text and other elements) each time the user clicks the up or down arrow. The goal is to simply allow users to rearrange divs to their liking, based on clicking the appropriate arrows.
The issue I'm running into is -- when I use .insertBefore to rearrange these elements? It only allows me to click the arrow and perform that action once. After that, nothing happens. It appears that I'm losing my event listener each time I do this. Is there a way for me to somehow say: "Keep that old event listener after it gets moved?"
Here is the code:
for (let i = 0; i < NumberOfSavedIdeas; i++) {
document.getElementById('DeleteIdeaButton' + [i]).addEventListener('click', () => {
var DivToDelete = document.getElementById("SavedIdeaDiv" + [i]);
DivToDelete.remove();
let ArrayContentMatch = FullSavedIdeasArray.indexOf(FullSavedIdeasArray[i])
FullSavedIdeasArray.splice(ArrayContentMatch, 1);
chrome.storage.local.set({SavedIdeas: FullSavedIdeasArray});
});
document.getElementById('MoveIdeaUp' + [i]).addEventListener('click', () => {
var DivToMove1 = document.getElementById("SavedIdeaDiv" + [i]);
var ParentDiv1 = document.getElementById("DivTesting");
ParentDiv1.insertBefore(DivToMove1, ParentDiv1.children[i-1]);
});
document.getElementById('MoveIdeaDown' + [i]).addEventListener('click', () => {
alert('move down')
});
}
Thanks!
This is because new element doesn't exist in DOM when all event listeners are set. Add an event listener on document instead of an element.
It should be wrapped like:
for (let i = 0; i < NumberOfSavedIdeas; i++) {
document.body.addEventListener( 'click', function (e) {
if (e.target.id == 'YourId' + [i]) {
yourFunction();
}
} );
}

GetelementbyID() when the element is dynamically created by js [duplicate]

In JQuery I can do:
$(document).on("click","a.someBtn",function(e){
console.log("hi");
});
to add an event listener to an element that doesn't exist yet. I cannot seem to figure out how to add an event listener to an element that does not exist yet in vanilla javascript.
The following does not work obviously:
query.addEventListener( "click", someListener );
Edit
What I would like to do is compare the item by query selectors. I am selecting the element that does not exist yet with querySelectorAll. It is a little more dynamic than just checking the tag name.
Use the target property in the event object to get the clicked element. Then, manually test for type/attributes/ids
document.addEventListener( "click", someListener );
function someListener(event){
var element = event.target;
if(element.tagName == 'A' && element.classList.contains("someBtn")){
console.log("hi");
}
}
You can use event.target
A reference to the object that dispatched the event.
Code
(function () {
"use strict";
document.getElementsByTagName('body')[0].addEventListener('click', function(e) {
if (e.target.tagName == 'A' && e.target.classList.contains("someBtn")) {
alert('Clicked');
}
}, false);
})();
(function() {
"use strict";
var a = document.createElement('a');
a.textContent = 'Click Me';
a.href = '#';
document.body.appendChild(a);
document.getElementsByTagName('body')[0].addEventListener('click', function(e) {
if (e.target.tagName == 'A') {
alert('Clicked');
}
}, false);
})();
What you want is to use DOM MutationObserver Events to apply the addEventListener. This DOM API is available on all major browser since 2012 I think.
I use this on to lower the google translator bar created by their snippet (https://www.w3schools.com/howto/howto_google_translate.asp). Since it creates the element dynamically (an iframe), it is the same problem you have. Just change the callback function and variables for your need.
//Observer for Google translator bar creation and action to move to bottom
// Select the nodetree that will be observed for mutations
var nodetree = document.getElementsByTagName("body")[0];
// Select the target node atributes (CSS selector)
var targetNode = "iframe.goog-te-banner-frame";
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: true };
// Callback function to execute when mutations of DOM tree are observed
var lowerGoogleTranslateBar = function(mutations_on_DOMtree) {
for(var mutation of mutations_on_DOMtree) {
if (mutation.type == 'childList') {
console.log(mutation);
if (document.querySelector(targetNode) != null) {
//40px is the height of the bar
document.querySelector(targetNode).style.setProperty("top", "calc(100% - 40px)");
//after action is done, disconnect the observer from the nodetree
observerGoogleTranslator.disconnect();
}
}
}
};
// Create an observer instance linked to the callback function
var observerGoogleTranslator = new MutationObserver(lowerGoogleTranslateBar);
// Start observing the target node for configured mutations
observerGoogleTranslator.observe(nodetree, config);
You can learn more about this here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
Here's a function that will let you add "live" events like jQuery's .on. It can be invoked like this:
addLiveListener(scope, selector, event, function reference);
Take a look at the function comment for the description of each of those parameters.
/**
* Adds a istener for specific tags for elements that may not yet
* exist.
* #param scope a reference to an element to look for elements in (i.e. document)
* #param selector the selector in form [tag].[class] (i.e. a.someBtn)
* #param event and event (i.e. click)
* #param funct a function reference to execute on an event
*/
function addLiveListener(scope, selector, event, funct) {
/**
* Set up interval to check for new items that do not
* have listeners yet. This will execute every 1/10 second and
* apply listeners to
*/
setInterval(function() {
var selectorParts = selector.split('.');
var tag = selectorParts.shift();
var className;
if (selectorParts.length)
className = selectorParts.shift();
if (tag != "") {
tag = tag.toUpperCase();
var elements = scope.getElementsByTagName(tag);
} else
var elements = scope.getElementsByClassName(className);
for (var i = 0; i < elements.length; i++) {
if (elements[i][event + '_processed'] === undefined && (tag == "" || elements[i].tagName == tag)) {
elements[i].addEventListener(event, funct);
}
}
}, 1000);
}
And here's a full working demo:
/**
* Adds another anchor with no events attached and lets
* our other code auto-attach events
*/
var currentAnchor = 3;
function addAnchor() {
currentAnchor++;
var element = document.createElement('a');
element.href = "#";
element.innerHTML = "Anchor " + currentAnchor;
element.className = "someBtn";
document.getElementById("holder").appendChild(element);
}
/**
* Adds a istener for specific tags for elements that may not yet
* exist.
* #param scope a reference to an element to look for elements in (i.e. document)
* #param selector the selector in form [tag].[class] (i.e. a.someBtn)
* #param event and event (i.e. click)
* #param funct a function reference to execute on an event
*/
function addLiveListener(scope, selector, event, funct) {
/**
* Set up interval to check for new items that do not
* have listeners yet. This will execute every 1/10 second and
* apply listeners to
*/
setInterval(function() {
var selectorParts = selector.split('.');
var tag = selectorParts.shift();
var className;
if (selectorParts.length)
className = selectorParts.shift();
if (tag != "") {
tag = tag.toUpperCase();
var elements = scope.getElementsByTagName(tag);
} else
var elements = scope.getElementsByClassName(className);
for (var i = 0; i < elements.length; i++) {
if (elements[i][event + '_processed'] === undefined && (tag == "" || elements[i].tagName == tag)) {
elements[i].addEventListener(event, funct);
}
}
}, 1000);
}
/**
* Now let's add live listener for "a" tags
*/
addLiveListener(document, "a.someBtn", "click", function() {
alert('Clicked ' + this.innerHTML);
});
a {
margin-right: 10px;
}
<!-- Add some pre-existing anchors -->
<p id="holder">
Anchor 1Anchor 2Anchor 3
</p>
<!-- A button to add dynamic new anchors -->
<input type="button" value="Add anchor" onclick="addAnchor();" />

Binding to content change in body with jQuery

I have a function from icomoon that runs when the window loads (see below).
I would like to change it so that the function gets called once when the document loads, and then if there are any subsequent changes to the body - eg: from js/ajax (and preferably then only on the changed part of the dom, so as not to loop through the entire document again and again). Any suggestions on what jquery on events I should use for this, and then to only check the changes once the first execution on the entire document has been completed? Needs to be Ie7+ compatible too.
Thanks much.
$( window ).load(function() {
function addIcon(el, entity) {
$(el).addClass("iconed");
var html = el.innerHTML;
el.innerHTML = '<span style="font-family: \'icomoon\'">' + entity + '</span>' + html;
}
var icons = {
.....
};
function iconify() {
var els = document.getElementsByTagName('*'),
i, attr, c, el;
for (i = 0; ; i += 1) {
el = els[i];
if(!el) {
break;
}
attr = el.getAttribute('data-icon');
if (attr) {
if (!$(el).hasClass("iconed")) {
addIcon(el, attr);
}
}
c = el.className;
c = c.match(/icon-[^\s'"]+/);
if (c && icons[c[0]]) {
if (!$(el).hasClass("iconed")) {
addIcon(el, icons[c[0]]);
}
}
}
}
iconify();
$('body').on("contentchanged", function() { //some event that triggers ONCE the document has fully loaded, and is triggered when the DOM changes..
iconify(); //would prefer if this function only checked the modified part of the DOM - rather than the entire DOM each time (except on the 1st execution - when window loaded.
});
});
You can fire custom events when the content of body is changed by ajax or any other function which you are aware of . And then bind a custom function to that event.
Let's say in
function addIcon(el, entity) {
$(el).addClass("iconed");
var html = el.innerHTML;
el.innerHTML = '<span style="font-family: \'icomoon\'">' + entity + '</span>' + html;
$(document).trigger('contentchanged');
}
And then you can write a custom function handling this event like:
$(document).on('contentchange','selector',function(){
//Your code goes here.
});
You can also refer for DOM mutation events

jQuery .change() event is only fired once

I have an application that retrieves Project names from a database when the DOM is ready. Each Project is added to a <select><option> in an html <form>. Once the list is populated the user can select a project title, which will request the remaining information from the database specific to that project.
To achieve this I'm using the $.change() jQuery method. Unfortunately, the event is only fired once when the <select> element is created and added to the DOM. Selecting another project from the list does not fire the event, and therefore does not trigger a $.post() call.
$(function(){
getProjects();
var firstLoad = true;
$("select").change(retrieveProject); // Removed parenthesis based on answers
// On page load, get project names from the database and add them to a Select form element
function getProjects() {
var selectionList;
$.getJSON("php/getProjects.php", function (data) {
selectionList = "<form><select>";
for (var i = 0; i < data.length; i++) {
selectionList += "<option name='prjTitle'>" + data[i].ProjectTitle + "</option>";
}
selectionList += "</select></form>";
}).complete(function() {
$('#project-selection-menu').append(selectionList).removeClass('hidden');
firstLoad = false;
});
}
function retrieveProject() {
if ( firstLoad == true ){
alert(firstLoad); // This alert fires
return false;
} else {
alert(firstLoad); // This alert doesn't fire
$.post("php/getProjects.php", function (data) { // This should have been "php/retrieveProject.php"!
// Do stuff with the returned data
}).complete(function() {
console.log("Success.");
});
}
}
)};
You're not setting up the event handler properly:
$("select").change(retrieveProject);
In your code, you were calling the "retrieveProject" function, and the return value from that function call was being passed as the "change" handler (and of course having no effect). That's why it appeared that the event was being generated upon page load.
When you're working with a function as a value, you don't use () after the function reference — it's the reference itself (the function name, in this case) that you want. That's what needs to be passed to jQuery.
Also — and this is important — make sure that your code is run either in a "ready" or "load" handler, or else that your <script> comes after the <select> element on the page. If the script is in the document head, then it'll run before the DOM is parsed, and it'll have no effect. (Another way to deal with that would be to use an .on() delegated form as suggested in another answer.)
More: it looks like you're overwriting your <select> element when you fetch the content in "getProjects". Thus, you should definitely use the delegated form:
$(document).on("change", "select", retrieveProject);
Also, you should be using local variables in "getProjects":
function getProjects() {
var selectionList; // keep this local to the function - implicit globals are risky
$.getJSON("php/getProjects.php", function (data) {
selectionList = "<form><select>";
for (var i = 0; i < data.length; i++) {
selectionList += "<option name='prjTitle'>" + data[i].ProjectTitle + "</option>";
}
selectionList += "</select></form>";
}).complete(function() {
$('#project-selection-menu').append(selectionList).removeClass('hidden');
firstLoad = false;
});
}
You need to handle event delegation
$(document).on('change', 'select', retrieveProject);
Also remove () next to the method retrieveProject
you can also do this by using following which will work fine.
$("select").change(function(){retrieveProject()});
or
$("select").on('change',function(){retrieveProject()});
Is this what your looking for? To run getProjects once the page loads just call it in your $(document).ready() function. Also you need to properly setup your change handler. See the fiddle for reference.
var firstLoad = true;
getProjects();
$("#selectTest").change(function(){
retrieveProject();
});
// On page load, get project names from the database and add them to a Select form element
function getProjects() {
$.getJSON("php/getProjects.php", function (data) {
selectionList = "<form><select>";
for (var i = 0; i < data.length; i++) {
selectionList += "<option name='prjTitle'>" + data[i].ProjectTitle + "</option>";
}
selectionList += "</select></form>";
}).complete(function() {
$('#project-selection-menu').append(selectionList).removeClass('hidden');
firstLoad = false;
});
}
function retrieveProject() {
if ( firstLoad == true ){
alert(firstLoad); // This alert fires
return false;
} else {
alert(firstLoad); // This alert doesn't fire
$.post("php/getProjects.php", function (data) {
// Do stuff with the returned data
}).complete(function() {
console.log("Success.");
});
}
}
http://jsfiddle.net/trevordowdle/Mf38E/
Try this:
$(document).bind('change', 'select', function(){
retrieveProject();
});

Detect element style change in chrome

I'm trying to find a way to detect changes to the element style but I haven't had much luck. The code below works on a new property I define like tempBgColor but I cannot override/shadow an existing property like color. I know jquery has a watch function, but it only detects changes from the jquery api but not directly changing the value of a style something like elem.style.color.
var e = document.getElementById('element');
e.style.__defineGetter__("color", function() {
return "A property";
});
e.style.__defineSetter__("color", function(val) {
alert("Setting " + val + "!");
});
Any pointers?
You should be able to do this with a MutationObserver - see demo (Webkit only), which is the new, shiny way of getting notified about changes in the DOM. The older, now deprecated, way was Mutation events.
Demo simply logs in the console the old and new values when the paragraph is clicked. Note that the old value will not be available if it was set via a non-inline CSS rule, but the change will still be detected.
HTML
<p id="observable" style="color: red">Lorem ipsum</p>​
JavaScript
var MutationObserver = window.WebKitMutationObserver;
var target = document.querySelector('#observable');
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('old', mutation.oldValue);
console.log('new', mutation.target.style.cssText);
});
});
var config = { attributes: true, attributeOldValue: true }
observer.observe(target, config);
// click event to change colour of the thing we are observing
target.addEventListener('click', function(ev) {
observable.style.color = 'green';
return false;
}, false);
Credit to this blog post, for some of the code above.
With Chrome's Developer Tools open, you can find the element whose style's change you're interested in, right click it, select "Break on..." and "Attributes modifications".
here is a naive implementation using setTimeout with undescorejs.
The only way to find out which change was made is to iterate through the style object properties.
Here is the live example
$( function () {
var ele = document.getElementById('ele'),
oldStyle = {};
function checkEquality() {
style = _.clone(ele.style);
if (!_.isEqual(style, oldStyle)) {
console.log('Not equal');
oldStyle = _.clone(style);
} else {
console.log('Equal');
}
_.delay(checkEquality, 2000);
}
checkEquality();
$('a#add_prop').on('click', function () {
var props = $('#prop').val().replace(/ /g, '').split(':');
console.log(props);
$(ele).css(props[0], props[1]);
});
$('#prop').on('keydown', function (e) {
if (e.keyCode == 13) {
$('a#add_prop').trigger('click');
}
});
});

Categories

Resources