I'm working on a website, with jQuery but I'm trying to not use it anymore. In jQuery you can add an even listener on a element that wasn't on the website or wasn't created yet and no problem. I have elements that are only on the DOM when you're logged in, and I only have one JS file for the whole website.
Problem is, for example, when you're logged in you can't see the "log in" button, it's not even in the DOM, but it still have the event listener in the code, no error on the console, script runs well.
$("#logInButton").on("click", somefunction);
But, using document.querySelector("#logInButton").onclick = somefunction and being logged in already, it throws an error because document.querySelector("#logInButton") is null.
I can do like:
let logInButton = document.querySelector("#logInButton");
logInButton ? logInButton.onclick = somefunction : "";
And it works well, but I know it's not a good practice. Any workaround or improvement to that, not using jQuery?
JSFiddle if what happens. (See console)
And it works well, but I know it's not a good practice.
If having #logInButton on the page is optional, that's perfectly good practice — other than using onclick rather than addEventListener (but that's probably a matter of style). Naturally, you'd have this code in a script linked at the end of the document, just prior to the </body> tag (or trigger it via a DOMContentLoaded callback).
But if you want the equivalent of the jQuery, you need to think in jQuery's "set-based" mindset and use querySelectorAll:
// Not very efficient
document.querySelectorAll("#logInButton").forEach(function() {
// Set up the handler here using `this`
});
Except that jQuery optimizes queries using #id format to a getElementById call (which is dramatically faster) and then uses an if (like yours) to build the set with either one element or zero.
Perhaps in your quest to not use jQuery, you might give yourself a couple of helper functions to take its place, as the DOM API is quite verbose. If you like jQuery's set-based nature, you might even make them set-based:
function MyQuery(selector) {
if (!selector) {
this.data = [];
} else if (typeof selector === "string") {
// (jQuery takes it further than this, search in an unminified version for `rquickExpr`)
var id = /#([\w-]+)/.match(selector);
if (id) {
var e = document.getElementById(id[0]);
this.data = e ? [e] : [];
} else {
this.data = Array.from(document.querySelector(selector));
}
} else {
/* ...handle other things, such as DOM elements or arrays of them...? */
this.data = /*...*/;
}
}
MyQuery.prototype = {
constructor: MyQuery,
on: function(eventName, handler) {
this.data.forEach(function(element) {
element.addEventListener(eventName, handler);
});
return this;
}
// ...etc...
};
function qset(selector) {
return new MyQuery(selector);
}
Then
qset("#logInButton").on("click", /*...*/);
Of course, you might find yourself basically recreating jQuery. But if you keep it lean...
Side note: Using forEach on the return value of querySelectorAll requires an up-to-date browser, or that you polyfill it:
if (typeof NodeList !== "undefined" &&
NodeList.prototype &&
!NodeList.prototype.forEach) {
Object.defineProperty(NodeList.prototype, "forEach", {
value: Array.prototype.forEach
});
}
For truly obsolete browsers (like IE8), you'd have to polyfill Array.prototype.forEach first.
You can do it the same way jQuery does it, using event bubbling.
document.addEventListener('click', function (ev) {
if (ev.target.id === 'someIdHere') {
console.log('click');
}
});
Related
I have a javascript code that is given below that is ES6 compatible however IE 11 does not support this. What would be the replacement code for this such that it works across all browsers?
[...document.querySelectorAll('.row')]
Im using this for 'click' event handling:
Array.prototype.slice.call(document.querySelectorAll('.row'))
.forEach(function(header) {
return header.addEventListener('click', function(e) {
headerClick(e, header, header.querySelector('.exy'))
});
});
For all browsers, you can use Array.prototype.slice via call or apply (it works on any array-like object):
Array.prototype.slice.call(document.querySelectorAll('.row'))
About your updated question:
Im using this for 'click' event handling:
Array.prototype.slice.call(document.querySelectorAll('.row'))
.forEach(function(header) {
return header.addEventListener('click', function(e) {
headerClick(e, header, header.querySelector('.exy'))
});
});
I wouldn't use querySelectorAll for this at all, I'd use event delegation. Presumably all of those .row elements are inside a common container (ultimately, of course, they're all in body, but hopefully there's a container "closer" to them than that). With event delegation, you do this:
Hook click just once, on the container
When a click occurs, check to see if it passed through one of your target elements en route to the container
For your quoted code, that looks something like this:
// A regex we'll reuse
var rexIsRow = /\brow\b/;
// Hook click on the container
document.querySelector("selector-for-the-container").addEventListener(
"click",
function(e) {
// See if we find a .row element in the path from target to container
var elm;
for (elm = e.target; elm !== this; elm = elm.parentNode) {
if (rexIsRow.test(elm.className)) {
// Yes we did, call `headerClick`
headerClick(e, elm, elm.querySelector('.exy'));
// And stop looking
break;
}
}
},
false
);
On more modern browsers, you could use elm.classList.contains("row") instead of the regular expression, but sadly not on IE9 or earlier.
That said, rather than maintaining a separate codebase, as gcampbell pointed out you could use ES6 (ES2015) features in your code and then transpile with a transpiler that converts them (well, the ones that can be converted, which is a lot of them) to ES5 syntax. Babel is one such transpiler.
I am trying to make two checkboxes, out of which only one can be selected at any point of time. I searched the forums a lot and found a few suggestions.
if (document.attachEvent){
// For IE Browsers.
document.attachEvent("DOMContentLoaded", function (event) {
var saSelector = document.querySelector('input[name=saWrite]');
var cgSelector = document.querySelector('input[name=cgWrite]');
if (cgSelector !== null) {
cgSelector.attachEvent('change', function (event) {
if (cgSelector.checked) {
document.querySelector('input[name=saWrite]').checked = false;
}
});
}
if (saSelector !== null) {
saSelector.attachEvent('change', function (event) {
if (saSelector.checked) {
document.querySelector('input[name=cgWrite]').checked = false;
}
});
}
});
}
I wrote a similar function with addEventListener in place of attachEvent for non-IE browsers. That works on Firefox. But this method somehow doesn't work for IE. Am I doing something wrong here? Any suggestions would be helpful. I wish i could use JQuery for this. But i cant.
https://jsfiddle.net/20g7ym8q/
You say you want to use JQuery but you can't. I realize starting out that may seem like a real limitation, but it isn't. Anything you can do with JQuery you can do with JavaScript.
Your code won't work on IE11 because attachEvent has been deprecated and removed in favor of accepting addEventListener as the standard way to attach an event in all modern browsers. If you're looking for generational support without JQuery and without code duplication, setting up your own Object to use as an intermediate layer between your code and the browser is probably the best way to go about this.
function $(ele) {
return {
ele: document.querySelector(ele),
on: function(ev, fn) {
(document.attachEvent) ?
this.ele.attachEvent(ev, fn) :
this.ele.addEventListener(ev, fn);
},
checked: function(change) {
if(typeof change !== undefined) this.ele.checked = change;
return this.ele.checked;
}
}
}
The above is a function that returns an Object with two methods and a property. It works similarly to JQuery for familiarity and consistency, but it is without the overhead of including the entire JQuery library.
The methods allow you to add an event using .on with an event type and function as parameters. The methods also allow you to set or get the checked property of the specified element. .checked() will simply return a boolean as to whether the box is checked, .checked(boolean) will set the elements property to the desired state.
In practice, to solve your dilemma of only one allowable check box, you could do this:
var sa = $('input[name="saWrite"]');
var cg = $('input[name="cgWrite"]');
cg.on('click', function(ev) {
sa.checked(false)
});
sa.on('click', function(ev) {
cg.checked(false);
});
This always gets me. After initializing all lovely UI elements on a web page, I load some content in (either into a modal or tabs for example) and the newly loaded content does not have the UI elements initialized. eg:
$('a.button').button(); // jquery ui button as an example
$('select').chosen(); // chosen ui as another example
$('#content').load('/uri'); // content is not styled :(
My current approach is to create a registry of elements that need binding:
var uiRegistry = {
registry: [],
push: function (func) { this.registry.push(func) },
apply: function (scope) {
$.each(uiRegistry.registry, function (i, func) {
func(scope);
});
}
};
uiRegistry.push(function (scope) {
$('a.button', scope).button();
$('select', scope).chosen();
});
uiRegistry.apply('body'); // content gets styled as per usual
$('#content').load('/uri', function () {
uiRegistry.apply($(this)); // content gets styled :)
});
I can't be the only person with this problem, so are there any better patterns for doing this?
My answer is basically the same as the one you outline, but I use jquery events to trigger the setup code. I call it the "moddom" event.
When I load the new content, I trigger my event on the parent:
parent.append(newcode).trigger('moddom');
In the widget, I look for that event:
$.on('moddom', function(ev) {
$(ev.target).find('.myselector')
})
This is oversimplified to illustrate the event method.
In reality, I wrap it in a function domInit, which takes a selector and a callback argument. It calls the callback whenever a new element that matches the selector is found - with a jquery element as the first argument.
So in my widget code, I can do this:
domInit('.myselector', function(myelement) {
myelement.css('color', 'blue');
})
domInit sets data on the element in question "domInit" which is a registry of the functions that have already been applied.
My full domInit function:
window.domInit = function(select, once, callback) {
var apply, done;
done = false;
apply = function() {
var applied, el;
el = $(this);
if (once && !done) {
done = true;
}
applied = el.data('domInit') || {};
if (applied[callback]) {
return;
}
applied[callback] = true;
el.data('domInit', applied);
callback(el);
};
$(select).each(apply);
$(document).on('moddom', function(ev) {
if (done) {
return;
}
$(ev.target).find(select).each(apply);
});
};
Now we just have to remember to trigger the 'moddom' event whenever we make dom changes.
You could simplify this if you don't need the "once" functionality, which is a pretty rare edge case. It calls the callback only once. For example if you are going to do something global when any element that matches is found - but it only needs to happen once. Simplified without done parameter:
window.domInit = function(select, callback) {
var apply;
apply = function() {
var applied, el;
el = $(this);
applied = el.data('domInit') || {};
if (applied[callback]) {
return;
}
applied[callback] = true;
el.data('domInit', applied);
callback(el);
};
$(select).each(apply);
$(document).on('moddom', function(ev) {
$(ev.target).find(select).each(apply);
});
};
It seems to me browsers should have a way to receive a callback when the dom changes, but I have never heard of such a thing.
best approach will be to wrap all the ui code in a function -even better a separate file -
and on ajax load just specify that function as a call back ..
here is a small example
let's say you have code that bind the text fields with class someclass-for-date to a date picker then your code would look like this ..
$('.someclass-for-date').datepicker();
here is what i think is best
function datepickerUi(){
$('.someclass-for-date').datepicker();
}
and here is what the load should look like
$('#content').load('/uri', function(){
datepickerUi();
})
or you can load it at the end of your html in script tag .. (but i dont like that , cuz it's harder to debug)
here is some tips
keep your code and css styles as clean as possible .. meaning that for text fields that should be date pickers give them one class all over your website ..
at this rate all of your code will be clean and easy to maintain ..
read more on OOCss this will clear what i mean.
mostly with jquery it's all about organization ... give it some thought and you will get what you want done with one line of code ..
edit
here is a js fiddle with something similar to your but i guess it's a bit cleaner click here
I'm sure we've all seen the site for vanilla-js (the fastest framework for JavaScript) ;D and I was just curious, exactly how much faster plain JavaScript was than jQuery at adding an event handler for a click. So I headed on over to jsPerf to test it out and I was quite surprised by the results.
jQuery outperformed plain JavaScript by over 2500%.
My test code:
//jQuery
$('#test').click(function(){
console.log('hi');
});
//Plain JavaScript
document.getElementById('test').addEventListener('click', function(){
console.log('hi');
});
I just can't understand how this would happen because it seems that eventually jQuery would end up having to use the exact same function that plain JavaScript uses. Can someone please explain why this happens to me?
As you can see in this snippet from jQuery.event.add it does only create the eventHandle once.
See more: http://james.padolsey.com/jquery/#v=1.7.2&fn=jQuery.event.add
// Init the element's event structure and main handler, if this is the first
events = elemData.events;
if (!events) {
elemData.events = events = {};
}
eventHandle = elemData.handle;
if (!eventHandle) {
elemData.handle = eventHandle = function (e) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply(eventHandle.elem, arguments) : undefined;
};
// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
eventHandle.elem = elem;
}
And here we have the addEventListener:
// Init the event handler queue if we're the first
handlers = events[type];
if (!handlers) {
handlers = events[type] = [];
handlers.delegateCount = 0;
// Only use addEventListener/attachEvent if the special events handler returns false
if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
// Bind the global event handler to the element
if (elem.addEventListener) {
elem.addEventListener(type, eventHandle, false);
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, eventHandle);
}
}
}
I think it's because internally jQuery really only has to call addEventListener() once, for its own internal handler. Once that's set up, it just has to add your callback to a simple list. Thus most of the calls to .click() just do some bookkeeping and a .push() (or something like that).
I am setting the className of a table row in my code, is it possible to do something similiar to set an event on a row? This is along the lines of what I would like to do :
for (var i = 1; i < numRows; i++) {
var ID = table.rows[i].id;
if (i % 2 == 0) {
table.rows[i].className = "GridRow";
table.rows[i].onmouseout = "GridRow";
}
else {
table.rows[i].className = "GridRowAlt";
table.rows[i].onmouseout = "GridRowAlt";
}
}
Yes, you can assign a function instance to the event handler that way:
table.rows[i].onmouseout = function() { ... };
Be careful doing that in loops, because you're creating a new function on every loop and the function closes over the data in scope (and so has an enduring reference to it, not a copy of it as of when the function was created; see this other recent question for more). But don't worry, closures are not complicated once you understand how they work.
In general, this is called "DOM0" event handling because it involves a method of attaching event handlers that was created prior to the first DOM specification. As of DOM2, there's a better way addEventListener:
table.rows[i].addEventListener('mouseout',function() { ... }, false);
It's "better" because you can have more than one event handler on the same event of the same element, whereas with the DOM0 mechanism, assigning a new event handler disconnects the previous one (if any).
On IE prior to IE9, sadly, addEventListener wasn't supported but it did have the very similar attachEvent:
table.rows[i].attachEvent('onmouseout',function() { ... });
Note the differences:
addEventListener's event names don't have the "on" prefix
addEventListener has one more param than attachEvent, which you almost always want to set false
Update:
All of the examples above are for inline anonymous functions, which is a bit unlike me, because I don't like anonymous functions. So just for clarity, from an events perspective, a function is a function. It can be a named function you declare elsewhere, or an inline anonymous function, whatever:
// Hook up...
table.rows[i].addEventListener('mouseout', handleRowMouseOut, false);
// Nice, reusable function defined elsewhere
function handleRowMouseOut(event) {
// ...
}
Off-topic: It's these sorts of browser differences that lead me to geneerally recommend using a library like jQuery, Prototype, YUI, Closure, or any of several others. They smooth over differences for you as well as providing lots of handy utility functions.
table.rows[i].onmouseout = "GridRow"; doesn't make a lot of sense, table.rows[i].onmouseout = function(){alert('hello');}; or some other valid script ought to work though.
Why don't you just use jQuery or some other JavaScript framework? This way your code gets more simple.
var i = 0;
$('#some_table tr').each(function() {
if (i % 2 == 0) {
$(this).addClass('GridRow');
$(this).mouseout(function(evt) { /* your GridRow function */ });
} else {
$(this).addClass('GridRowAlt');
$(this).mouseout(function(evt) { /* your GridRowAlt function */ });
}
i++;
})
Sultan
The original question is not to alert "GridRow". I'm quit sure GridRow is a function name. Fortunately each function is a child of window so write window["GridRow"].
I would add a well known bind-event function, because you need it quite often.
var bindEvent=function(elem,evt,func){
if(elem.addEventListener){
elem.addEventListener(evt,func,false);
}
else if(elem.attachEvent){
elem.attachEvent('on'+evt,function(){
func.call(event.srcElement,event);
})
}
};
and then:
bindEvent(table.rows[i],"mouseout",window["GridRow"]);