DEV Community

Cover image for Frontend System Design - Capture product visible on viewport when user stops scrolling.
Prashant Yadav
Prashant Yadav

Posted on • Originally published at learnersbucket.com

Frontend System Design - Capture product visible on viewport when user stops scrolling.

Visit learnersbucket.com If you are preparing for your JavaScript interview. You will find DSA, System Design and JavaScript Questions.


This system design question was asked to one of my linkedin connection in NoBroker’s interview. When he approached me regarding the solution of this. It immediately caught my attention and I solved this problem that day itself.

As it is an interesting problem I thought of writing an article around it, so here it is.

The question was quoted as “If user scroll and see any property and stays there for more than 5 sec then call API and store that property”.

Apart from online real-estate platform, this can be also applied on others platforms as well, such as social media like Facebook where if users reads a post for few seconds, store and use it to provide recommendations of new posts. Same can be used on E-commerce platform or other platforms where products are listed.

Let us see how should we approach such problems and then solve it with an example. I have created a dummy HTML template which contains different blocks, which we can use for testing.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .wrapper{
            display: flex;
            align-items: center;
            justify-content: center;
            flex-wrap: wrap;
        }

        .blocks{
            flex: 1 300px;
            height: 300px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            margin: 5px;
            background: red;
            font-size: 40px;
            color: #fff;
        }
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="blocks">1</div>
        <div class="blocks">2</div>
        <div class="blocks">3</div>
        <div class="blocks">4</div>
        <div class="blocks">5</div>
        <div class="blocks">6</div>
        <div class="blocks">7</div>
        <div class="blocks">8</div>
        <div class="blocks">9</div>
        <div class="blocks">10</div>
        <div class="blocks">11</div>
        <div class="blocks">12</div>
        <div class="blocks">13</div>
        <div class="blocks">14</div>
        <div class="blocks">15</div>
        <div class="blocks">16</div>
        <div class="blocks">17</div>
        <div class="blocks">18</div>
        <div class="blocks">19</div>
        <div class="blocks">20</div>
        <div class="blocks">21</div>
        <div class="blocks">22</div>
        <div class="blocks">23</div>
        <div class="blocks">24</div>
        <div class="blocks">25</div>
        <div class="blocks">26</div>
        <div class="blocks">27</div>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now when this web page will be scrolled, we will log the blocks which are within the viewport when ever user stops for more than 1 seconds.

A vital thing to keep in mind is to read the problem statement multiple times and then break the problem into sub-problems so that we can tackle each of them independently.

By reading the problem statement, I figured two sub-problems and decided to break it into two parts.

  • A way to check if the element is within the viewport.
  • A way to make the API call only after user stops scrolling and waits for sometime (5 secs in this case), if user scrolls before that then we should revoke the call.

Check if an element is within the viewport.

“With in the viewport” means then element which are within the visible part of the screens not within the visible area.

For this we will create a function which will return true or false, depending upon whether element is within the viewport of not.

To determine this we will use the Element.getBoundingClientRect() method which returns the elements position within the viewport. It returns an object with an element’s height and width, as well as it’s distance from the top, bottom, left, and right of the viewport.

// Get the H1
const h1 = document.querySelector('h1');

// Get it's position in the viewport
const bounding = h1.getBoundingClientRect();

// Log
console.log(bounding);
// {
//  height: 118,
//  width: 591.359375,
//  top: 137,
//  bottom: 255,
//  left: 40.3125,
//  right: 631.671875
// }
Enter fullscreen mode Exit fullscreen mode

Then next thing after getting the elements placement details is to determine if it is within the viewport.

If an element is in the viewport then its position from top and left will always be greater than or equal to 0. It’s distance from the right will be less than or equal to the total width of the viewport, and it’s distance from the bottom will be less than or equal to the height of the viewport.

There are couple of ways to get the width and height of the viewport.

For width, Some browsers support window.innerWidth while some support document.documentElement.clientWidth and some support both. We try using one of them and other as fallback to get the width using OR operator.

(window.innerWidth || document.documentElement.clientWidth)
Enter fullscreen mode Exit fullscreen mode

Similarly to get the height, some browsers support window.innerHeight while some support document.documentElement.clientHeight and some support both. Thus we can use the same fallback approach here as well.

(window.innerHeight || document.documentElement.clientHeight)
Enter fullscreen mode Exit fullscreen mode

Combining this together, We can check if the element is in the viewport like this.

const isInViewport = function (elem) {
     const bounding = elem.getBoundingClientRect();
     return (
       bounding.top >= 0 &&
       bounding.left >= 0 &&
       bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
       bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
};
Enter fullscreen mode Exit fullscreen mode

Now we can use this helper method on each element to determine if they are within the viewport or not.

First sub-problem is solved, now lets try to solve the second one.


Call a function when user stops scrolling or any other interactions for sometime.

For this we can use the debouncing technique.

Debouncing is a method or a way to execute a function when it is made sure that no further repeated event will be triggered in a given frame of time.

In simple words if the scroll event is not triggered again within the specified time (assume 5 seconds) then only invoke the function. This is implemented using the setTimeout timer function.

I have already explained two different variations of debouncing.

  1. Normal debouncing.
  2. Debouncing with Immediate flag.

Based on the use, we can chose any one of them. For this problem we will go with the normal one.

const debounce = (func, delay) => {
  let inDebounce;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(inDebounce);
    inDebounce = setTimeout(() => func.apply(context, args), delay);
  };
};
Enter fullscreen mode Exit fullscreen mode

This takes care of our second sub-problem. Now lets put this all together and create the final solution.


Putting everything together

Lets put each piece in the place to get the final picture.

Select all the elements / products / articles / blocks of the DOM which you want to store in the API call, as I have assigned blocks class to each of them, I will query select them all and store in a variable.

// Get all the products
const blocks = document.querySelectorAll('.blocks');
Enter fullscreen mode Exit fullscreen mode

Next thing is we will need a function which will check which elements are within the viewport and then take appropriate course of action thereafter.

// Helper function to check if element is in viewport
const isInViewport = function (elem) {
    const bounding = elem.getBoundingClientRect();
    return (
        bounding.top >= 0 &&
        bounding.left >= 0 &&
        bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
};

// Function which will make the API call
const getBlocks = function () {
      blocks.forEach((block) => {
        if (isInViewport(block)) {
          //make API call here
          console.log(block.innerText);
        }
  });

  // add a space
  console.log(" ");
 }
Enter fullscreen mode Exit fullscreen mode

Call this function after debouncing the scroll event for which we will have to assign an event listener.

// Debounce a function call
const debounce = (func, delay) => {
    let inDebounce;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func.apply(context, args), delay);
    };
};

// Assign the event listener
window.addEventListener('scroll', debounce(getBlocks, 1000), false);
Enter fullscreen mode Exit fullscreen mode

And that's it, we are done.

We can see the working of this in this image.

Capture product visible on viewport when user stops scrolling


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .wrapper{
            display: flex;
            align-items: center;
            justify-content: center;
            flex-wrap: wrap;
        }

        .blocks{
            flex: 1 300px;
            height: 300px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            margin: 5px;
            background: red;
            font-size: 40px;
            color: #fff;
        }
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="blocks">1</div>
        <div class="blocks">2</div>
        <div class="blocks">3</div>
        <div class="blocks">4</div>
        <div class="blocks">5</div>
        <div class="blocks">6</div>
        <div class="blocks">7</div>
        <div class="blocks">8</div>
        <div class="blocks">9</div>
        <div class="blocks">10</div>
        <div class="blocks">11</div>
        <div class="blocks">12</div>
        <div class="blocks">13</div>
        <div class="blocks">14</div>
        <div class="blocks">15</div>
        <div class="blocks">16</div>
        <div class="blocks">17</div>
        <div class="blocks">18</div>
        <div class="blocks">19</div>
        <div class="blocks">20</div>
        <div class="blocks">21</div>
        <div class="blocks">22</div>
        <div class="blocks">23</div>
        <div class="blocks">24</div>
        <div class="blocks">25</div>
        <div class="blocks">26</div>
        <div class="blocks">27</div>
    </div>

    <script>
        // Helper function to check if element is in viewport
        const isInViewport = function (elem) {
            const bounding = elem.getBoundingClientRect();
            return (
                bounding.top >= 0 &&
                bounding.left >= 0 &&
                bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
            );
        };

        // Debounce a function call
        const debounce = (func, delay) => {
            let inDebounce;
            return function() {
                const context = this;
                const args = arguments;
                clearTimeout(inDebounce);
                inDebounce = setTimeout(() => func.apply(context, args), delay);
            };
        };

        // Function which will make the API call
        const getBlocks = function () {
            blocks.forEach((block) => {
                if (isInViewport(block)) {
                    console.log(block.innerText);
                }
            });

            console.log(" ");
        }

        // Get all the products
        const blocks = document.querySelectorAll('.blocks');

        // Assign the event listener
        window.addEventListener('scroll', debounce(getBlocks, 1000), false);
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Discussion (3)

Collapse
sparkalow profile image
Brian Mathews

Interesting article. I wonder if an Intersection Observer could also be used to avoid debouncing the scroll event.

Collapse
learnersbucket profile image
Prashant Yadav Author

Yes, but that's is a new thing. This old school works in older browsers.

Collapse
sparkalow profile image
Brian Mathews

Sorry, I forget that not everyone gets to drop support for 20+ year old browsers.