Load function on scroll

Hi all,

Please I need help with this script, currently the counter starts counting on page load. I need the numbers to count as the page is scrolled to each element.
Thank you

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Counting</title>
  <link rel="stylesheet" href="./style.css">

</head>
<body>

<div class="number">$6,350,354.43</div>
<div class="month">March</div>
<div class="number">$8,500,435.33</div>
<div class="month">April</div>
<div class="number">$3,500,435.53</div>
<div class="month">May</div>


<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='script.js'></script>

</body>
</html>

.number {
	display: block;
	font-size: 6rem;
	line-height: 6.5rem;
}
.number * + * {
	margin-top: 0;
}

.digit-con {
	display: inline-block;
	height: 6.5rem;
	overflow: hidden;
	vertical-align: top;
}
.digit-con span {
	display: block;
	font-size: 6rem;
	line-height: 6.5rem;
	position: relative;
	text-align: center;
	top: 0;
	width: 0.55em;
}
.month{	
	height:600px;
}


function Counter(obj){
  
  // get the number
  var number = obj.text();
  obj.attr('data-number',number);
  
  // clear the HTML element
  obj.empty();
  
  // create an array from the text, prepare to identify which characters in the string are numbers
  var numChars = number.split("")
  var numArray = [];
  var setOfNumbers = [0,1,2,3,4,5,6,7,8,9];

  // for each number, create the animation elements
  for(var i=0; i<numChars.length; i++) { 
    if ($.inArray(parseInt(numChars[i], 10),setOfNumbers) != -1) {
      obj.append('<span class="digit-con"><span class="digit'+numArray.length+'">0<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br></span></span>');
      numArray[numArray.length] = parseInt(numChars[i], 10);
    }
    else {
      obj.append('<span>'+numChars[i]+'</span>');
    }	
  }

  // determine the height of each number for the animation
  var increment = obj.find('.digit-con').outerHeight();
  var speed = 2000;
  
  // animate each number
  for(var i=0; i<numArray.length; i++) {
    obj.find('.digit'+i).animate({top: -(increment * numArray[i])}, Math.round(speed / (1 + (i * 0.333))));
  }
}

$(document).ready(function(){
  $('.number').each(function(){
	  Counter($(this));
	});
});

I think the IntersectionObserver would be good for this:

The code was taken from a previous thread so hopefully is robust enough. :slight_smile:

3 Likes

Wow, thank you, it’s awesome

1 Like

Please how can the arrow functions be written as regular functions?

$(document).ready(function () {
  const number = document.querySelectorAll(".number");

  function handleIntersection(entries) {
    entries.map((entry) => {
      if (entry.isIntersecting) {
        let elem = entry.target;
        Counter($(elem));
        observer.unobserve(entry.target);
      }
    });
  }

  const observer = new IntersectionObserver(handleIntersection);

  number.forEach((item) => observer.observe(item));
});

Here is how I convert arrow functions. Here’s the function as you provided:

  function handleIntersection(entries) {
    entries.map((entry) => {
      if (entry.isIntersecting) {
        let elem = entry.target;
        Counter($(elem));
        observer.unobserve(entry.target);
      }
    });
  }

First I check that no weirdness from using the this keyword occurs, as arrow-notation changes how that is handled. There are no this keywords being used so all is good there.

The function keyword is added, and I prefer to also name the function:

    // entries.map((entry) => {
    entries.map(function checkIntersection(entry) => {

Then braces are added if needed, to enclose the function contents. That isn’t needed here.

Then a return statement is added to the function if needed. It’s not needed here so carry on.

The arrow notation then gets removed:

    // entries.map(function checkIntersection(entry) => {
    entries.map(function checkIntersection(entry) {

The function could then be easily extracted from there as well, but that’s an optional extra.

function checkIntersection(entry) {
  if (entry.isIntersecting) {
    let elem = entry.target;
    Counter($(elem));
    observer.unobserve(entry.target);
  }
}
function handleIntersection(entries) {
  entries.map(checkIntersection);
}
2 Likes

When I write it like this, it works on desktop, but not on mobile, please how do I get it to function properly?

$(document).ready(function () {
  const number = document.querySelectorAll(".number");
    function handleIntersection(entries) {
		entries.map(function checkIntersection(entry) {
			      if (entry.isIntersecting) {
        var elem = entry.target;
        Counter($(elem));
        observer.unobserve(entry.target);
      }
    });
  }
  const observer = new IntersectionObserver(handleIntersection);

  number.forEach(function loopIntersection(item) {
	  observer.observe(item)
	  });
});

If it helps here is the compatibility chart for IntersectionObserver.

Does it work properly on mobile as the previous arrow notation? That shouldn’t be the problem, but it’s worth making sure.

My codepen above is working fine on my iPhone :slight_smile:

Yes the previous arrow function works on mobile.
I wanted to use this originally to load on scroll, please how do I use the inVisible function in this script

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Counting</title>
  <link rel="stylesheet" href="./style.css">

</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="intro">
  <h1>Scroll</h1>
</div>
<div class="container-fluid text-center">
  <h1>Some cool facts</h1>
  <div class="row">
    <div class="col-sm-3">
      <h2 data-max="50">+ Happy Customers</h2>
    </div>
    <div class="col-sm-3">
      <h2 data-max="25000">+ Lines of code</h2>
    </div>
    <div class="col-sm-3">
      <h2 data-max="10">+ Projects</h2>
    </div>
    <div class="col-sm-3">
      <h2 data-max="30" id="test">+ Developers</h2>
    </div>
  </div>
</div>
<div class="intro">
</div>

<script  src="./script.js"></script>

</body>
</html>

div {
  padding: 0;
  margin: 0;
}
i {
  font-size: 4em !important;
  margin-top: 10%;
  color: teal;
}

h1 {
  padding-top: 30px;
  color: white !important;
}

h2 {
  color: teal;
}

.intro {
  height: auto;
  min-height: 100vh;
  text-align: center;
  background-color: teal;
}

function inVisible(element) {
  //Checking if the element is
  //visible in the viewport
  var WindowTop = $(window).scrollTop();
  var WindowBottom = WindowTop + $(window).height();
  var ElementTop = element.offset().top;
  var ElementBottom = ElementTop + element.height();
  //animating the element if it is
  //visible in the viewport
  if ((ElementBottom <= WindowBottom) && ElementTop >= WindowTop)
    animate(element);
}

function animate(element) {
  //Animating the element if not animated before
  if (!element.hasClass('ms-animated')) {
    var maxval = element.data('max');
    var html = element.html();
    element.addClass("ms-animated");
    $({
      countNum: element.html()
    }).animate({
      countNum: maxval
    }, {
      //duration 5 seconds
      duration: 5000,
      easing: 'linear',
      step: function() {
        element.html(Math.floor(this.countNum) + html);
      },
      complete: function() {
        element.html(this.countNum + html);
      }
    });
  }

}

//When the document is ready
$(function() {
  //This is triggered when the
  //user scrolls the page
  $(window).scroll(function() {
    //Checking if each items to animate are 
    //visible in the viewport
    $("h2[data-max]").each(function() {
      inVisible($(this));
    });
  })
});

I’m not sure what you are asking as that invisible function seems t be working in the code you posted above?

Or did you mean you want to use that new htm (and some of the js) with the IntersectionObserver example?
e.g.

You seem to have a working example using the scroll method so can you clarify your current requirements please :slight_smile:

Please how can the inVisible function be used to call the function in the original post. The technique used by the inVisible function to load on scroll works both on mobile & desktop and uses regular functions

If you mean you want to mix those two of your scripts together with your original html and js then I guess it would look like this:

I’ll leave it up to @Paul_Wilkins to comment on its efficiency as I just copy and pasted that from what you posted.:slight_smile:

So do the examples I have given you. :slight_smile:

Note that the scroll event doesn’t always work that well in some mobiles as it doesn’t get continually updated while the element is being scrolled (because it’s essentially dragged). Unlike the IntersectionObserver method which is constantly monitored. Also the scroll event can hi-jack the performance of the page unless it is throttled or debounced in some way.

Its working, only that now the numbers do not start with zero. Please how do the numbers start from zero.
The IntersectionObserver is cool, but the main goal is to express the whole script with regular functions due to compatibility concerns

They do start from zero but only once the element is fully in the viewport. That’s what the code you asked to use does :slight_smile:

You could change it so that it starts as soon as a part of it is visible.

e.g. Change the element.height() to just a small offset of 20.

//var ElementBottom = ElementTop + element.height();
var ElementBottom = ElementTop + 20;

Codepen updated:

Otherwise you’d probably have to cycle through the elements to set them to zero when they are hidden before the scroll happens.

Please is there another elegant way to achieve this?

The thing is, the first count object is in view before the page scroll begins.

You would need to call the routine by default first so that it runs before anything has scrolled.

e.g…

Hopefully @Paul_Wilkins will be around later to tidy it up but it seems to be working.

1 Like

My main concern is in regard to the name of the inVisible function. That’s quite the confusing name.

The function itself does two different things, when it should only do one thing instead. Right now the function is checking if the element is visible, and it’s also calling the Counter function when it is visible.

That second part needs to be moved out of the function so that it can be rena… no, wait. Instead of moving out the second part, we can instead extract code out to a separate function called isVisible, and make good progress from there.

function isVisible(element) {
  //Checking if the element is
  //visible in the viewport
  var WindowTop = $(window).scrollTop();
  var WindowBottom = WindowTop + $(window).height();
  var ElementTop = element.offset().top;
  //var ElementBottom = ElementTop + element.height();
  var ElementBottom = ElementTop + 20;
  //animating the element if it is
  //visible in the viewport
  return ElementBottom <= WindowBottom && ElementTop >= WindowTop;
}
function inVisible(element) {
  if (isVisible(element)) {
    Counter(element);
  }
}

We can now inline the inVisible function, completely removing the need for that poorly named function.

  $(".number").each(function () {
    if (isVisible($(this))) {
      Counter($(this));
    }
  });
  $(window).scroll(function () {
    $(".number").each(function () {
      if (isVisible($(this))) {
        Counter($(this));
      }
    });
  });

That has though created another problem, one of duplication. We can solve that by triggering the scroll event after it’s been created.

  $(window).scroll(function () {
    $(".number").each(function () {
      if (isVisible($(this))) {
        Counter($(this));
      }
    });
  });
  $(window).trigger("scroll");

The $(this) is getting in the way of further improvements. We can just pass element to those functions, and improve the functions so that they convert any HTML element references into jQuery objects instead.

function isVisible(el) {
  element = $(el);
  ...
}
function Counter(el) {
  const obj = $(el);
  ...
}
...
  $(window).scroll(function () {
    $(".number").each(function () {
      if (isVisible(this)) {
        Counter(this);
      }
    });
  });
...

In the scroll function, I want to tease apart the if statement from calling the counter, so that a filter and a map can be used instead. That means using function parameters and working with those instead.

  $(window).scroll(function () {
    $(".number").each(function (i, el) {
      if (isVisible(el)) {
        Counter(el);
      }
    });
  });

Now that the element is being directly passed in to those functions, we can use filter and map to process things.

  $(window).scroll(function () {
    const counterNumbers = $(".number").toArray();
    counterNumbers.filter(isVisible).map(Counter);
  });

And lastly, we are setting up a scroll event, so there’s no need at all to wait for the document ready event. That ready wrapper can be completely removed.

//When the document is ready
// $(function () {
  $(window).scroll(function () {
    const counterNumbers = $(".number").toArray();
    counterNumbers.filter(isVisible).map(Counter);
  });
  $(window).trigger("scroll");
// });

Hopefully this updated code is the kind of improvement that was being looked for.

$(window).scroll(function () {
  const counterNumbers = $(".number").toArray();
  counterNumbers.filter(isVisible).map(Counter);
});
$(window).trigger("scroll");

2 Likes

Just one little addition I would want to add would be throttle — I don’t believe jquery does this for you. You can use lodash for convenience.

$(window).scroll(
  _.throttle(function () {
    const counterNumbers = $(".number").toArray();
    counterNumbers.filter(isVisible).map(Counter);
  }, 100) // 100ms intervals
);

lodash minified cdn

or alternatively I found just the debounce and throttle code here
lodash-throttle-debounce cdn

An article to read here

2 Likes

Thank you PaulOB for your touch of genius, truly the script does not start from zero, I hope to find out why it does that

Thank you Paul_Wilkins for explaining things in detail, I’ve gained some knowledge from your expertise

1 Like