0

This is a Javascript project, using the jQuery library.

I have a sticky bar I would like to show whenever the browser window is below a certain page position, and otherwise hide it. Its ID is #sticky-atc-bar.

The page position is determined by the top of an element with class .product-section.

Based on research into how to do this, I originally came up with the following JS.

$( document ).ready(function() {
    $('#sticky-atc-bar').hide();

    var section = $(".product-section");
    var offsetSection = section.offset().top; //check for top property
    $(function() {
        $(window).scroll(function() {
        console.log('Scroll Event');

            if ($(window).scrollTop() >= offsetSection) { // reached product section
                console.log('True:');
                $('#sticky-atc-bar').show();
            } else { // not reached product section
                console.log('False');
                $('#sticky-atc-bar').hide();
            }
        });
    }); 
});

This works. Yet, I am aware it generates a lot of scroll events, the entire time the user is engaged with the page. Knowing that's very inefficient, I went looking for alternative approaches. Along the way I read this article called "Learning from Twitter", which made total sense.

I also came across this solution to a similar requirement. My version of that code is:

var target = $(".product-section").offset().top,
    timeout = null;

$('#sticky-atc-bar').hide();

$(window).scroll(function () {
    if (!timeout) {
        timeout = setTimeout(function () {
            console.log('scroll');            
            clearTimeout(timeout);
            timeout = null;
            if ($(window).scrollTop() >= target) {
                 $('#sticky-atc-bar').show();            
            } else {
                 $('#sticky-atc-bar').hide();
            }
        }, 250);
    }
});

I set it up on this jsFiddle, and it works.

However ... It still generates a significant number of events. They have simply been reduced to one event every 250 ms, so 4 every second.

What I would like to know is if there's a better way to go about this? Put another way, aside from changing the timeout on this second piece of code, can I reduce this operation to even fewer events? And is that even necessary at this point, or is one event every 250ms not a significant impact?

inspirednz
  • 4,807
  • 3
  • 22
  • 30
  • IntersectionObserver? then you don't need to use jQuerty – Bravo Jun 23 '22 at 03:20
  • Reading up on Intersection Observer now. Looks like it might do the trick. I see there's no support on Internet Explorer, but year to date, only 0.38% of users on the site used IE. I'm going to see if I can get my head around how to implement Intersection Observer. – inspirednz Jun 23 '22 at 03:36

1 Answers1

1

Here you go: https://jsfiddle.net/son0azfj/

var productEl = document.querySelector('.product-section')
var stickyEl = document.querySelector('#sticky-atc-bar')

// check if we have elements
if (productEl && stickyEl) {
  // create an observer object, binding toggleDisplayFactory to it
  var observer = new IntersectionObserver(toggleDisplayFactory(stickyEl))
  
  // observe the product element
  observer.observe(productEl)
}

// [1] create a named function which accepts an element...
function toggleDisplayFactory(element) {
  // and returns a function - the handler that IntersectionObserver expects
  return function handler(entries) {
      // determine if the observed element is in the viewport or not
      var isInViewPort = Boolean(entries.find(entry => entry.intersectionRatio > 0))
      /**
       * if in the viewport:
       *   we want the element passed in at [1] to use it's own styles, i.e. remove the inline style property
       * else:
       *   we want the element to inherit its default display property (e.g. block, inline, inline-block, etc.) and thus be visible
       */
      var display = isInViewPort ? null : 'inherit'
    
      // set the display property of the element passed in at [1]
      element.style.display = display
  }
}
  1. use InterSectionObserver to watch for the position of an element with respect to the viewport. Use that element's position in the viewport to "do something else", i.e. show / hide another element
  2. avoid setTimeout for manipulating the DOM - use requestAnimationFrame instead (not required here)
  3. avoid querying DOM elements repeatedly - get them once, and reference them where they need to be manipulated
  4. prefer setting defaults in CSS, and then in JS override a specific property inline, removing the property when you no longer need it

Reasoning:

IntersectionObserver allows you to observe an arbitrary element with respect to its position in the viewport, instead of a scroll event which fires rapidly as one scrolls. The events an observed element trigger are only triggered when its position relative to the viewport changes

setTimeout - If you were to use a timeout, you would need to think of some number which is appropriate to use when using setTimeout. How do you do this? Well, you can only guess... you'd have to use a magic number. requestAnimationFrame instead knows when the DOM is ready for re-rendering and calculating layout - no magic numbers required


Links:


EDIT: Feel free to replace document.querySelector with $(selector) - the rest of the code will work as-is.

Larry
  • 1,238
  • 2
  • 20
  • 25
  • Thank you Larry. This is very helpful. I'm glad to have learned of Intersection Observer, and how to go about implementing it. I also appreciate your other tips / feedback. BTW, I visited South Africa late last year. Beautiful part of the world. – inspirednz Jun 23 '22 at 21:06
  • @inspirednz no problem, glad I could help :) Great to hear you've visited - it's indeed a beautiful place! – Larry Jun 23 '22 at 21:08
  • Hi @Larry, I have a question for you. If I wanted to add / remove a class from another selector, as part of the "if in the viewport / else" part of your JS, how would I go about that? As there's no actual `if then` statement in that JS, I've been struggling to get my head around how to add code for changing CSS selectors. – inspirednz Jul 14 '22 at 03:40
  • Actually … after playing around in the jFiddle, it was much easier than I anticipated. I was able to just an in `if (isInViewPort == true) { … [add the class] … } else { … [remove the class] … }`. Seems to work. Thanks again. – inspirednz Jul 14 '22 at 04:35
  • @inspirednz haha ye, I'm using a few advanced approaches in that code. There's no `if / else` in there because I'm using a ternary operator - you can see where `display` is defined. A ternary _is_ an `if / else`, just more terse. Using `if / else` is the same as what I've done there, just a few more characters and lines. Also, you don't need to use `isInViewPort === true` - `isInViewPort` is already a `boolean`, you could use `if (isInViewPort) {...}` and you'd get the same result :) – Larry Jul 29 '22 at 23:23