For the mods and following users reading this thread:
This is not an issue related to the theme or WordPress.
That being said:
Your issue is related to the location of your script and the way it is loaded. Yes, simple as that ??
The issue:
Since browsers render the DOM from top to bottom, the browser will load your script at the moment it encounters it. Since the script is not deferred (see <script>: The Script element) and loaded in the head, it will fully load and execute your script before continuing with the DOM.
This means the script will run, search for “.counter” elements, won’t find any (since they are not loaded yet) and in the end it quits so the browser can continue rendering the DOM.
General notes about loading scripts and resources:
1. Scripts can (and should) be loaded asynchronously or at least at the very end to avoid render blocking threads.
2. If you need a script to do something with the content of your page, the script will have to be deferred, loaded at the very end (the footer) or listen to the “DOMContentLoaded” element and handle the functions and actions there.
To simplify my answer I provide a probably “cleaner” (at least in my opinion) solution to the JS code you try to accomplish. Simply put this script in your custom js file and it should run as expected.
/**
* Animates the counter of the provided element with the given settings.
* Requires the data-target attribute to be present for the given element.
*
* @param {Node|Element} counterElement The element to animate
* @param {number} speedInMillis The time the animation will take in millis
* @param {number} fps The fps of the animation
* @see https://www.ads-software.com/support/topic/problems-with-custom-js-file-in-child-theme/
*/
function animiateCounter(counterElement, speedInMillis = 1000, fps = 30) {
// check preconditions
if (
!counterElement ||
!counterElement.hasAttribute("data-target") ||
fps < 1
) {
// quit if preconditions not met
return;
}
// determine initial value from element
let value = +counterElement.innerText;
// determine target value from attribute
const targetValue = +counterElement.getAttribute("data-target"),
// define the interval timeout
intervalTimeout = (speedInMillis / fps) >> 0,
// define by how much the value should be incremented
incrementBy = ((target - value) / intervalTimeout) >> 0,
// define the interval to animate the counter
incrementorInterval = setInterval(() => {
// value exceeds the target value, so interrupt the counter
if (value >= targetValue) {
// clear the interval
clearInterval(incrementorInterval);
// quit here
return;
}
// increment the value (max out at target value)
value = Math.min(value + incrementBy, targetValue);
// request animation frame to paint the value
window.requestAnimationFrame(
() => (counterElement.textContent = value)
);
}, intervalTimeout);
}
// wait for the dom content to be loaded
document.addEventListener("DOMContentLoaded", (event) => {
document
// query the counter elements
.querySelectorAll(".counter")
// animate each queried counter
// change the animation values here as needed
.forEach((counterElement) => animiateCounter(counterElement, 1000, 30));
});
You can change the animation speed and the number of frames/steps by passing the corresponding arguments to the function at the very end.
Simply change this line to your liking by adjusting the duration of the animation (in this case 1000) and/or the fps (in this case 30).
.forEach((counterElement) => animiateCounter(counterElement, 1000, 30));