What are the differences between setting DOM attribute directly and via requestAnimationFrame? - javascript

I am reading BluePrintJS source code and noticed they put DOM element attributes changing logic inside a requestAnimationFrame block. What are the differences to set DOM attribute directly and via requestAnimationFrame?
private handlePopoverClosing = (node: HTMLElement) => {
// restore focus to saved element.
// timeout allows popover to begin closing and remove focus handlers beforehand.
requestAnimationFrame(() => {
if (this.previousFocusedElement !== undefined) {
this.previousFocusedElement.focus();
this.previousFocusedElement = undefined;
}
});
const { popoverProps = {} } = this.props;
Utils.safeInvoke(popoverProps.onClosing, node);
};

Performance. requestAnimationFrame will be run when at the start of a render frame, which helps with something known as layout thrashing.
By doing this you group all DOM changes into one frame which is done all at once rather than spreading rendering logic across multiple frames, which is more expensive (DOM work is s l o w).
The general idea is that you want to group DOM writes such that they happen before DOM reads instead of interspersing reads among writes - rAF does that by ensuring the write happens at a specific time, with other writes.

Related

Replacing page elements dynamically through a greasemonkey script (even post page load)

I have the following javascript function:
function actionFunction() {
let lookup_table = {
'imageurl1': "imageurl2",
};
for (let image of document.getElementsByTagName("img")) {
for (let query in lookup_table) {
if (image.src == query) {
image.src = lookup_table[query];
}
}
}
}
I want it to work even after a page is fully loaded (in other words, work with dynamically generated html elements that appeared post-load by the page's js).
It could either be by running the function every x seconds or when a certain element xpath is detected within the page, or every time a certain image url is loaded within the browser (which is my main goal here).
What can I do to achieve this using javascript + greasemonkey?
Thank you.
Have you tried running your code in the browser's terminal to see if it works without greasemonkey involved?
As to your question - you could either use setInterval to run given code every x amount of time or you could use the MutationObserver to monitor changes to the webpage's dom. In my opinion setInterval is good enough for the job, you can try learning how the MutationObserver works in the future.
So rewriting your code:
// arrow function - doesn't lose this value and execution context
// setInterval executes in a different context than the enclosing scope which makes functions lose this reference
// which results in being unable to access the document object
// and also window as they all descend from global scope which is lost
// you also wouldn't be able to use console object
// fortunately we have arrow functions
// because arrow functions establish this based on the scope the arrow function is defined within
// which in most cases is global scope
// so we have access to all objects and their methods
const doImageLookup = () => {
const lookup_table = {
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png': 'https://www.google.com/logos/doodles/2022/gama-pehlwans-144th-birthday-6753651837109412-2x.png',
};
const imgElementsCollection = document.getElementsByTagName('img');
[...imgElementsCollection].forEach((imgElement) => {
Object.entries(lookup_table).forEach(([key, value]) => {
const query = key; // key and value describe object's properties
const replacement = value; // here object is used in an unusual way, i would advise to use array of {query, replacement} objects instead
if (imgElement.src === query) {
imgElement.src = replacement;
}
});
});
};
const FIVE_MINUTES = 300000; // ms
setInterval(doImageLookup, FIVE_MINUTES);
You could make more complex version by tracking the img count and only doing the imageLookop if their number increases. This would be a big optimization and would allow you to run the query more frequently (though 5 minutes is pretty long interval, adjust as required).

Web Component offsetHeight/offsetWidth zero when connected

I am trying to convert a library of mine into a web component and I have run into a problem with when to tell the component has been laid out.
Web Components have a callback called connectedCallback which looks like it might be what I need, but when inspecting self in the callback, my offset dimensions are both zero:
connectedCallback() {
console.log(this.offsetWidth, this.offsetHeight);
}
When I queue up the dimensions query, the dimensions are there, so sometime between the web component connecting and the event queue reaching the next item, the web component gets laid out by the browser engine, gaining correct dimension values:
connectedCallback() {
setTimeout(() => console.log(this.offsetWidth, this.offsetHeight), 0);
}
I am wondering if there is a callback to tell when the layout event has happened?
I created a simple component without shadowDOM. I wrote the following code in a constructor:
this.addEventListener('readystatechange', (evt) => {console.log('readystatechange', evt)});
this.addEventListener('load', (evt) => {console.log('load', evt)});
this.addEventListener('progress', (evt) => {console.log('progress', evt)});
this.addEventListener('DOMContentLoaded', (evt) => {console.log('DOMContentLoaded', evt)});
And got no output. So none of these events trigger on a Web component.
Then I added this code outside the component:
function callback(mutationsList, observer) {
for(var mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.removedNodes.length) {
console.log(`${mutation.removedNodes.length} child node(s) have been removed.`);
}
if (mutation.addedNodes.length) {
console.log(`${mutation.addedNodes.length} child node(s) have been added.`);
}
}
}
}
var config = { childList: true };
var observer = new MutationObserver(callback);
And this line to the constructor:
observer.observe(this, config);
On my particular component my callback was never called.
I did have a different component that changes its children and the callback was called.
So, I don't see any event or even mutation observer that triggers when the component is fully loaded.
Personally I would use a setTimeout like you did since that seems to be the only sure way of getting a call when the component is done. Although you may have to be specific where you put your setTimeout to make sure you have fully rendered your DOM.
Those values depend on the whole DOM so need a DOM repaint..
which hasn't occured yet.
If every element would trigger a DOM repaint we would all be complaining about performance.
The Browser decides (browser specific logic) when to do a DOM repaint.
Usual and accepted way to execute code after a DOM repaint is a setTimeout( FUNC , 0 )
so strictly speaking this does NOT force a DOM repaint, it is still the Browser deciding when to repaint
If you have a long running script.... FUNC will execute after the long running script is done.
Yes an Event would be nice...
but that takes CPU cycles... and in 997 out of a 1000 cases you don't need the info
.. thus there is NO Event
So in the 0,03% of cases where you do need DOM info you do a setTimeout
Note that this is not Custom Element specific, for appendChild calls the same applies.
If you have a sh*pload of components that need this offset info you might want to look into a Framework that does virtual DOM diffing
More background: Medium: What the heck is repaint/reflow (2019)

How to change the HTML content as it's loading on the page

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.

Javascript object destroy event handler

MyMapStaticObject
var PlaceViewModel = function(){
MyMapStaticObject.addLayer(someLayer);
}
PlaceViewModel.prototype.addMarker = function(item){
}
I have a PlaceViewModel that has a function named addMarker to add marker to map. I will use PlaceViewModel new istances in different classes.
var inst = new PlaceViewModel();
When I initialize the PlaceViewModel, I am adding new layer to map via MyMapStaticObject. I should remove layer when instance destroyed.
Can I handle javascript destroy event?
Javascript does not have a destroy event. It is a garbage collected language and will free an object when there is no longer any code that can reach the object reference. When it does free the object, it does not provide any event to notify of that.
If you want to implement some sort of clean-up code that will remove the layer, then you will have to add a method that you can call when you are done with the object, so you can call that method and it can then remove the layer in that method. Calling this method will have to be a manual operation on your part (most likely it will be hooked into the management of other things going on in your code and you can call it by that code at the appropriate time).
Bit late to the party here, but I wanted to know when an object got destroyed. Problem is JS doesn't have any built in way of doing this. I wanted this so that I could add websocket events to a page, and then remove when it went to another page, of course I could have implemented this in say the page loading section, but I have different frameworks and wanted a more generic solution to object destroying problem. Javascript is a garbage collected language, but it still would have been nice to have some life-cycle events we could attach too. There is of course proxy's, but again that wouldn't help here, because it's the proxy itself I would need to know that has been deleted.
Well, there is one place you do get a kind of destroy event, and that's with the MutationObserver, that most modern browsers now support. It's not strictly destroying, it's adding and removing nodes from the DOM. But generally speaking if you have events, you are likely to have some DOM node you could attach too, or even if it's none visual you could just add a none visible DOM element.
So I have a little function called domDestroy(element, obj), when it detects the element been removed, it then checks if the obj has a destroy method, if one exists it will call it.
Now one gotcha I had is that I create my pages in an hidden DOM node, and of course when I placed into the visible DOM node, I was getting a delete because I was detaching from the invisible DOM node, and then attaching to the visible DOM. Not what we want at all.
The solution was pretty simple, when doing this kind of double buffering, it's normally done in 1 step, eg. hide current page, show new page. So what I do is keep track of when it's been removed and keep in a simple Set, and then also keep track of elements been added, and if this element is part of the Set I will remove it. I then just check this Set again on the next tick, if's it's still there, it's been really deleted and we call the destroy method of the object.
Below is a simple example, basically if you right click and inspect the page, you can move the LI's up and down with dragging and dropping, this would cause a DOM detach and re-attach,. But if you instead delete one of the LI's, you will notice it say delete then, because it now knows it wasn't re-attached to another DOM.
Of course, one thing to be aware of, if you do any attaching / detaching of DOM elements try and do this within the same tick, IOW: be aware of asynchronous ops in between. Also you might use detached DOM's to build your pages, here you could easily alter the function to cope with this too, basically add these using the destroyObserver.observe(.
const dsy = "__dom-destroy-obj";
const destroyList = new Set();
let tm;
function cleanUp() {
tm = null;
for (const el of destroyList) {
for (const d of el[dsy]) {
d.destroy();
}
}
destroyList.clear();
}
function checkDestroy(el) {
if (el[dsy]) {
for (const d of el[dsy]) {
if (!d.destroy) {
console.warn("No destroy, on dom-destroy-obj target");
} else {
destroyList.add(el);
if (tm) return; //already a timer running
tm = setTimeout(cleanUp, 1);
}
}
}
if (el.childNodes) for (const n of el.childNodes) checkDestroy(n);
}
function checkAdded(el) {
if (el[dsy]) {
destroyList.delete(el);
}
if (el.childNodes) for (const n of el.childNodes) checkAdded(n);
}
const destroyObserver = new MutationObserver(
function (mutations) {
for (const m of mutations) {
if (m.removedNodes.length) {
for (const i of m.removedNodes) {
checkDestroy(i);
}
}
if (m.addedNodes.length) {
for (const i of m.addedNodes) {
checkAdded(i);
}
}
}
}
);
destroyObserver.observe(document.body, {
childList: true,
subtree: true
});
function domDestroy(element, obj) {
if (!element[dsy]) element[dsy] = new Set();
element[dsy].add(obj);
}
//simple test.
for (const i of document.querySelectorAll("li")) {
domDestroy(i, {
destroy: () => console.log("destroy")
});
}
<span>
From your browsers inspector, try moving the LI's, and deleting them. Only when you delete the DOM node, should the destroy method get called.
</span>
<ul>
<li>Re order</li>
<li>Or delete</li>
<li>Some of these</li>
</ul>

Is there 'element rendered' event?

I need to accurately measure the dimensions of text within my web app, which I am achieving by creating an element (with relevant CSS classes), setting its innerHTML then adding it to the container using appendChild.
After doing this, there is a wait before the element has been rendered and its offsetWidth can be read to find out how wide the text is.
Currently, I'm using setTimeout(processText, 100) to wait until the render is complete.
Is there any callback I can listen to, or a more reliable way of telling when an element I have created has been rendered?
The accepted answer is from 2014 and is now outdated. A setTimeout may work, but it's not the cleanest and it doesn't necessarily guarantee that the element has been added to the DOM.
As of 2018, a MutationObserver is what you should use to detect when an element has been added to the DOM. MutationObservers are now widely supported across all modern browsers (Chrome 26+, Firefox 14+, IE11, Edge, Opera 15+, etc).
When an element has been added to the DOM, you will be able to retrieve its actual dimensions.
Here's a simple example of how you can use a MutationObserver to listen for when an element is added to the DOM.
For brevity, I'm using jQuery syntax to build the node and insert it into the DOM.
var myElement = $("<div>hello world</div>")[0];
var observer = new MutationObserver(function(mutations) {
if (document.contains(myElement)) {
console.log("It's in the DOM!");
observer.disconnect();
}
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
$("body").append(myElement); // console.log: It's in the DOM!
The observer event handler will trigger whenever any node is added or removed from the document. Inside the handler, we then perform a contains check to determine if myElement is now in the document.
You don't need to iterate over each MutationRecord stored in mutations because you can perform the document.contains check directly upon myElement.
To improve performance, replace document with the specific element that will contain myElement in the DOM.
There is currently no DOM event indicating that an element has been fully rendered (eg. attached CSS applied and drawn). This can make some DOM manipulation code return wrong or random results (like getting the height of an element).
Using setTimeout to give the browser some overhead for rendering is the simplest way. Using
setTimeout(function(){}, 0)
is perhaps the most practically accurate, as it puts your code at the end of the active browser event queue without any more delay - in other words your code is queued right after the render operation (and all other operations happening at the time).
This blog post By Swizec Teller, suggests using requestAnimationFrame, and checking for the size of the element.
function try_do_some_stuff() {
if (!$("#element").size()) {
window.requestAnimationFrame(try_do_some_stuff);
} else {
$("#element").do_some_stuff();
}
};
in practice it only ever retries once. Because no matter what, by the next render frame, whether it comes in a 60th of a second, or a minute, the element will have been rendered.
You actually need to wait yet a bit after to get the after render time. requestAnimationFrame fires before the next paint. So requestAnimationFrame(()=>setTimeout(onrender, 0)) is right after the element has been rendered.
In my case solutions like setTimeout or MutationObserver weren't totaly realiable.
Instead I used the ResizeObserver. According to MDN:
Implementations should, if they follow the specification, invoke
resize events before paint and after layout.
So basically the observer always fires after layout, thus we should be able to get the correct dimensions of the observed element.
As a bonus the observer already returns the dimensions of the element. Therefore we don't even need to call something like offsetWidth (even though it should work too)
const myElement = document.createElement("div");
myElement.textContent = "test string";
const resizeObserver = new ResizeObserver(entries => {
const lastEntry = entries.pop();
// alternatively use contentBoxSize here
// Note: older versions of Firefox (<= 91) provided a single size object instead of an array of sizes
// https://bugzilla.mozilla.org/show_bug.cgi?id=1689645
const width = lastEntry.borderBoxSize?.inlineSize ?? lastEntry.borderBoxSize[0].inlineSize;
const height = lastEntry.borderBoxSize?.blockSize ?? lastEntry.borderBoxSize[0].blockSize;
resizeObserver.disconnect();
console.log("width:", width, "height:", height);
});
resizeObserver.observe(myElement);
document.body.append(myElement);
This can also we wrapped in a handy async function like this:
function appendAwaitLayout(parent, element) {
return new Promise((resolve, reject) => {
const resizeObserver = new ResizeObserver((entries) => {
resizeObserver.disconnect();
resolve(entries);
});
resizeObserver.observe(element);
parent.append(element);
});
}
// call it like this
appendAwaitLayout(document.body, document.createElement("div")).then((entries) => {
console.log(entries)
// do stuff here ...
});
The MutationObserver is probably the best approach, but here's a simple alternative that may work
I had some javascript that built the HTML for a large table and set the innerHTML of a div to the generated HTML. If I fetched Date() immediately after setting the innerHTML, I found that the timestamp was for a time prior to the table being completely rendered. I wanted to know how long the rendering was taking (meaning I needed to check Date() after the rendering was done). I found I could do this by setting the innerHTML of the div and then (in the same script) calling the click method of some button on the page. The click handler would get executed only after the HTML was fully rendered, not just after the innerHTML property of div got set. I verified this by comparing the Date() value generated by the click handler to the Date() value retrieved by the script that was setting the innerHTML property of the div.
Hope someone finds this useful
suppose your element has classname class="test"
The following function continue test if change has occured
if it does, run the function
function addResizeListener(elem, fun) {
let id;
let style = getComputedStyle(elem);
let wid = style.width;
let hei = style.height;
id = requestAnimationFrame(test)
function test() {
let newStyle = getComputedStyle(elem);
if (wid !== newStyle.width ||
hei !== newStyle.height) {
fun();
wid = newStyle.width;
hei = newStyle.height;
}
id = requestAnimationFrame(test);
}
}
let test = document.querySelector('.test');
addResizeListener(test,function () {
console.log("I changed!!")
});
when you make for example
var clonedForm = $('#empty_form_to_clone').clone(true)[0];
var newForm = $(clonedForm).html().replace(/__prefix__/g, next_index_id_form);
// next_index_id_form is just a integer
What am I doing here?
I clone a element already rendered and change the html to be rendered.
Next i append that text to a container.
$('#container_id').append(newForm);
The problem comes when i want to add a event handler to a button inside newForm, WELL, just use ready event.
$(clonedForm).ready(function(event){
addEventHandlerToFormButton();
})
I hope this help you.
PS: Sorry for my English.
According to #Elliot B.'s answer, I made a plan that suits me.
const callback = () => {
const el = document.querySelector('#a');
if (el) {
observer.disconnect();
el.addEventListener('click', () => {});
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, { subtree: true, childList: true });

Categories