The title of this post reads as webdev-hipster as a skinny flannel scarf at an alleycat race. Sorry.
I'm not great with script runtime optimization, so I'm wondering just how bad computationally the following function call is going to be. I know it wouldn't be practical for a big site, but where I wan to use it, the jQuery call is going to return no more than a half dozen objects, so the volume isn't high.
Modernizr.load({
test: Modernizr.borderradius && Modernizr.boxshadow,
nope: "_/js/polyfills/pie.js",
complete: function(){
if(window.PIE){
$('*').css('box-shadow').each(function(){ PIE.attach(this); });
$('*').css('border-radius').each(function(){ PIE.attach(this); });
}
}
});
Thanks everyone.
Try this.
Modernizr.load({
test: Modernizr.borderradius && Modernizr.boxshadow,
nope: "_/js/polyfills/pie.js",
complete: function(){
if(window.PIE){
$('*').each(function(){
var $this = $(this);
//check if box-shadow or border-radius is applied
if($this.css('box-shadow') || $this.css('border-radius')){
PIE.attach(this);
}
});
}
}
});
...the jQuery call is going to return no more than a half dozen objects...
So half a dozen is six. Four of those will be html, head, script, and body. :-) You only have two other elements on the page?
Seriously, if the number is very low, it doesn't matter. You'd want to limit the $() call to only the elements that really need it, though, rather than $("*") which is a big hammer.
If you really need to run through the whole document, use a simple recursive-descent function:
function applyPie(element) {
var node;
for (node = element.firstChild; node; node = node.nextSibling) {
if (node.nodeType === 1) { // 1 = element
node.style.boxShadow = /* ...?... there's nothing in your jQuery call */;
node.style.borderRadius = /* ...?... there's nothing in your jQuery call */;
PIE.attach(node);
applyPie(node);
}
}
}
applyPie(document.documentElement);
That calls PIE.attach on every element except the documentElement. You might use nodeName (or tagName) so you don't attach PIE to html, head, style, and such. Using a simple recursive-descent function avoids creating large flat arrays in memory, which is what $("*") does.
Related
I do A/B Testing on our site and I do most of my work is in a JS file that is loaded at the top of the page before anything else is rendered but after jQuery has loaded which comes in handy at times.
Taking a very simple example of changing an H1 tag, I would normally inject a style in the head to set the H1 opacity to 0 and then on DOMContentLoaded, I would manipulate the H1 contents and then set the opacity to 1. The reason for this is to avoid a flash of the old content before the change takes place - hiding the whole object is more graceful on the eye.
I've started to look at the MutationObserver API. I've used this before when changing content in an overlay dialog box that the user could open which seems to be quite a cool approach and I'm wondering if anyone has managed to use a MutationObserver to listen to the document as it's first loading/ parsing and make changes to the document before first render and before DOMContentLoaded?
This approach would then let me change the H1 content without having to hide it, change it, then show it.
I've attempted but failed so far and have just ended up reading about the to-be-obselete Mutation Events and wondering if I'm trying to do something that just isn't possible. However we've (not me) have managed to put a robot on Mars so I'm hoping I can solve this.
So is it possible to use MutationObservers to change the HTML content on-the-fly as the page is being loaded/ parsed?
Thanks for any help or any pointers.
Regards,
Nick
The docs on MDN have a generic incomplete example and don't showcase the common pitfalls.
Mutation summary library provides a human-friendly wrapper, but like all wrappers it adds overhead.
See Performance of MutationObserver to detect nodes in entire DOM.
Create and start the observer.
Let's use a recursive document-wide MutationObserver that reports all added/removed nodes.
var observer = new MutationObserver(onMutation);
observer.observe(document, {
childList: true, // report added/removed nodes
subtree: true, // observe any descendant elements
});
Naive enumeration of added nodes.
Slows down loading of enormously big/complex pages, see Performance.
Sometimes misses the H1 elements coalesced in parent container, see the next section.
function onMutation(mutations) {
mutations.forEach(mutation, m => {
[...m.addedNodes]
.filter(node =>
node.localName === 'h1' && /foo/.test(node.textContent))
.forEach(h1 => {
h1.innerHTML = h1.innerHTML.replace(/foo/, 'bar');
});
});
}
Efficient enumeration of added nodes.
Now the hard part. Nodes in a mutation record may be containers while a page is being loaded (like the entire site header block with all its elements reported as just one added node): the specification doesn't require each added node to be listed individually, so we'll have to look inside each element using querySelectorAll (extremely slow) or getElementsByTagName (extremely fast).
function onMutation(mutations) {
for (var i = 0, len = mutations.length; i < len; i++) {
var added = mutations[i].addedNodes;
for (var j = 0, node; (node = added[j]); j++) {
if (node.localName === 'h1') {
if (/foo/.test(node.textContent)) {
replaceText(node);
}
} else if (node.firstElementChild) {
for (const h1 of node.getElementsByTagName('h1')) {
if (/foo/.test(h1.textContent)) {
replaceText(h1);
}
}
}
}
}
}
function replaceText(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let node; (node = walker.nextNode());) {
const text = node.nodeValue;
const newText = text.replace(/foo/, 'bar');
if (text !== newText) {
node.nodeValue = newText;
}
}
}
Why the two ugly vanilla for loops? Because forEach and filter and ES2015 for (val of array) could be very slow in some browsers, see Performance of MutationObserver to detect nodes in entire DOM.
Why the TreeWalker? To preserve any event listeners attached to sub-elements. To change only the Text nodes: they don't have child nodes, and changing them doesn't trigger a new mutation because we've used childList: true, not characterData: true.
Processing relatively rare elements via live HTMLCollection without enumerating mutations.
So we look for an element that is supposed to be used rarely like H1 tag, or IFRAME, etc. In this case we can simplify and speed up the observer callback with an automatically updated HTMLCollection returned by getElementsByTagName.
const h1s = document.getElementsByTagName('h1');
function onMutation(mutations) {
if (mutations.length === 1) {
// optimize the most frequent scenario: one element is added/removed
const added = mutations[0].addedNodes[0];
if (!added || (added.localName !== 'h1' && !added.firstElementChild)) {
// so nothing was added or non-H1 with no child elements
return;
}
}
// H1 is supposed to be used rarely so there'll be just a few elements
for (var i = 0, h1; (h1 = h1s[i]); i++) {
if (/foo/.test(h1.textContent)) {
// reusing replaceText from the above fragment of code
replaceText(h1);
}
}
}
I do A/B testing for a living and I use MutationObservers fairly often with good results, but far more often I just do long polling which is actually what most of the 3rd party platforms do under the hood when you use their WYSIWYG (or sometimes even their code editors). A 50 millisecond loop shouldn't slow down the page or cause FOUC.
I generally use a simple pattern like:
var poller = setInterval(function(){
if(document.querySelector('#question-header') !== null) {
clearInterval(poller);
//Do something
}
}, 50);
You can get any DOM element using a sizzle selector like you might in jQuery with document.querySelector, which is sometimes the only thing you need a library for anyway.
In fact we do this so often at my job that we have a build process and a module library which includes a function called When which does exactly what you're looking for. That particular function checks for jQuery as well as the element, but it would be trivial to modify the library not to rely on jQuery (we rely on jQuery since it's on most of our client's sites and we use it for lots of stuff).
Speaking of 3rd party testing platforms and javascript libraries, depending on the implementation a lot of the platforms out there (like Optimizely, Qubit, and I think Monetate) bundle a version of jQuery (sometime trimmed down) which is available immediately when executing your code, so that's something to look into if you're using a 3rd party platform.
We have a bunch of forms on our Intranet coded for IE9 that contain code similar to this:
var dept = req.responseXML.selectNodes("//Dept")[0].text;
We just upgraded all of our PCs to IE10 and selectNodes is now obsolete and has been replaced with querySelectorAll, so the corrected bit of code would be:
var dept = req.responseXML.querySelectorAll("Dept")[0].textContent;
All these forms use a shared JS file for sending requests out so I thought if I could define some prototype functions/properties I could fix this incompatibility without touching every form. I made some progress and was able to map selectNodes to querySelectorAll using:
Document.prototype.selectNodes = function(param) {
return this.querySelectorAll(param.replace("//", ""));
}
However I am now running into issues mapping text to textContent. The following doesn't seem to work:
Element.prototype.text = function() {
return this.textContent;
};
If anyone has any adivce I would greatly appreciate it, I really don't want to track all these forms down. Thanks!
It really seems that you ought to fix your code rather than hack the DOM methods in order to avoid fixing your code. Imagine how this builds up over time and your code gets further and further away from programming to a standard DOM and then at some point, you'll probably even have a name collision with something that changes in a browser.
If you wanted to hack the code so that elem.text returns elem.textContent, then because these are properties references, not function calls you need to use a getter like this:
Object.defineProperty(HTMLElement.prototype, "text", {
get: function() {
return this.textContent || this.innerText;
},
set: function(txt) {
if (this.textContent) {
this.textContent = txt;
} else {
this.innerText = txt;
}
}
});
When I click on my website sometimes you can visually see the images slowly loading into the place. I don't really like this and if possible would like to prevent it from happening.
From reading round it seems preloading images is the solution I'm looking for? Let me know if that's correct or if there is a better way.
On this forum I see lots of answers to preloading images and below is a code I think works but I want to change it slightly:
var preloadImages = [ img/1.jpg,img/2.jpg];
for(var i = 0 ; i < preloadImages.length; i++) {
new Image().src = preloadImages[i];
}
I think this code above works but it requires me to type in every image source into an array. On my website there are lots of images and I will probably continue to add more. So is there a way to push all the image sources into the array without actually typing them in. So as I add more images the preloading take care of itself.
If you wanted to go down the route you already have you could search the entire DOM for tags first and then add them to the array.
Perhaps something along these lines:
images = []
$(body).find('img').each ->
imgSrc = $(this).attr('src')
image.push imgSrc
i = 0
while i < preloadImages.length
(new Image).src = preloadImages[i]
i++
That will need jQuery and probably some tweaking. I can't imagine this being the best approach however as you still need to wait for the DOM, then jQuery to load and then run through the entire DOM with the function(s).
If the issue is more that the content is moving around you can (and probably should) prevent this by giving the tag itself a height and width if you know it in advance.
Additionally it's always worth optimizing your images (either by hand or on build/compile using something like Gulp.js) if you haven't already. It's incredible how much you can reduce load times by simply by using the optimal image (in terms of size and format).
You can call below function after page load
function preloadImages(){
$('img').each(function(){
new Image().src = this.src;
});
}
You can use this function to set multiple after-page-load functions
function addLoadEvent(func) {
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = function(){
func();
};
} else {
window.onload = function() {
if (oldonload) {
oldonload();
}
func();
};
}
}
then add your function like this
addLoadEvent(preloadImages);
But I don't think you can really pre-load images like this. To really pre-load images, you have to start loading those images as soon as your DOM starts to render on the client-side. And that means loading them in the head section. But if you do that, because you don't have your html just yet, you will not be able to run the above function, because it needs the DOM to be fully downloaded. You have to hardcode your image sources into javascript like you already did, and put your function as the first thing in the head section.
I am designing a new single page application with JavaScript and JQuery. However, my biggest concern is binding events ( like click, etc. ) via selectors to DOM elements that will be removed once a new "view" is loaded ( a new page is created from an AJAX call )
What is the cleanest way to implement a SPA without creating memory leaks and slowing the page down by replacing the main view DIV with new content?
I know this is an old question but I thought it deserved an answer so here you go:
There are a couple of ways to go about this (it's a bit of a broad question so here is a bit of a broad answer):
FIRST WAY:
// This is in plain JS, but the jQuery equivalent is:
// document.on('click', '#my-id, function(e) { /* Function stuff */ });
document.addEventListener('click' function(e) {
if (e.target.id === 'my-id') {
// Function stuff
}
});
The benefit of doing it this way is that you never have to remove an event listener, as there will always be a document. You could even set up a system of "stages" (I call them stages, I don't know if there is a better word for them) by using classes, i.e. your navigation links all have the class nav-item and in your event listener you could have this code:
// jQuery equivalent of if statement is: if ($(this).is('nav-item')) {}
if (e.target.className.match(/nav-item/)) { // You could also use .include(), etc.
switch(e.target.id) {
case 'item1':
loadPageOneFunction(); // Or whatever
break;
case 'item2':
loadPageTwoFunction(); // Or whatever
break;
}
}
SECOND WAY
// Note how I named the function
document.getElementById('my-id').addEventListener('click', function clickFunction(e) {
myAjaxFunction(e.target); // Or whatever
});
// Later...
if (pageToLoad === 'page2') {
// You are required to name the function you are removing.
document.getElementById('my-id').removeEventListener('click', clickFunction);
document.getElementById('my-page2-id').addEventListener('click', clickFunction);
}
I honestly prefer the first way as it is less code, I can use nameless functions (anonymous functions) - I know, big deal, but still...
There is a third way, which is just to not worry about removing the event listeners. You can read about that here.
Again, this is a very broad answer as its a broad question.
I'm using JS and jQuery for the first time after a lot of experience with Java and C++. I'm loving jQuery's idea of $(document).on('click', 'btn-selector', react), but for more complex widgets I'm finding myself in the same rut over and over: in each react handler, I have to look up the widget as a whole and reconstruct all my knowledge about it.
For example, I'm making a simple widget out of <input>s with which the user can make a grading scale: 90 maps to an A, 80 maps to a B, etc. When one of the inputs changes, I want to check to make sure that the inputs are still in order (your scale can't go 90, 70, 80, for example).
So, I have something like
Actual
$(document).on('click', '.scale-input', function() {
var widget = $(this).closest('.scale-widget-container');
ensureLevelsAreInOrder(widget);
});
Almost every single handler has to have this first line to find its context. I'd much rather have code that looks like this:
Preferred
$(document).on('click', '.scale-input', ensureLevelsAreInOrder);
The problem is that in this form, ensureLevelsAreInOrder only has a reference to the input that changed, not the larger context.
In Java or C++, I would have called a constructor on the widget, and each input would have a handler with the context baked in via member variables. I could do something similar with
$(function() {
$('.scale-widget-container').scaleWidget();
});
with scaleWidget() setting up the contextualized handlers, but the page I'm working with loads a lot of its html with ajax and I don't have a reliable time to run that initialization.
Is this a common problem that we just have to deal with if we don't want JS in our HTML, or is there a solution I haven't come across yet?
Not sure what it is you're after exactly, but you don't seem to touch on two quite important concepts when it comes to JS: the event object, and closures. Both of these are open to you to get what you need:
event object:
The callback function is passed an argument, that describes the event itself, and references the elements affected by that event, This isn't exclusive to jQ (just google addEventListener), but it's quite handy:
$(document).on('click', '.scale-input', function(e)//<-- e is our event
{
console.log(e);//check console
});
Which, in vanilla JS would look like this:
document.addEventListener('click', function(e)
{
if (!e.className.test(/\bscale\-input\b/))
{
return e;
}
console.log(e);
}, false);
Another thing you might want to consider is enclosing references to whatever it is you need in an IIFE's scope:
(function()
{
var containers = $('.scale-widget-container'),
localBool = false,
asMany = 'varsAs you need',
previousScales = [],
inputs = $('.scale-input');//references to all DOM nodes you mention
$(document).on('click','.scale-input',function(e)
{
console.log($(this));
console.log(containers);
previousScales.push(this.value);//or something
console.log(previousScales);
//and so on.
});
}());
Hope this helped
Update:
If IE isn't a browser you don't care about that much, you could use one of the DOM-modified events, specifically DOMTreeModified:
(function()
{
var nodes = [];//<-- store current nodes here, if applicable
nodes.containsNode = function(node)
{
var i;
for (i=0;i<this.length;i++)
{
if (this[i] && this[i] === node)
{//node is set, return its index
return i;
}
}
//node not found, return -1
return -1;
};
document.body.addEventListener('DOMSubtreeModified',function(e)
{
var all = document.getElementsByClassName('scale-input'),
i;
for (i=0;i<all.length;i++)
{
if (nodes.containsNode(all[i]) === -1)
{
nodes.push(all[i]);//add new
}
}
},false);
}());
More on the mutation events, and their issues, on the DOM events wiki