I get an error in a doubleclick event: Cannot read properties of null (reading 'addEventListener') at HTMLDocument.addDeleteListener
for this portion of code...
const cardElement = document.querySelector('.card');
function addDeleteListener() {
cardElement.addEventListener('dblclick', (event) => {
event.target.remove();
});
}
document.addEventListener('DOMContentLoaded', addDeleteListener);
the cards are present in the page, and the selector is properly .class (they are added via this function):
function addCard(cardTitle, column) {
const card = document.createElement('div');
card.classList.add('card');
card.innerText = cardTitle;
column.appendChild(card);
}
I cannot seem to work it out :(
ayone can help please?
EDIT:
I have followed some of your advices, for which i thank you all very much, and i have used a different approach...
the new code, now working, is th efollowing:
// Function to add a new card to a column
function addCard(cardTitle, column) {
const card = document.createElement('div');
card.classList.add('card');
card.innerText = cardTitle;
column.appendChild(card);
card.addEventListener('dblclick', () => {
card.remove();
});
}
You can define the event listener on some parent container which isn't inserted dynamically, for example, body.
document.body.addEventListener('dblclick', (event) => {
if (event.target.classList.contains('card')) {
event.target.remove();
}
});
This way you:
Define only one event listener
Don't need to manage event listeners when dynamically adding new elements
As other noticed, the problem is that .card element is not in the dom when DOMContentLoaded event fires. The reason may be different: current info about problem is not enough because we don't know when exactly addCard function called. Seems like the const cardElement = document.querySelector('.card'); stays beore that call.. Of before your column element insertion into the dom (or even deeper).
In addition, I agree with recommendation to move the listener creation inside addCard function.
In addition to Anton's comment:
There is two variants of listeners creation:
Directly to clickable element;
To their static parent (Anton's way);
For example:
A - static parent
B - clickable element (.card in your case)
C - any child of B element
In the 2 (second) case, listener will broke when you add any C to the B element because when you click at the C element, the event.target will be a C element.
That means that you must add extra logic to check which B element contains that event.target. It will require extra dom manipulations (direct or reversed).
In addition, it will be a kind of interruption into A wrapper. It is not a problem, when A is special wrapper for cards, but on other case it is not handy.
In the 1 (first) case, listener will got click event, no matter how deep the C element was.
The A element will be untouched.
The listener's lifecycle is more clear: it lives as long as B element exist.
In future you will be able to move button into isolated component without breaking changes.
The 1 case is a more safe and clear way. Yes, it requires a bit work at B's creation and delete methods, but it's worth it.
In addition, amount of click listeners was never a bit notable problem in my experience.
The second option has the right to life, but you MUST know why and what you are doing.
Sorry for my eng xD
Related
I've the following html structure
<body data-page="first">
<div class="start">Test</div>
</body>
and the following js
$('body[data-page="first"] .start').on('click',function (){
body.attr('data-page','second');
});
$('body[data-page="second"] .start').on('click',function (){
console.log('Test');
});
I would expect, that after the second click on .start, the console would show "Test", but it doesn't...
Can you tell me what I'm doing wrong?
Thanks in advance!
While you have your answer, I don't think the essential point has been made in any of the answers so far, and that is that the binding of an event handler must happen after the target element exists.
When you try to bind an event handler to a particular element in the DOM, the element must exist at the time. If it does not exist, the handler has nothing to bind to, and so the binding fails. If you later create the element, it's too late, unless you re-run the binding statement.
It will soon become second nature to call appropriate event handler binding statements after you create a new element (by modifying the HTML using javascript) that needs a handler.
For instance, in my current project I regularly make AJAX calls to a server to replace blocks of HTML as things happen on the page. Even if some of the new elements are exactly the same as the ones being replaced, they will not inherit any bindings from the replaced elements. Whenever I update the HTML I call a function that contains necessary statements to bind my event handlers to the new copy of the active elements.
Your code would work if you made the following change:
$('body[data-page="first"] .start').on('click',function ()
{
body.attr('data-page','second');
$('body[data-page="second"] .start').on('click',function (){
console.log('Test');
});
})
A couple of other (off-topic, but related) points:
It's possible to bind a handler to an element multiple times. The trick to avoiding this is to include the .off() method in the chain before binding (noting though that .off("click") will unbind all click handlers bound to that element, not just yours) e.g.
$("#mybutton").off("click").click(function(){myHandler()});
"the arrow function doesn’t have its own 'this' value" () so don't use arrow functions in event handlers if you plan to reference any of the element's properties via 'this'. e.g.
$("#mybutton").off("click").click(() => {console.log(${this.id})}); // >> "undefined"
The issue is that the page is rendered with the data-page set to first, and when you click again on it, that part of javascript still see "first", since is not rerendered, so you need a dynamic function, the read all the intereaction with that button, and than check wich value that attribute has. Like this you can make infinite cases, and still go on.
$('body .start').on('click',function (){
const attr = $('body').attr('data-page');
if(attr === 'first') {
$('body').attr('data-page','second');
} else {
console.log('second');
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<body data-page="first">
<div class="start">Test</div>
</body>
And if you don't like the fact that is targetting all the "body" wich is weird, becouse you should have only 1 body, you can use an ID to target the right one
PS: is never a good idea to duplicate your function, if you can set everything in a dynamic function, that reads everything, is easier to debug in the feature, and is lighter and more clean to work on
$('body[data-page="first"] .start').click(function (){
var body = $('body[data-page="first"] .start');
body.attr('data-page','second');
});
This method can help :
var timesClicked = 0;
$('.start').on('click',function (){
timesClicked++;
if (timesClicked>1) {
console.log('Test');
}
});
I have a script which is obscured (ie cannot read the div elements from script). Upon rendering in inspect DOM element I can locate what the div I want to assign a function to. However I cannot do this. Do I need the div id within the script (ie obscured div id)?
I want to assign click on function and plan do it as follows:
<script>
$(document).ready(function () {
$('.contentoverlay').on('mouseover', function() {
click();
});
});
</script>
The script in itself is very long so I won't paste it here.
Overall I want to know:
Do I need to locate the obscure script's div is and assign my click function to it
Is the function above correct
If there are multiple overlays do I have to assign the function above to each one (the overlays in themselves are mouse pointer none, so they do pass down clicks, but would the function above get passed down or even blocked if I don't assign it to all the overlays)?
example: http://output.jsbin.com/joxetuc
First, i want explain my comment first, then i will answer your 3 questions in the end
A) honestly i have bad english here, i dont know what "obscured" means (even i google this), but i think that's means new DOM that newly created (dynamic created element) by javascript. like this:
started DOM <table></table>
New DOM <tr><td>texttt</td></tr>
the result <table><tr><td>texttt</td></tr></table>
if this not correct, my whole answer below will be wrong.
B) So, in jsbin example, i only give you div#container. And then create new DOM inside div#container when you click created button.
C) i create 1 function to handle if new DOM is clicked, which will write $(element).text() to div.log
//function to execute if element clicked
$('.container').on('click', '#newid', function(i,e){
var txt = $(this).text();
$('.log').append(txt+' ');
});
D) i handling event mouseover too with same function in click function. Because i'm lazy, i only write $(this).click(); which $(this) is refer to div#newid
//if mouseover, perform click
$('.container').on('mouseover', '#newid', function(i,e){
$(this).click();
});
so, in that jsbin i show you how to handle mouseover new DOM to execute click() function.
Now, Your Question
1) Do I need to locate the obscure script's div is and assign my click function to it?
You dont need it, as long as you know how to locate that element.
2) Is the function above correct?
that's not correct function. the correct 1 is:
parentElemenentOfNewDOM.on('mouseover', newDOMElement, function() {
$(this).click(); //this will perform your default click function that you give before
});
3) If there are multiple overlays do I have to assign the function above to each one (the overlays in themselves are mouse pointer none, so they do pass down clicks, but would the function above get passed down or even blocked if I don't assign it to all the overlays)?
you dont need to assign same function to handle multiple element (overlays). What you need is the correct script to handle all of your new multiple element. And my script will handle all of the new element.
if you want to know the different, see this DEMO http://output.jsbin.com/vukudu
I have a function which dynamically creates an element and attaches a click event to this new element.
In the current state of my app, this function is called 5 times: for the 4 first created elements, all works fine, but the 5th one has no event attached!
I insist: I'm not merely saying that click doesn't work: Using $._data(myElemn,'events') in the console, I get Object { click=[1]} returned for the 4 working elements, but "undefined" for the last one.
Here is the code.
But I don't think is where the problem lies: since it works for other elements, It seems that the difference should come from the particular context of the 5th element.
So my question is rather: can we imagine which particular conditions may cause and event not to be attached (obviously,without any error message).
var createDDT= function(element) { /*
---------
Creates a drop-down toggle button embedded into element.
*/
$(element).css({position:'relative'}) // (relative: since DDT pos is absolute)
.append(
$('<span \/>').addClass(DDT)
.css({display:'none',})
.append($('<span \/>'))
// when click, toggle submenu:
.click(function(event) {
// hide or show current %Submenu:
var submenu=$(event.target).closest('li').find(jqSUBMENU);
submenu.toggleClass(OPEN);
// hide any other %Open %Submenu:
$(jqOPEN).not(submenu).removeClass(OPEN);
setTimeout(liveWidthDisplay,_params.cssTimeout); // adjust LWD's position
return false; // avoid following link, if embedded in <a>
})
);
}
[EDIT] As I previously said, the issue resides probably outside of the function. To emphasize it, in my app I tried replacing the code by the following:
var createDDT= function(element) { /*
---------
Creates a drop-down toggle button embedded into element.
*/
$(element).css({position:'relative'}) // (relative: since DDT pos is absolute)
.append(
$('<span \/>').addClass(DDT)
.css({display:'none',})
.append($('<span \/>'))
// when click, toggle submenu:
.click(function(event) {
alert(event.target.id);
})
);
}
Then the result is unchanged: $._data(myElem,'events') returns "undefined".
Unfortunately, I can't realistically add a significant context into jsFiddle, since it is a huge app.
can we imagine which particular conditions may cause and event not to
be attached
Yes, either you aren't actually passing your 5th element to the createDDT function - or else you have some other code that is destroying the event handler - possibly innerHTML or some such.
In any case a better way to handle this, that will ensure you get the event is to attach a single event handler higher up in the DOM. For example on the body (although any parent element that exists in the DOM will do).
// presuming DDT is a classname such as ".foo"
$('body').on('click', DDT, function(event) {
alert(event.target.id);
});
This way you have a single event that you don't need to create for each element regardless of it is dynamically added to the DOM or not.
In jQuery, you can do the following:
$('#j_unoffered').on('click', '.icon_del', function () {...
This puts one handler on the element j_unoffered that fires if any descendant element with class icon_del is clicked. It applies, furthermore, to any subsequently created icon_del element.
I can get this working fine in Closure where the click is on the element itself.
goog.events.listen(
goog.dom.getElement('j_unoffered'),
goog.events.EventType.CLICK,
function(e) {...
How can I specify a parent event target in Closure that works for its children/descendants in the same way as the jQuery example?
I'm assuming I need to use setParentEventTarget somehow, but I'm not sure how to implement it for DOM events. Most of the documentation I've found pertains to custom dispatch events.
-- UPDATE --
I'm wondering if there is anything wrong with this rather simple solution:
goog.events.listen(
goog.dom.getElement('j_unoffered'),
goog.events.EventType.CLICK,
function(e) {
if (e.target.className.indexOf('icon_del') !== -1) {...
It still leaves this bound to the parent, but e.target allows a work-around. The fifth argument in listen (opt_handler) allows you to bind this to something else, so I guess that's an avenue, too.
I don't know about such possibility too, so I suggest other piece of code:
var addHandler = function(containerSelector, eventType, nestSelector, handler) {
var parent = goog.isString(containerSelector) ?
document.querySelector(containerSelector) :
containerSelector;
return goog.events.listen(
parent,
eventType,
function(e) {
var children = parent.querySelectorAll(nestSelector);
var needChild = goog.array.find(children, function(child) {
return goog.dom.contains(child, e.target);
});
if (needChild)
handler.call(needChild, e);
});
});
Usage:
addHandler('#elem', goog.events.EventType.CLICK, '.sub-class', function(e) {
console.log(e.target);
});
Update:
If you will use this e.target.className.indexOf('icon_del') there will be possibility to miss the right events. Consider a container div with id = container, it has couple of divs with class innerContainer, and each of them contains couple of divs with class = finalDiv. And consider you will add event handler with your code above, which will check e.target for innerContainer class. The problem is when user will click on finalDiv your handler will be called, but the event target will be finalDiv, which is not innerContainer, but contained by it. Your code will miss it, but it shouldn't. My code checks if e.target has nested class or contained by it, so you will not miss such events.
opt_handler can't really help you either, because there might be many nested elements you want to hanlde (which of them will you pass here? maybe all, but not that helpful, you can get them in event handler whenever you want), moreover they can be added dynamically after, so when you add handler you could not know about them.
In conclusion, I think doing such a job in an event handler is justified and most efficient.
What you are referring to is called event delegation
It seems that this is not possible (out of the box) with Google Closure Library; So my recommandation is to use jQuery or another similar event handling library that offers this functionality. If this is not possible or if you wanna do it by hand here's one possible approach (NOTE: this is not for production use)
var delegateClick = function(containerId, childrenClass, handler){
goog.events.listen(goog.dom.getElement(containerId), goog.events.EventType.CLICK, function(event){
var target = event.target;
//console.log(event);
while(target){
if ( target.className && target.className.split(" ").indexOf(childrenClass)!== -1) {
break;
}
target = target.parentNode;
}
if(target){
//handle event if still have target
handler.call(target, event);
}
});
}
//then use it, try this here: http://closure-library.googlecode.com/git/closure/goog/demos/index.html
//..select the nav context
delegateClick( 'demo-list' ,'goog-tree-icon', function(event){console.log(event);})
Here's a more in depth analysis of event delegation
Again, you should use a proven library for this, here are some options: jQuery, Zepto, Bean
I've been trying to create a game in strictly HTML5 and JavaScript and have run into an issue that I can't seem to wrap my head around. In an attempt to try and avoid using third party classes/libraries, I'm creating my own class for handling custom buttons within the HTML5 canvas element. I almost got them to work and then had to re-write most of the script after realizing that my event listeners kept adding on top of each other every time the canvas redrew (I was using an anonymous function in the mouseDown event listener before, but have since switched to a different method).
First of all, my event listeners now use a function which holds a reference to whichever button I'm trying to use. My prototype's mouseDownFunc is then called, it checks the boundary of the button instance's dimensions, and then finally calls a referenced onPress() (which is actually an overridden method that every button uses, so each button has a custom set of instructions when pressed).
So, if you're still following along (I know, it's a bit confusing without seeing the full script), the problem is that because my event listeners are using the same function, they're overwriting the previous event listener, so only the last button added functions correctly. To sum this all up, how can I add multiple event listeners to the canvas element, which all use the same function without erasing the previous event listeners. Note that I'm trying to do this without the use of jQuery or other third-party extensions.
If more information is needed in regards to my code so that it's easier to understand, let me know. Thanks in advance for any type of feedback.
Edit: Perhaps this might help. Note that this isn't the complete code, but contains the main points:
Adding a button:
this.test_button = new CreateButton(this, 'test_button');
this.test_button.onPress = function() {
alert('Blue button works!');
};
this.test_button.create(200, 50, 30, 200, 'text');
When using create() on a button, variables are checked and stored, as well as an array that holds onto all current buttons (so they can be referenced at any point). Then this is run: this.that.custom_canvas.addEventListener('mousedown', this.create.prototype.mouseDownFunc, false);
When mouseDownFunc is called, this takes place:
CreateButton.prototype.create.prototype.mouseDownFunc = function(e) {
var mouse_x = e.clientX-this.offsetLeft;
var mouse_y = e.clientY-this.offsetTop;
// Check if the mini-button button was actually clicked
if(mouse_x >= test2.x && mouse_y >= test2.y && mouse_x <= (test2.x + test2.width) && mouse_y <= (test2.y + test2.height)){
alert('clicked and working!');
test2.onPress(); // A reference to the saved function
}
};
Currently, my test2 is a reference to any given object -- it's a global var, but after I get these other issues fixed, I'll deal with getting rid of the global var (I know, I know - it was just a temporary quick-fix).
Maybe instead of an event listener for each and every possible button, and checking box size within the function, you could create a single event that calls a routing function to check where on the element the event occurred, and then delegate to another function
You need to design something to handle the event dispatch in your program. You seem to have components that have their listeners all disorganized. You could build a tree data structure (https://en.wikipedia.org/wiki/Tree_%28data_structure%29) that is a hierarchy for the event dispatch in your components ( such as buttons, text areas etc.). The idea is that when the tree is traversed the events will be handled in an ordered fashion. The tree would be reorganized based on how the user interacts with your program. For a simple example, to start this tree could perhaps prioritize the most recently drawn component (out of some structure that holds a list of everything to be drawn) as the event listener to receive event handling first. Then, if a component is blocked by another component the blocked component (like a button covering the button) it's event handling could either be disabled or scheduled to happen later depending on your implementation. Of course your implementation may be more complex, but you need to keep track the event handling. Java uses a component heirarchy data structure to handle a wide variety of GUI events that you can learn more about here http://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html