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)
Related
I'm using Cypress 7.7.0 (also tested on 8.0.0), and I'm running into an interesting race condition. I'm testing a page where one of the first interactions that Cypress does is click a button to open a modal. To keep bundle sizes small, I split the modal into its own prefetched webpack chunk. My Cypress test starts with cy.get('#modal-button').click() but this doesn't load the modal because the modal hasn't finished downloading/loading. It does nothing instead (doesn't even throw any errors to the console). In other words, Cypress interacts with the page too quickly. This was also reproduced with manual testing (I clicked on the button super fast after page load). I have tried setting the modal to be preloaded instead, but that didn't work either.
I am able to solve the problem by introducing more delay between page load and button interaction. For example, inserting any Cypress command (even a cy.wait(0)) before I click on the button fixes the solution. Cypress, however, is known for not needing to insert these brittle solutions. Is there a good way to get around this? I'd like to keep the modal in its own chunk.
FYI: I'm using Vue as my front end library and am using a simple defineAsyncComponent(() => import(/* webpackPrefetch: true */ './my-modal.vue')) to load the modal component. I figure that this problem is general to Cypress though.
There's nothing wrong with cy.wait(0).
All you are doing is handing control from the test to the next process in the JS queue, in this case it's the app's startup script which is presumably waiting to add the click handler to the button.
I recently found that this is also needed in a React hooks app to allow the hook to complete it's process. You will likely also come across that in Vue 3, since they have introduced a hook-like feature.
If you want to empirically test that the event handler has arrived, you can use the method given here (modified for click()) - When Can The Test Start?
let appHasStarted
function spyOnAddEventListener (win) {
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'click') {
appHasStarted = true
win.EventTarget.prototype.addEventListener = addListener // restore original listener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0) // recheck "appHasStarted" variable
}
isReady()
})
}
it('greets', () => {
cy.visit('app.html', {
onBeforeLoad: spyOnAddEventListener
}).then(waitForAppStart)
cy.get('#modal-button').click()
})
But note setTimeout(isReady, 0) will probably just achieve the same as cy.wait(0) in your app, i.e you don't really need to poll for the event handler, you just need the app to take a breath.
It seems like your problem is that you're already rendering a button before the code backing it is loaded. As you noticed, this isn't only an issue for fast automated bots, but even a "regular" user.
In short, the solution is to not display the button early, but show a loading dialog instead. Cypress allows waiting for a DOM element to be visible with even a timeout option. This is more robust than a brittle random wait.
I ended up going with waiting for the network to be idle, although there were several options available to me.
The cypress function I used to do this was the following which was heavily influenced by this solution for waiting on the network:
Cypress.Commands.add('waitForIdleNetwork', () => {
const idleTimesInit = 3
let idleTimes = idleTimesInit
let resourcesLengthPrevious
cy.window().then(win =>
cy.waitUntil(() => {
const resourcesLoaded = win.performance.getEntriesByType('resource')
if (resourcesLoaded.length === resourcesLengthPrevious) {
idleTimes--
} else {
idleTimes = idleTimesInit
resourcesLengthPrevious = resourcesLoaded.length
}
return !idleTimes
})
)
})
Here are the pros and cons of the solution I went with:
pros: no need to increase bundle size or modify client code when the user will likely never run into this problem
cons: technically still possible to have a race condition where the click event happens after the assets were downloaded, but before they could all execute and render their contents, but very unlikely, not as efficient as waiting on the UI itself for indication of when it is ready
This was the way I chose solve it but the following solutions would have also worked:
creating lightweight placeholder components to take the place of asychronous components while they download and having cypress wait for the actual component to render (e.g. a default modal that just has a spinner being shown while the actual modal is downloaded in the background)
pros: don't have to wait on network resources, avoids all race conditions if implemented properly
cons: have to create a component the user may never see, increases bundle size
"sleeping" an arbitrary amount (although this is brittle) with cy.wait(...)
pros: easy to implement
cons: brittle, not recommended to use this directly by Cypress, will cause linter problems if using eslint-plugin-cypress (you can disable eslint on the line that you use this on, but it "feels ugly" to me (no hate on anyone who programs that way)
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.
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>
I have an ajax callback which injects html markup into a footer div.
What I can't figure out is how to create a way to monitor the div for when it's contents change. Placing the layout logic I'm trying to create in the callback isn't an option as each method (callback and my layout div handler) shouldn't know about the other.
Ideally I'd like to see some kind of event handler akin to $('#myDiv').ContentsChanged(function() {...}) or $('#myDiv').TriggerWhenContentExists( function() {...})
I found a plugin called watch and an improved version of that plugin but could never get either to trigger. I tried "watching" everything I could think of (i.e. height property of the div being changed via the ajax injection) but couldn't get them to do anything at all.
Any thoughts/help?
The most effective way I've found is to bind to the DOMSubtreeModified event. It works well with both jQuery's $.html() and via standard JavaScript's innerHTML property.
$('#content').bind('DOMSubtreeModified', function(e) {
if (e.target.innerHTML.length > 0) {
// Content change handler
}
});
http://jsfiddle.net/hnCxK/
When called from jQuery's $.html(), I found the event fires twice: once to clear existing contents and once to set it. A quick .length-check will work in simple implementations.
It's also important to note that the event will always fire when set to an HTML string (ie '<p>Hello, world</p>'). And that the event will only fire when changed for plain-text strings.
You can listen for changes to DOM elements (your div for example) by binding onto DOMCharacterDataModified tested in chrome but doesn't work in IE see a demo here
Clicking the button causes a change in the div which is being watched, which in turn fills out another div to show you its working...
Having a bit more of a look Shiki's answer to jquery listen to changes within a div and act accordingly looks like it should do what you want:
$('#idOfDiv').bind('contentchanged', function() {
// do something after the div content has changed
alert('woo');
});
In your function that updates the div:
$('#idOfDiv').trigger('contentchanged');
See this as a working demo here
There is a neat javascript library, mutation-summary by google, that lets you observe dom changes concisely. The great thing about it, is that if you want, you can be informed only of the actions that actually made a difference in the DOM, to understand what I mean you should watch the very informative video on the project's homepage.
link:
http://code.google.com/p/mutation-summary/
jquery wrapper:
https://github.com/joelpurra/jquery-mutation-summary
You might want to look into the DOMNodeInserted event for Firefox/Opera/Safari and the onpropertychange event for IE. It probably wouldn't be too hard to utilize these events but it might be a little hack-ish. Here is some javascript event documentation: http://help.dottoro.com/larrqqck.php
Now we can use a MutationObserver ; Well, apparently we must.
Use of Mutation Events is deprecated. Use MutationObserver instead.
jquery.min.js:2:41540
From https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver :
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();
I have a web page with DIVs with a mouseover handler that is intended to show a pop-up information bubble. I don't want more than one info bubble to be visible at a time. But when the user moves the mouse rapidly over two items, I sometimes get two bubbles. This should not happen, because the code for showing a pop-up cancels the previous pop-up.
If this were a multi-threaded system then the problem would be obvious: there are two threads trying to show a pop-up, and they both cancel existing pop-ups then pop up their own pop-ups. But I assumed JavaScript is always run single-threaded, which would prevent this. Am I wrong? Are event handlers running asynchronously, in which case I need synchronized access to shared data, or should I instead be looking for bugs in the library code for cancelling pop-ups?
Edited to add:
The library in question is SIMILE Timeline and its Ajax library;
The event handler does call SimileAjax.DOM.cancelEvent(domEvt), which I assume based on the name cancels the bubbling of events;
Just to make thing s more complicated, what I am actually doing is starting a timeout that if not cancelled by a moustout shows the pop-up, this being intended to prevent pop-ups flickering annoyingly but annoyingly having the reverse effect.
I'll have another poke at it and see if I can work out where I am going wrong. :-)
Yes, Javascript is single-threaded. Even with browsers like Google Chrome, there is one thread per tab.
Without knowing how you are trying to cancel one pop-up from another, it's hard to say what is the cause of your problem.
If your DIVs are nested within one another, you may have an event propagation issue.
I don't know the library you are using, but if you are only trying to display one tooltip of somesort at a time... use a flyweight object. Basically a flyweight is something that is made once and used over and over again. Think of a singleton class. So you call a class statically that when first invoked automatically creates an object of itself and stores it. One this happens every static all references the same object and because of this you don't get multiple tooltips or conflicts.
I use ExtJS and they do tooltips, and message boxes as both flyweight elements. I'm hoping that your frameworks had flyweight elements as well, otherwise you will just have to make your own singleton and call it.
It is single threaded in browsers. Event handlers are running asynchroniously in one thread, non blocking doesn't allways mean multithreaded. Is one of your divs a child of the other? Because events spread like bubbles in the dom tree from child to parent.
Similar to what pkaeding said, it's hard to guess the problem without seeing your markup and script; however, I'd venture to say that you're not properly stopping the event propagation and/or you're not properly hiding the existing element. I don't know if you're using a framework or not, but here's a possible solution using Prototype:
// maintain a reference to the active div bubble
this.oActiveDivBubble = null;
// event handler for the first div
$('exampleDiv1').observe('mouseover', function(evt) {
evt.stop();
if(this.oActiveDivBubble ) {
this.oActiveDivBubble .hide();
}
this.oActiveDivBubble = $('exampleDiv1Bubble');
this.oActiveDivBubble .show();
}.bind(this));
// event handler for the second div
$('exampleDiv2').observe('mouseover'), function(evt) {
evt.stop();
if(this.oActiveDivBubble) {
this.oActiveDivBubble.hide();
}
this.oActiveDivBubble = $('exampleDiv2Bubble');
this.oActiveDivBubble .show();
}.bind(this));
Of course, this could be generalized further by getting all of the elements with, say, the same class, iterating through them, and applying the same event handling function to each of them.
Either way, hopefully this helps.
FYI: As of Firefox 3 there is a change pretty much relevant to this discussion: execution threads causing synchronous XMLHttpRequest requests get detached (this is why the interface doesn't freeze there during synchronous requests) and the execution continues. Upon synchronous request completion, its thread continues as well. They won't be executed at the same time, however relying on the assumption that single thread stops while a synchronous procedure (request) happening is not applicable any more.
It could be that the display isn't refreshing fast enough. Depending on the JS library you are using, you might be able to put a tiny delay on the pop-up "show" effect.
Here's the working version, more or less. When creating items we attach a mouseover event:
var self = this;
SimileAjax.DOM.registerEvent(labelElmtData.elmt, "mouseover", function (elt, domEvt, target) {
return self._onHover(labelElmtData.elmt, domEvt, evt);
});
This calls a function that sets a timeout (pre-existing timeouts for a different item is cancelled first):
MyPlan.EventPainter.prototype._onHover = function(target, domEvt, evt) {
... calculate x and y ...
domEvt.cancelBubble = true;
SimileAjax.DOM.cancelEvent(domEvt);
this._futureShowBubble(x, y, evt);
return false;
}
MyPlan.EventPainter.prototype._futureShowBubble = function (x, y, evt) {
if (this._futurePopup) {
if (evt.getID() == this._futurePopup.evt.getID()) {
return;
} else {
/* We had queued a different event's pop-up; this must now be cancelled. */
window.clearTimeout(this._futurePopup.timeoutID);
}
}
this._futurePopup = {
x: x,
y: y,
evt: evt
};
var self = this;
this._futurePopup.timeoutID = window.setTimeout(function () {
self._onTimeout();
}, this._popupTimeout);
}
This in turn shows the bubble if it fires before being cancelled:
MyPlan.EventPainter.prototype._onTimeout = function () {
this._showBubble(this._futurePopup.x, this._futurePopup.y, this._futurePopup.evt);
};
MyPlan.EventPainter.prototype._showBubble = function(x, y, evt) {
if (this._futurePopup) {
window.clearTimeout(this._futurePopup.timeoutID);
this._futurePopup = null;
}
...
SimileAjax.WindowManager.cancelPopups();
SimileAjax.Graphics.createBubbleForContentAndPoint(...);
};
This seems to work now I have set the timeout to 200 ms rather than 100 ms. Not sure why too short a timeout causes the multi-bubble thing to happen, but I guess queuing of window events or something might still be happening while the newly added elements are being laid out.