How do I avoid CLS (Cumulative Layout Shift) if I'm loading external content and the height is dynamically assigned? - core-web-vitals

I have a button to pull some data from backend. But the length of the data is uncertain and the time to load is also uncertain.
This will generate high CLS penalty. Is there anyway to avoid this?
var stHandler = 0;
$('#render').click(function(){
clearTimeout(stHandler);
$('#result').html('loading...');
// [Simulate the server response time]
// CLS will not be penalized if the everything happens in 100ms,
// but most of the case, the server will return the data longer than that
stHandler = setTimeout(function(){
$('#result').html(fakeResultBuilder);
}, 500 + Math.random()*1000);
})
// [Simulate the real-world condition]
// Every time you load this content, the height will be different.
function fakeResultBuilder(){
var html = '';
for (var i=0; i<Math.random()*100; i++) {
html += '<div class="block"></div>';
}
return html;
}
button {cursor:pointer}
.block {height:10px; background:#f00; width:20px; margin:5px}
#result {border:1px solid #999}
footer {margin-top:1em}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
<button id="render">Render</button>
</div>
<h2>Result:</h2>
<div id="result"></div>
<footer>Page Footer - will have huge CLS sometimes</footer>

The key is to adjust as much as possible within the 500ms grace period (note not 100ms as given in your code comment). The 500ms grace period is set because clicking on things (like your Render button) is an expected shift. However, if it happens considerably later (and 500ms is set as the limit) then it's back to being an unexpected shift. The user may be in the middle of reading some of the footer while they wait.
While the full response may not be able to be returned within that 500ms, you can still reserve some space (perhaps using min-height?) to, at the very least, reduce the CLS. The default height of an empty div is 0px so ANYTHING you do here will be better than than that as surely it will be larger than 0ms?
Additionally if it's enough to always knock the footer off-screen then adding a minimum height to push that footer off screen will mean any further moves for it are not considered CLS.
This may leave a large white space, which may not be ideal, so you may wish to consider some kind of "Loading..." message or a loading spinner. This will give an indication that something is happening to the user rather than them being unsure and perhaps rage-clicking the render button again and again.

Related

Positioning multiple fixed sticky elements with Waypoints

I'm using Waypoints and their Sticky shortcut to 'stick' an element with the id stick-this to the top of the viewport once it gets scrolled past. I am having some difficulty positioning the element past another fixed element on the screen, however.
There is a <div> with a class .header which always remains fixed. I am trying to position the top of the new element to the height() of the .header element so they are 'stacked' on top of one another and both visible. This is the code I am using to accomplish this:
var sticky = new Waypoint.Sticky({
element: $('#stick-this')[0],
handler: function() {
$(".stuck").css({ "top" : $(".header").height() });
}
})
So, essentially, once the #stick-this is scrolled past, it becomes sticky with a position:fixed class and the top is dynamically determined by the height() of .header.
This works great until I scroll back up, and the top style is still applied to this element, in spite of the stuck class not being applied anymore.
So when I scroll past, the element ends up like this
<div id="stick-this" class="stuck" style="top:70px /*or whatever the height() ends up being */">
and when I scroll back up the element ends up like this with the top property still in place
<div id="stick-this" class="" style="top:70px /*I need this back to 0px */">
Is it possible to have a function called when the "sticky" is removed, so that the inline style property can be set to top:0px or something like that?
For anyone else struggling with this, I ended up dynamically writing the CSS when the sticky element's class is initiated and inserting it into the head:
var sticky = new Waypoint.Sticky({
element: $('#stick-this')[0],
offset: $('.header').outerHeight(true),
handler: function(direction) {
$("<style>")
.prop("type", "text/css")
.html("\
.stuck {\
position: fixed;\
top:" + $(".header").height() + "px;\
}")
.appendTo("head");
}
})
so, the class will be added with the correct top positioning, and once the class is removed, the top property is inherently returned back to 0px.
It's important to have the \ after each line break in the .html() portion of this code in order for it to work.

dojo animation on interrupt?

Is there an equivalent of an 'on interrupt' function when using dojo.fx animations?
On my site, I clear an area by sliding the div off the screen to the left and then clearing the innerHTML. So that the div is available for future use in the same place, I set its final position to be equal to its original position (though it's empty and invisible, so the user doesn't see). I do this by referencing the position when the function is fired to avoid hard coding the position in.
function clear (node) {
// get the current position
var position = domGeom.position (node, true);
// slide the calendar off the screen
var anim = dojo.fx.slideTo ({
node: node,
left: -2000,
unit: "px"
});
// attach on onEnd function
dojo.connect (anim, "onEnd", function (n1, n2) {
node.innerHTML = "";
// Reset the node's position
domStyle.set (node, "left", position.x + "px");
});
anim.play();
}
The problem is that if the user clicks 'clear' while the function is in progress, for example if they accidentally double click, then the 'position' variable takes the value of the node mid-animation. If I later put some content in the node, it's in the wrong place, or sometimes completely off screen.
What I was thinking of doing was adding a listener to the animation so that if it was ever interrupted, it would stop the animation and instead execute the 'onEnd' function immediately, so that the second clear function would have the correct position variable.
Unfortunately, I can't find such a listener; could anyone point me in the right direction, or suggest an alternative solution?
You might be interrupting the flow of events in your page, but you're not really 'interrupting' in Animation terms: there's no way an Animation could give you an event to tell you that Clear has been pressed, I'm afraid.
If you want the animation to be stopped when the user presses Clear, you'll have to stop the animation manually when the user presses Clear: Dojo can't automatically detect there's any link between the two things, and indeed there may well be other animations that you want to continue even if the user does press Clear.
So, you'll probably be looking to call .stop on the animation when appropriate.

Slickgrid: Final column autosize to use all remaining space

I'm using SlickGrid and struggling to find an elegant solution to the following:
All columns must have a specific initial width when first rendered but be resizable afterwards
The final column should auto fill the remaining column space when the window is resized
I've seen:
Make one column fill remaining space in SlickGrid without messing up explicit width columns
resizing of grid when resizing browser window
How do I autosize the column in SlickGrid?
But these don't seem to quite do what I need.
If I use the forceFitColumns option, then all columns will autosize (unless I put a maxsize on them).
Using resizeCanvas on window.resize works well - but it still only works if forceFitColumns is true.
If I set minWidth=maxWidth - then I can't resize the column.
Any suggestions?
I'm not sure it would correct all your problem but in my case I do use the forceFitColumns and then depending how I want my column to react in size I will use a combination of minWidth and width, and in some cases the ones that will never exceed a certain width, I would then use a maxWidth as well. Now the problem you have is when setting the minWidth to be the same with as maxWidth this of course will make it unresizable, well think about it you set a minimum and a maximum, SlickGrid is respecting it by now being able to size it afterwards. I also have my grid which takes 95% width of my screen so I have a little padding on the side and with it I use a auto-resize using jQuery.
Here is my code:
// HTML Grid Container
<div id="myGridContainer" style="width:95%;">
<div class="grid-header" style="width:100%">
<label>ECO: Log / Slickgrid</label>
<span style="float:right" class="ui-icon ui-icon-search" title="Toggle search panel" onclick="toggleFilterRow1()"></span>
</div>
<div id="myGrid" style="width:100%;height:600px;"></div>
<div id="myPager"></div>
</div>
// My SlickGrid Options
var options = {
enableCellNavigation: true,
forceFitColumns: true
};
// The browser auto-resize
$(window).resize(function () {
$("#myGrid").width($("myGridContainer").width());
$(".slick-viewport").width($("#myGrid").width());
grid.resizeCanvas();
});
EDIT
I also was annoyed by the fact that using all of these together is blocking you from resizing the width of the column. I came up with a different solution, much later after, which makes the fields to expand (take available width) and does not block you afterwards on resizing the width. So this new solution I believe is giving you exactly what you are looking for... First of all remove the maxWidth property and only use minWidth and width, actually you could probably use only the width if you wanted. Now I had to unfortunately, modify 1 of the core file slick.grid.js with the following code:
//-- slick.grid.js --//
// on line 69 insert this code
autoExpandColumns: false,
// on line 1614 PREVIOUS CODE
if (options.forceFitColumns) {
autosizeColumns();
}
// on line 1614 change to this NEW CODE
if (options.forceFitColumns || options.autoExpandColumns) {
autosizeColumns();
}
then going back to my grid definition, I replace my previous options with this:
// My NEW SlickGrid Options
var options = {
enableCellNavigation: true,
forceFitColumns: false, // make sure the force fit is false
autoExpandColumns: true // <-- our new property is now usable
};
with this new change it has some functionality of the force fit (expanding) but does not restrict you on resizing your columns width afterwards like the force fit does. I also tested it with the columnPicker, if you hide a column it's resizing the others accordingly. I also modified the file slick.columnpicker.js to include a checkbox for that property but that is totally optional...I can add the code for that too if any of you want it as well. Voila!!! :)
EDIT #2
I realized much later that there's no need to modify the core file, we can simply call grid.autosizeColumns() after the grid creation. Like this
var options = { forceFitColumns: false };
grid = new Slick.Grid("#myGrid", data, columns, options);
// will take available space only on first load
grid.autosizeColumns();
This will automatically resize the columns to fit the screen on first load but will not give you the restriction of the forceFitcolumns flag.
I know it's kind late for this reply.
But i've managed to do that without having to change things at slick.grid.js or set min/maxWidth at columns array.
Instead what i did was to iterate through the columns array adding the values of "width" field of each column and then i've did a simple math count to set the last column width as innerWidth - totalColumsWidth + lastColumnWidth.
Code:
function lastColumnWidth(columns)
{
var widthSum = 0;
angular.forEach(columns, function(col) {
if(col.width) { widthSum = col.width + widthSum; }
});
if(window.innerWidth > widthSum) {
columns[columns.length-1].width = columns[columns.length-1].width + (window.innerWidth - widthSum);
}
return columns;
}

Finding Out Which Elements Are in the Viewport in Dojo

I would like to fire a function when a user is scrolling and reaches a certain element, so that I can mark that element "read" (think of how posts were marked read after viewing them in the now defunct Google Reader). So, for example, imagine a page like the following:
<div id="1">Node 1</div>
<div id="2">Node 2</div>
<div id="3">Node 3</div>
<div id="4">Node 4</div>
Assuming only the first two <divs> fit within the viewport, I would like to use Dojo's event listener and fire an event when div 3 came into view and again when div 4 came into view. (In addition to my goal of recording when an element has been read, this would also allow me to easily implement infinite scrolling.)
The jQuery Waypoints tool seems to do exactly what I want, but since most of my code uses Dojo, I really don't want to use jQuery just for this one task. I'd like to find a prewritten tool or simple method for doing this that isn't dependent on a library/toolkit other than Dojo (I'm fine with it using Dojo or no library at all).
UPDATE: It appears Skrollr also does what I am looking for, but only for animation -- that is, it can animate an element based on its relationship to the viewport. This doesn't exactly help, though, since my goal isn't animation but to activate a function (that will make an AJAX call via Dojo) when an element comes within the viewpoint.
Thanks to a few pointers from a friend, I was able to put together a functional solution to this question. Assuming all the divs are within another div with the id articles, when the user scrolls the bottom of the element at least 50px into the viewport, I then apply a "read" class to the element.
posts = dojo.query("#articles > div");
dojo.connect(window, 'onscroll', function(){
var vs = win.getBox();
var output;
var readCount;
for (var i = 0; i < posts.length; i++) {
var includeScroll = false;
var locInfo = domGeom.position(posts[i], includeScroll)
var fullyOnScreen = locInfo.y + locInfo.h + 50;
if ((fullyOnScreen > 0) && (fullyOnScreen < vs.h)) {
//Apply "read" class to div if it has been scrolled to.
domClass.add(posts[i], "read");
}
if (locInfo.y > vs.h) {
break;
}
}
});
Remaining question: While this solves my goal, I'm not sure it is the most efficient to poll the location of every div in or above the viewport every time the user scrolls -- even multiple times per scroll event. Are there ways to make this more efficient? It seems to run OK on my Core 2 Duo system, but I don't want to be sloppy just because it doesn't seem laggy.
I thought about calculating the position of all of the divs from the absolute top just once on start up and then doing some comparisons between the viewport and the top each time I scroll. This would probably be more efficient, but I'm not sure if the efficiency gains justify the added complexity of the code.

How do I disable horizontal scrollbar in jScrollPane (JQuery)?

Can you guys please let me know what is the best way to disable the horiontal scroll bar?
I have div with width: 100% and height :280px. When we have long continuous text (without any spaces), we are getting a horizontal scrollbar displayed.
Btw I am using jscrollPane.
Any suggestions would be appreciated.
What I have found in jScrollPane - settings object documentation:
contentWidth - int (default undefined)
The width of the content of the scroll pane. The default value of
undefined will allow jScrollPane to calculate the width of it's
content. However, in some cases you will want to disable this (e.g. to
prevent horizontal scrolling or where the calculation of the size of
the content doesn't return reliable results)
So to get rid of horizontal bars, just set content width lower than the container width.
Example:
$('#element').jScrollPane({
contentWidth: '0px'
});
The answer from SÅ‚awek Wala (contentWidth: '0px') is a really magic wand :)
In IE8 unnecessary horisontal scrollbar appears often upon elastic containers. But that's only part of the trouble: when horisontal scrollbar appears the content overflows through both vertical gutter and scrollbar.
So, if one disables horisontal scrollbar just making it invisible (as the other answers suggest) then the second part of the trouble remains.
contentWidth: '0px' fixes the both symptoms.
However, knowncitizen was right, '0px' does something weird with the jScrollPane because contentWidth is an integer property (btw contentWidth: 'foo' gives us the same pretty result ).
To avoid unpredictable effects one can use any positive but small enough number like this: contentWidth: 1
This is quite outdated question. But in case someone has same issue as you and I:
as I haven't found any property or API call to achieve this, I used simple solution - disabled via CSS:
.jspHorizontalBar { display: none !important; }
Not very elegant way, but saved time of investigating or 'hacking' jScrollPane code.
Pass horizontalDragMaxWidth: 0 to the options.
None of the solutions worked for me here so here's what I did using nested divs:
JS
$('#scrollpane').jScrollPane();
HTML
<div id="scrollpane" style="max-height: 400px; width: 700px">
<div style="overflow:hidden; width: 650px">
Your long content will be clipped after 650px
</div>
</div>
I was able to accomplish this using CSS.
Since the parent should have the class horizontal-only, when we only want a horizontal bar, I added the class jspVerticalBar as a child so that when it appears ONLY under the horizontal-only class, it will not display it.
It will still work if you have set the vertical and horizontal on the same page.
div.horizontal-only .jspVerticalBar { display:none; }
After trying and failing with the other answers, we had to hack jScrollPane to make this work. In jquery.jscrollpane.js, line 171:
pane.css('overflow', 'auto');
// Hack: Combat size weirdness with long unbreakable lines.
pane.css('position', 'static');
// End hack
if (s.contentWidth) {
contentWidth = s.contentWidth;
} else {
contentWidth = pane[0].scrollWidth;
}
contentHeight = pane[0].scrollHeight;
// Hack: Continued.
pane.css('position', 'absolute');
// End hack
pane.css('overflow', '');
Not sure how safe it is but that works for us.
For me, the best solution was in to add left: 0 !important; for classes .customSelect and .jspPane in the CSS:
.customSelect .jspPane {
overflow-x: hidden;
left: 0 !important;
}

Resources