Implementing jQuery's "live" binder with native Javascript - javascript

I am trying to figure out how to bind an event to dynamically created elements. I need the event to persist on the element even after it is destroyed and regenerated.
Obviously with jQuery's live function its easy, but what would they look like implemented with native Javascript?

Here's a simple example:
function live(eventType, elementId, cb) {
document.addEventListener(eventType, function (event) {
if (event.target.id === elementId) {
cb.call(event.target, event);
}
});
}
live("click", "test", function (event) {
alert(this.id);
});
The basic idea is that you want to attach an event handler to the document and let the event bubble up the DOM. Then, check the event.target property to see if it matches the desired criteria (in this case, just that the id of the element).
Edit:
#shabunc discovered a pretty big problem with my solution-- events on child elements won't be detected correctly. One way to fix this is to look at ancestor elements to see if any have the specified id:
function live (eventType, elementId, cb) {
document.addEventListener(eventType, function (event) {
var el = event.target
, found;
while (el && !(found = el.id === elementId)) {
el = el.parentElement;
}
if (found) {
cb.call(el, event);
}
});
}

In addition to Andrew's post and Binyamin's comment, maybe this is an option:
With this you can use 'nav .item a' as the selector.
Based on Andrew's code.
function live (eventType, elementQuerySelector, cb) {
document.addEventListener(eventType, function (event) {
var qs = document.querySelectorAll(elementQuerySelector);
if (qs) {
var el = event.target, index = -1;
while (el && ((index = Array.prototype.indexOf.call(qs, el)) === -1)) {
el = el.parentElement;
}
if (index > -1) {
cb.call(el, event);
}
}
});
}
live('click', 'nav .aap a', function(event) { console.log(event); alert('clicked'); });

The other solutions are a little overcomplicated...
document.addEventListener('click', e => {
if (e.target.closest('.element')) {
// .element has been clicked
}
}
There is a polyfill in case you need to support Internet Explorer or old browsers.

An alternative to binding an event to dynamically to a specific element could be a global event listener. So, each time you update the DOM with another new element event on that element will also the "catches". An example:
var mybuttonlist = document.getElementById('mybuttonlist');
mybuttonlist.addEventListener('click', e=>{
if(e.target.nodeName == 'BUTTON'){
switch(e.target.name){
case 'createnewbutton':
mybuttonlist.innerHTML += '<li><button name="createnewbutton">Create new button</button></li>';
break;
}
}
}, false);
ul {
list-style: none;
padding: 0;
margin: 0;
}
<ul id="mybuttonlist">
<li><button name="createnewbutton">Create new button</button></li>
</ul>
In this example I have an event listener on the <ul> for click events. So, an event happens for all child elements. From the simple event handler I created, you can see that it is easy to add more logic, more buttons (with different or repeating names), anchors etc.
Going all in, you could add the eventlistener to document instead of the list element, catching all click events on the page and then handle the click events in the event handler.

Related

Replace jquery $("body").on for child elements with Vanilla Script

how is it possible to replace this jQuery with Vanilla:
$( document ).ready(function() {
$('body').on('click', '.f_click', function (e) {
e.preventDefault();
alert("TEST");
});
});
My first try was:
document.addEventListener('click', function (e) {
console.log(e.target);
if (e.target.classList.contains('f_bme_start')) {
alert('open Search!');
return false;
}
}, false);
this works, but not on child elements.
Has somebody an idea how to solve this?
I want to replace all my jQuery code because of slow performance.....
THANKS
You're only checking the element that was actually clicked, not its ancestor elements.
In modern environments you can use the DOM's closest method (and that link has polyfills for older environments):
document.addEventListener('click', function (e) {
const target = e.target.closest(".f_bme_start");
if (target) {
alert('open Search!');
return false;
}
});
That searches through the ancestors of the clicked element for a match for a given CSS selector. If you were hooking the event on a container element other than the document or document.body, I'd also use contains to make sure the search through ancestors didn't go to an ancestor of the container element:
const target = e.target.closest(".f_bme_start");
if (target && e.currentTarget.contains(target)) {
alert('open Search!');
return false;
}
But there's no need if you're hooking the event on document or document.body.
THANKS a lot!
what is the best solution?
For performance and for compatibility?
I think this one is best?:
document.addEventListener('click', function (e) {
for (var target = e.target; target && target != this; target = target.parentNode) {
console.log(target.classList);
if (target.classList.contains('f_click')) {
alert('open without jQuery!');
return false;
}
}
}, false);

Is there any way to make the onClick global or DRY?

$("input").on("keypress",function(e){
if(e.which===13){
$("ul").last().append("<li>"+$(this).val()+"</li>");
}
$("li").on("click",function(){
$(this).toggleClass("striked");
});
$("li").on("mouseenter",function(){
$(this).css("color","green");
});
});
$("li").on("click",function(){
$(this).toggleClass("striked");
});
$("li").on("mouseenter",function(){
$(this).css("color","green");
});
$("#slide").on("click",function(){
$("input").slideToggle();
});
Here, I have used the onClick event on<li> to apply the striked class two times just to make it work for both dynamic and non-dynamic elements on the page. But the code is replicated and seems long. Is there any way to shorten so that I can write it once and it gets activated for both types of elements?
Use event delegation instead, on the ul, so you only have to set up listeners once, rather than setting up multiple listeners for every element on load and on each .append. Also, save the ul and the input jQuery-wrapped elements in a variable once rather than selecting them and wrapping them with jQuery each time they're used:
const $ul = $("ul");
const $input = $("input");
$input.on("keypress", function(e) {
if (e.which === 13) {
$ul.last().append("<li>" + $(this).val() + "</li>");
}
});
$ul.on("click", 'li', function() {
$(this).toggleClass("striked");
});
$ul.on("mouseenter", 'li', function() {
$(this).css("color", "green");
});
$("#slide").on("click", function() {
$input.slideToggle();
});
A rather generic approach would be to capture the click event and check if it is from ul
document.body.onclick = function(e){
e = e || event;
var from = findParent('ul',e.target || e.srcElement);
if (from){
/* it's a link, actions here */
}
}
//find first parent with tagName [tagname]
function findParent(tagname,el){
while (el){
if ((el.nodeName || el.tagName).toLowerCase()===tagname.toLowerCase()){
return el;
}
el = el.parentNode;
}
return null;
}
now you can change the tagName passed to the findParent function and do accordingly
Read Here
You can try using the jquery all selector $('*'). For more information on this see
https://api.jquery.com/all-selector/.
Or you can add a specific class to every element you want to have an onClick action.

JavaScripts equivalent to jQuery document click with a class

What is the JavaScript equivalent to this jQuery:
$(document).on('click', '.add-star', function (event) {
//event will return the .add-star
})
Markup looks like this
<div class="add-star">
<svg>
<path />
</svg>
</div>
When I do document.addEventListener('click', function(e) {... the e.target gets me the path not the parent add-star. From what I know with the jQuery way it bubbles up on the event looking for the class specified and returns that in the event. But there is no class specified with the JS event, so it returns just the immediate clicked element, the path from the svg.
How would I return add-star from the js event?
It's pretty easy. You just use .matches() on each element starting at e.target, traversing through each .parentNode until the bound element. When/if a match is found, you call the function.
So create a function that receives the callback and returns a new function handles this operation.
function delegate(selector, handler) {
return function(event) {
var el = event.target;
do {
if (el.matches(selector)) {
handler.call(el, event);
}
} while ((el = el.parentNode) && el !== this);
};
}
Then call that function to create the handler.
document.addEventListener('click', delegate('.add-star', function (event) {
//event will return the .add-star
}));
You have two main ways of handling events here, the event delegation method is similar to what your jQuery example is doing so I'll make that #1. This method uses e.target.matches to accomplish checking for an element that might not exist. The second method is for more traditional elements and uses document.querySelector
Method 1 delegated events
document.addEventListener('click', e => {
if (!e.target.matches('.add-star')) { return }
// do stuff
});
Method 2 non-dynamic selectors
let ele = document.querySelector('.add-star');
ele.addEventListener('click', e => { // do stuff });

How to detect if mouse cursor is out of element?

I have a listener which runs when I click on document.
document.addEventListener('click', print);
function print(element)
{
doSomething();
}
It creates div id=panel, where I print some information.
When I run the print function I would like to detect whether I clicked outside of the div#panel (The panel exists when I click second time).
I wish not to use the mouseout event listener because I think it is redundant to use listener for mouse movements when the event click is already fired.
How to detect when I clicked out of div#panel?
You can check the target of jQuery's click event, which element it was:
$(document).click(function(e) {
var target = $(e.target);
if( !target.is("#panel") && target.closest("#panel").length === 0 ) {
// click was not on or inside #panel
}
});
Your event handler gets passed an event object, not an element. Since you are listening for the click event, the event will be of type MouseEvent and that event object will have a target property which you can use to check if the target element matches your desired element.
function handler(event) {
if (event.target == document.getElementById("panel")) {
// Do stuff
}
}
document.addEventListener('click', handler);
Edit: I intentionally gave the vanilla JS answer since your own code fragments don't use jQuery. But jQuery wouldn't change anything as its event handling API is almost just a thin wrapper over JS.
I am just using event from the click. Here it is
var elem=document.getElementById("elem");
var rects=elem.getBoundingClientRect();//get the bounds of the element
document.addEventListener('click', print);
function print(e)
{
//check if click position is inside or outside target element
if(e.pageX<= rects.left +rects.width && e.pageX>= rects.left && e.pageY<= rects.top +rects.height && e.pageY>= rects.top){
console.log("Inside element");
}
else{
console.log("Outside element");
}
}
JS Bin link : https://jsbin.com/pepilehigo/edit?html,js,console,output
A different approach, using only javascript is:
function print(evt) {
if (!(evt.target.tagName == 'DIV' && evt.target.classList.contains('myDiv'))) {
var div = document.createElement('div');
div.classList.add('myDiv');
div.textContent="new div";
document.body.appendChild(div);
}
}
window.onload = function() {
document.addEventListener('click', print);
}
.myDiv {
border:1px solid green;
}

Binding multiple events to a listener (without JQuery)?

While working with browser events, I've started incorporating Safari's touchEvents for mobile devices. I find that addEventListeners are stacking up with conditionals. This project can't use JQuery.
A standard event listener:
/* option 1 */
window.addEventListener('mousemove', this.mouseMoveHandler, false);
window.addEventListener('touchmove', this.mouseMoveHandler, false);
/* option 2, only enables the required event */
var isTouchEnabled = window.Touch || false;
window.addEventListener(isTouchEnabled ? 'touchmove' : 'mousemove', this.mouseMoveHandler, false);
JQuery's bind allows multiple events, like so:
$(window).bind('mousemove touchmove', function(e) {
//do something;
});
Is there a way to combine the two event listeners as in the JQuery example? ex:
window.addEventListener('mousemove touchmove', this.mouseMoveHandler, false);
Any suggestions or tips are appreciated!
Some compact syntax that achieves the desired result, POJS:
"mousemove touchmove".split(" ").forEach(function(e){
window.addEventListener(e,mouseMoveHandler,false);
});
In POJS, you add one listener at a time. It is not common to add the same listener for two different events on the same element. You could write your own small function to do the job, e.g.:
/* Add one or more listeners to an element
** #param {DOMElement} element - DOM element to add listeners to
** #param {string} eventNames - space separated list of event names, e.g. 'click change'
** #param {Function} listener - function to attach for each event as a listener
*/
function addListenerMulti(element, eventNames, listener) {
var events = eventNames.split(' ');
for (var i=0, iLen=events.length; i<iLen; i++) {
element.addEventListener(events[i], listener, false);
}
}
addListenerMulti(window, 'mousemove touchmove', function(){…});
Hopefully it shows the concept.
Edit 2016-02-25
Dalgard's comment caused me to revisit this. I guess adding the same listener for multiple events on the one element is more common now to cover the various interface types in use, and Isaac's answer offers a good use of built–in methods to reduce the code (though less code is, of itself, not necessarily a bonus). Extended with ECMAScript 2015 arrow functions gives:
function addListenerMulti(el, s, fn) {
s.split(' ').forEach(e => el.addEventListener(e, fn, false));
}
A similar strategy could add the same listener to multiple elements, but the need to do that might be an indicator for event delegation.
Cleaning up Isaac's answer:
['mousemove', 'touchmove'].forEach(function(e) {
window.addEventListener(e, mouseMoveHandler);
});
EDIT
ES6 helper function:
function addMultipleEventListener(element, events, handler) {
events.forEach(e => element.addEventListener(e, handler))
}
ES2015:
let el = document.getElementById("el");
let handler =()=> console.log("changed");
['change', 'keyup', 'cut'].forEach(event => el.addEventListener(event, handler));
For me; this code works fine and is the shortest code to handle multiple events with same (inline) functions.
var eventList = ["change", "keyup", "paste", "input", "propertychange", "..."];
for(event of eventList) {
element.addEventListener(event, function() {
// your function body...
console.log("you inserted things by paste or typing etc.");
});
}
I have a simpler solution for you:
window.onload = window.onresize = (event) => {
//Your Code Here
}
I've tested this an it works great, on the plus side it's compact and uncomplicated like the other examples here.
One way how to do it:
const troll = document.getElementById('troll');
['mousedown', 'mouseup'].forEach(type => {
if (type === 'mousedown') {
troll.addEventListener(type, () => console.log('Mouse is down'));
}
else if (type === 'mouseup') {
troll.addEventListener(type, () => console.log('Mouse is up'));
}
});
img {
width: 100px;
cursor: pointer;
}
<div id="troll">
<img src="http://images.mmorpg.com/features/7909/images/Troll.png" alt="Troll">
</div>
AddEventListener take a simple string that represents event.type. So You need to write a custom function to iterate over multiple events.
This is being handled in jQuery by using .split(" ") and then iterating over the list to set the eventListeners for each types.
// Add elem as a property of the handle function
// This is to prevent a memory leak with non-native events in IE.
eventHandle.elem = elem;
// Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn);
types = types.split(" ");
var type, i = 0, namespaces;
while ( (type = types[ i++ ]) ) { <-- iterates thru 1 by 1
You can also use prototypes to bind your custom function to all elements
Node.prototype.addEventListeners = function(eventNames, eventFunction){
for (eventName of eventNames.split(' '))
this.addEventListener(eventName, eventFunction);
}
Then use it
document.body.addEventListeners("mousedown touchdown", myFunction)
// BAD: One for each event - Repeat code
textarea.addEventListener('keypress', (event) => callPreview);
textarea.addEventListener('change', (event) => callPreview);
// GOOD: One run for multiple events
"keypress change".split(" ").forEach((eventName) => textarea.addEventListener(eventName, callPreview));

Categories