React child updates change parent scroll position - javascript

I have a React container component that is suppose to house a list of child timer components. These child components are just countdown timers.
To achieve this my child component class uses setInterval to update the child every second. As the updates happened, I noticed as I scrolled down or up the list, the container component would suddenly jump up or down in a noticeably big way exactly when the child timers update themselves, but the container's render method is never called.
Along with this, if I simply don't scroll or stop scrolling, the jumping never happens. If I scrolled and then stopped scrolling, the onscroll handler keeps firing in sync with the child timer updates even though I stopped scrolling.
I've never encountered a situation like this before. Can React children implicitly force their parent container to randomly scroll up and down when updating?
Here is the code for the container:
class UserContentDetail extends React.Component {
constructor(props) {
super(props);
this.state ={
page: 1,
perPage: 15,
animes: [],
currTab: props.currTab
};
this.startApiRequests = this.startApiRequests.bind(this);
this.handleResponse = this.handleResponse.bind(this);
this.handleData = this.handleData.bind(this);
this.handleError = this.handleError.bind(this);
}
componentDidMount() {
this.startApiRequests();
}
componentDidUpdate() {
if (this.props.currTab !== this.state.currTab) {
this.startApiRequests();
}
}
startApiRequests() {
let currSeason = anilistApiConstants.SEASON_SUMMER;
let currSeasonYear = 2018;
let resultPromise = null;
switch(this.props.currTab) {
case sideNavConstants.SIDE_NAV_TAB_MY_ANIME:
resultPromise = api.getMyAnimes(this.props.myAnimeIds, this.state.page, this.state.perPage);
break;
case sideNavConstants.SIDE_NAV_TAB_POPULAR_ANIME:
resultPromise = api.getPopularAnimes(this.state.page, this.state.perPage);
break;
case sideNavConstants.SIDE_NAV_TAB_NEW_ANIME:
resultPromise = api.getNewAnimes(currSeason, currSeasonYear, this.state.page, this.state.perPage);
break;
}
resultPromise.then(this.handleResponse)
.then(this.handleData)
.catch(this.handleError);
}
handleResponse(response) {
return response.json().then(function (json) {
return response.ok ? json : Promise.reject(json);
});
}
handleData(data) {
let results = data.data.Page.media;
for (let i = 0; i < results.length; ++i) {
if (results[i].nextAiringEpisode == null) {
results[i].nextAiringEpisode = {empty: true};
}
}
this.setState({
page: 1,
perPage: 15,
animes: results,
currTab: this.props.currTab
});
}
handleError(error) {
alert('Error, check console');
console.error(error);
}
render() {
console.log('rendering list');
return(
<div className={userMasterDetailStyles.detailWrapper}>
<div className={userMasterDetailStyles.detailList}>
{this.state.animes.map(anime => <AnimeCard {...anime} key={anime.id} />)}
</div>
</div>
);
}
}
Here is the code for my timers (AnimeCardTime) and which is surrounded by a card container (AnimeCard):
class AnimeCardTime extends React.Component {
constructor(props) {
super(props);
this.state = {
timeUntilNextEpisode: props.timeLeft,
animeId: props.id
};
this.countdownTimer = null;
this.getNextEpisodeTimeUntilString = this.getNextEpisodeTimeUntilString.bind(this);
this.startTimer = this.startTimer.bind(this);
this.endTimer = this.endTimer.bind(this);
}
componentDidMount() {
this.startTimer();
}
componentWillUnmount() {
this.endTimer();
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.animeId) {
return {
timeUntilNextEpisode: nextProps.timeLeft,
animeId: nextProps.id
};
}
return null;
}
startTimer() {
if (this.props.timeLeft != undefined) {
this.countdownTimer = setInterval(() => {
this.setState({
timeUntilNextEpisode: this.state.timeUntilNextEpisode - 60,
});
}, 1000);
}
}
endTimer() {
if (this.countdownTimer != null) {
clearInterval(this.countdownTimer);
}
}
secondsToTimeString(timeSecondsUntil) {
timeSecondsUntil = Number(timeSecondsUntil);
let d = Math.floor(timeSecondsUntil / (3600*24));
let h = Math.floor(timeSecondsUntil % (3600*24) / 3600);
let m = Math.floor(timeSecondsUntil % (3600*24) % 3600 / 60);
let dDisplay = d > 0 ? d + 'd ' : '';
let hDisplay = h > 0 ? h + 'hr ' : '';
let mDisplay = m > 0 ? m + 'm ' : '';
return dDisplay + hDisplay + mDisplay;
}
getNextEpisodeTimeUntilString() {
if (this.props.timeLeft != undefined) {
return 'Ep ' + this.props.nextEpisode + ' - ' + this.secondsToTimeString(this.state.timeUntilNextEpisode);
}
else {
return this.props.season + ' ' + this.props.seasonYear;
}
}
render() {
return(<h6 className={userMasterDetailStyles.cardTime}>{this.getNextEpisodeTimeUntilString()}</h6>);
}
}
const AnimeCard = (props) => {
let secondsToTimeString = (timeSecondsUntil) => {
timeSecondsUntil = Number(timeSecondsUntil);
let d = Math.floor(timeSecondsUntil / (3600*24));
let h = Math.floor(timeSecondsUntil % (3600*24) / 3600);
let m = Math.floor(timeSecondsUntil % (3600*24) % 3600 / 60);
let dDisplay = d > 0 ? d + 'd ' : '';
let hDisplay = h > 0 ? h + 'hr ' : '';
let mDisplay = m > 0 ? m + 'm ' : '';
return dDisplay + hDisplay + mDisplay;
};
let getNextEpisodeTimeUntilString = () => {
if (props.status === anilistApiConstants.STATUS_RELEASING) {
return 'Ep ' + props.nextAiringEpisode.episode + ' - ' + secondsToTimeString(props.nextAiringEpisode.timeUntilAiring);
}
else {
return props.season + ' ' + props.startDate.year;
}
};
return(
/* <h6 className={userMasterDetailStyles.cardTime}>{getNextEpisodeTimeUntilString()}</h6> */
<a className={userMasterDetailStyles.animeCardLinkContainer} href={props.siteUrl}>
<div className={userMasterDetailStyles.animeCardContainer}>
<h6 className={userMasterDetailStyles.cardTitle}>{props.title.romaji}</h6>
<AnimeCardTime timeLeft={props.nextAiringEpisode.timeUntilAiring} nextEpisode={props.nextAiringEpisode.episode} season={props.season} seasonYear={props.startDate.year} id={props.id}/>
<img className={userMasterDetailStyles.cardImage} src={props.coverImage.large}/>
<p className={userMasterDetailStyles.cardDescription}>{props.description.replace(/<(?:.|\n)*?>/gm, '')}</p>
<p className={userMasterDetailStyles.cardGenres}>{props.genres.reduce((prev, curr) => {return prev + ', ' + curr;})}</p>
</div>
</a>
);
};

I realized the problem was more with the css rather than the react code. I had not set an explicit height for the container and I assume it was this uncertainty that caused the browser to suddenly scroll up and down the container whenever the list elements re-rendered/updated themselves.

Related

React useEffect and useState returns a number instead of an object

I've been battle with this problem for 2 days now, I'm trying to implement a continuous vertical text scrolling, but for some reasons unknown to me react returns a number instead of my array object (after I logged the output to see what was going wrong with my code) at the second iteration (as the array is constantly updated at interval). For the record, this was initially implemented in an Angular JS application, but I'm trying to convert it to react with useEffect() and useState() to update the changes in the array.
Below is what I've done so far:
const Skills = () => {
let skillWrap = useRef();
let [activeSkills, setActiveSkills] = useState([]);
console.log(activeSkills);
console.log(typeof activeSkills);
let sMax = totalSkills.length - 1; // 0 -> 19
let activeStart = Math.floor(Math.random() * (sMax + 1));
let activeEnd = activeStart === 0 ? sMax : activeStart - 1;
for (let e = activeStart; e <= sMax; e++) {
setActiveSkills(activeSkills.push(totalSkills[e]));
}
if (activeStart !== 0) {
for (let s = 0; s <= activeEnd; s++) {
setActiveSkills(activeSkills.push(totalSkills[s]));
}
}
let scrollDis = 0,
scrollingDown = false,
scrollingUp = false,
scrollingDownSelf = false,
scrollingUpSelf = false,
scrollCatchInterval = 40,
scrollDirection = "up",
hasScrolledRecently = false;
const wheelEventHandler = e => {
let skillFocused = skillWrap.current.childNodes[19];
skillFocused.classList.remove("active");
function animateScroll(scrollDis, callback) {
let curLeftTop = scrollDis * 8,
curLeftFinal = scrollDis * 4;
tween(skillWrap.current, -curLeftFinal, curLeftTop, 1, callback);
}
function scrollUp() {
setTimeout(() => {
for (let su = 0; su < scrollDis; su++) {
activeEnd--;
activeStart--;
if (activeEnd < 0) activeEnd = 19;
if (activeStart < 0) activeStart = 19;
/*setActiveSkills(activeSkills.unshift(totalSkills[activeStart]));
setActiveSkills(activeSkills.pop());*/
activeSkills.unshift(totalSkills[activeStart]);
activeSkills.pop();
}
skillFocused.classList.add("active");
skillWrap.current.style.transform = "none";
scrollDis = 0;
scrollingUp = false;
scrollingUpSelf = false;
if (e.deltaZ === 0) {
setTimeout(() => {
hasScrolledRecently = false;
}, 3000);
}
}, 0);
}
function scrollDown() {
setTimeout(() => {
for (let sd = 0; sd < Math.abs(scrollDis); sd++) {
activeEnd++;
activeStart++;
if (activeEnd > 19) activeEnd = 0;
if (activeStart > 19) activeStart = 0;
/*setActiveSkills(activeSkills.push(totalSkills[activeEnd]));
setActiveSkills(activeSkills.shift());*/
activeSkills.push(totalSkills[activeEnd]);
activeSkills.shift();
}
skillFocused.classList.add("active");
skillWrap.style.transform = "none";
scrollDis = 0;
scrollingDown = false;
scrollingDownSelf = false;
if (e.deltaZ === 0) {
setTimeout(() => {
hasScrolledRecently = false;
}, 3000);
}
}, 0);
}
if (
(e.deltaY === 100 || e.deltaY === 3) &&
!scrollingUp &&
!scrollingDownSelf
) {
// (scroll down) add skill to bottom & remove skill from top
scrollDirection = "down";
scrollDis--;
scrollingDown = true;
if (e.deltaZ === 0) hasScrolledRecently = true;
let scd = scrollDis;
setTimeout(() => {
if (scrollDis === scd) {
if (scrollDis < -6) scrollDis = -6;
scrollingDownSelf = true;
animateScroll(scrollDis, scrollDown);
}
}, scrollCatchInterval);
} else if (
(e.deltaY === -100 || e.deltaY === -3) &&
!scrollingDown &&
!scrollingUpSelf
) {
// (scroll up) add skill to top & remove skill from bottom
scrollDirection = "up";
scrollDis++;
scrollingUp = true;
if (e.deltaZ === 0) hasScrolledRecently = true;
let scu = scrollDis;
setTimeout(() => {
if (scrollDis === scu) {
if (scrollDis > 5) scrollDis = 5;
scrollingUpSelf = true;
animateScroll(scrollDis, scrollUp);
}
}, scrollCatchInterval);
}
};
function tween(o, x, y, durationSecs, onComplete) {
let fps = 30,
count = 0,
stopAt = fps * durationSecs,
easef = Quad_easeInOut;
let f = function() {
count++;
if (count >= stopAt) {
tween_stop(o);
if (onComplete) onComplete();
} else {
tween_setProperty(
o,
easef(count, 0, x, stopAt),
easef(count, 0, y, stopAt)
);
}
};
clearInterval(o._tween_int);
o._tween_int = setInterval(f, (durationSecs * 1000) / fps);
}
function tween_stop(o) {
clearInterval(o._tween_int);
}
function tween_setProperty(o, x, y) {
o.style.cssText += ";transform:translate3d(" + x + "vw," + y + "vh,0);";
}
function Quad_easeInOut(t, b, c, d) {
if ((t /= d / 2) < 1) return (c / 2) * t * t * t * t + b;
return (-c / 2) * ((t -= 2) * t * t * t - 2) + b;
}
useEffect(() => {
/*console.log(activeSkills);*/
setTimeout(() => {
skillWrap.current.childNodes[19].classList.add("active");
}, 2000);
window.addEventListener("wheel", wheelEventHandler);
function constantScroll() {
// prevents scrolling while changing views
setTimeout(function() {
// emulate scrolling of the skill list
let scrollEvent = new WheelEvent("wheel", {
deltaY: scrollDirection === "up" ? -100 : 100,
deltaZ: 1 // used to differentiate between user scroll / programmatic scroll
});
if (!hasScrolledRecently) {
// 3 scroll events are dispatched to mirror scrolling of 3 skills
for (let r = 0; r < 3; r++) {
window.dispatchEvent(scrollEvent);
}
}
constantScroll();
}, 3000);
}
// wait 3 seconds before issuing first scroll
setTimeout(function() {
constantScroll();
}, 2000);
return () => {
window.removeEventListener("wheel", wheelEventHandler);
console.log("Skills Component will unmount");
};
}, [activeSkills]);
return (
<div>
<div className="view skills active active-f">
<div className="header-container skills">
<div className="header-title-wrap">
<div className="cover" />
<h1 className="header big first">My</h1>
<h1 className="header big last">Skillsset</h1>
</div>
<div className="header-info-wrap">
<div className="header-content-body skill-one">
<div className="line-left" />
<p className="header body about-one">
The core of my skill set largely surrounds the MERN{" "}
<strong>stack</strong>, to achieve both a clear and dynamic user
experience. I also have some experience with mobile integration
(Android and iOS). Strengthening my grasp of the mobile app
development space is one of my primary goals for the near
future.
</p>
</div>
</div>
</div>
<div className="skill-container active active-f">
<div className="skill-wrap">hey</div>
</div>
</div>
<div className="skill-container active-f active">
<div ref={skillWrap} className="skill-wrap">
{console.log(activeSkills)}
{activeSkills.map((skill, i) => (
<div key={i} className="skill">
{skill}
</div>
))}
</div>
</div>
</div>
);
};
Below is the https://codesandbox.io/ page to see what I've done so far, and the link to the Angular JS implementation which I'm trying to achieve with React.
My React implementation (from the skills route): https://codesandbox.io/s/shy-http-jlrle
Original Angular implementation (skills route): https://codesandbox.io/s/github/eazylaykzy/portfolio-ex
I really would appreciate any effort at helping me get this to work.
Just to abstract the first part as mentioned in my comment, you could write your own hook and use it as so
import React, { useMemo } from 'react';
// How to shuffle an array
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle(array) {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
function useActiveSkills(skills) {
return useMemo(() => shuffle(skills), [skills]);
}
function Component() {
const activeSkills = useActiveSkills();
return (
<>
{activeSkills.map((skill) => (
<p>{skill.name}</p>
))}
</>
);
}

setstate isn't setting value to state

i'm setting my state after calculating it in mouseup and mouse leave event but it's not updating it how to solve in mouseup and mouse leave
Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
onMouseMove = (e) => {
if (!this.isDown) {
return;
}
e.preventDefault();
var x = e.pageX - this.slider.current.offsetLeft;
var walk = x - this.startX;
this.startX = x;
var z = walk;
var finalValue = this.state.left + (z / 3);
finalValue = Math.floor(finalValue * 100) / 100;
this.setState({ left: finalValue }, () => { });
this.setState({ percent: false })
}
onMouseLeave = () => {
this.isDown = false;
var left = this.state.left;
for (let i = 0; i < 6; i++) {
this.el = 306*[i]
console.log(this.el);
if (left<=this.el) {
this.setState({left:this.el},()=>{})
// return
}
console.log(this.state.left);
}
}
onMouseUp = () => {
this.isDown = false;
this.slider.current.style.cursor = 'pointer';
var left = this.state.left;
for (let i = 0; i < 6; i++) {
this.el = 306*[i]
console.log(this.el);
if (left<=this.el) {
this.setState({left:this.el},()=>{})
// return
}
console.log(this.state.left);
}
}
render() {
return (
<div className="slider-wrapper" >
<div onMouseDown={this.onMouseDown}
style={this.state.percent ? this.goLeftPercent() : this.mouseMove()}
onMouseUp={this.onMouseUp} onMouseLeave={this.onMouseLeave}
onMouseMove={this.onMouseMove} ref={this.slider} className="slider-container">
)
}
If I understand correctly you can try the below changes.
Constructor
constructor() {
super();
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
}
Render()
<li onMouseUp={this.onMouseUp} onMouseLeave={this.onMouseLeave} >

Class button timeout

So i have a class that has some functions that are making a carousel work. But pressing the next button fast deletes the previous animation and starts the next so i want to make the button wait for the animation to end. The animation inside css is 0.6 seconds but the timeous i set inside goNext functions is completely ignored when i fast click the button. What am I doing wrong here?
index.js
let carousel = document.getElementById("carousel");
let seats = document.querySelectorAll("ul > li");
if (seats.length === 1)
carousel.style.left = 0;
class SLID {
constructor() {
this.nextDisable = true;
this.changeNextDisable = this.changeNextDisable.bind(this);
}
changeNextDisable() {
this.nextDisable = false;
}
goToNext() {
if(this.nextDisable===false){
this.nextDisable = true;
var el, i, j, new_seat, ref;
el = document.querySelector("ul > li.is-ref");
el.classList.remove('is-ref');
new_seat = el.nextElementSibling || seats[0];
new_seat.classList.add('is-ref');
new_seat.style.order = 1;
for (i = j = 2, ref = seats.length; (2 <= ref ? j <= ref : j >= ref); i = 2 <= ref ? ++j : --j) {
new_seat = new_seat.nextElementSibling || seats[0];
new_seat.style.order = i;
}
carousel.classList.remove('toPrev');
carousel.classList.add('toNext');
carousel.classList.remove('is-set');
return setTimeout(()=> {
this.changeNextDisable();
carousel.classList.add('is-set');
}, 60);
}
goToPrev() {
var el, i, j, new_seat, ref;
el = document.querySelector("ul > li.is-ref");
el.classList.remove('is-ref');
new_seat = el.previousElementSibling || seats[seats.length - 1];
new_seat.classList.add('is-ref');
new_seat.style.order = 1;
for (i = j = 2, ref = seats.length; (2 <= ref ? j <= ref : j >= ref); i = 2 <= ref ? ++j : --j) {
new_seat = new_seat.nextElementSibling || seats[0];
new_seat.style.order = i;
}
carousel.classList.remove('toNext');
carousel.classList.add('toPrev');
carousel.classList.remove('is-set');
return setTimeout((function () {
return carousel.classList.add('is-set');
}), 50);
}
}
}
let s = new SLID();
document.getElementById("nextButton").addEventListener("click", () => {
s.goToNext();
});
document.getElementById("prevButton").addEventListener("click", () => {
s.goToPrev();
});
According to your animation type (transition or animation) you can replace
return setTimeout(()=> {
this.changeNextDisable();
carousel.classList.add('is-set');
}, 60);
(and trying avoid it) to:
carousel.addEventListener("animationend", () => {
this.changeNextDisable();
carousel.classList.add('is-set');
}, {
once: true,
});
or
carousel.addEventListener("transitionend", () => {
this.changeNextDisable();
carousel.classList.add('is-set');
}, {
once: true,
});
same thing for goToPrev method.
You tell us that the CSS animation has a duration of 0.6 seconds, which is 600 milliseconds, not 60 or 50 milliseconds, which is what you set your setTimeout delay to.
So:
return setTimeout(()=> {
this.changeNextDisable();
carousel.classList.add('is-set');
}, 600);
// ^^^^
Having said that, you should look into listening to the transactionend event, as then you don't need to know how long the CSS transition takes.
That would look like this:
const done = () => {
carousel.removeEventListener("transactionend", done);
this.changeNextDisable();
carousel.classList.add('is-set');
}
return carousel.addEventListener("transactionend", done);

React count down error

I have implemented a vanilla js countdown into a react component as follow:
import React, { Component } from 'react';
class CustomCountDown extends Component {
constructor(props) {
super(props);
this.endTime;
this.msLeft;
this.time;
this.hours;
this.mins;
this.element;
}
twoDigits( n ){
return (n <= 9 ? "0" + n : n);
}
updateTimer() {
this.msLeft = this.endTime - (+new Date);
if (this.msLeft < 1000 ) {
element.innerHTML = "countdown's over!";
} else {
this.time = new Date(this.msLeft );
this.hours = this.time.getUTCHours();
this.mins = this.time.getUTCMinutes();
this.element.innerHTML = (this.hours ? this.hours + ':' + this.twoDigits( this.mins ) : this.mins) + ':' + this.twoDigits( this.time.getUTCSeconds() );
setTimeout( this.updateTimer, this.time.getUTCMilliseconds() + 500 );
}
}
countdown( elementName, minutes, seconds ) {
this.element = document.getElementById( elementName );
this.endTime = (+new Date) + 1000 * (60*minutes + seconds) + 500;
this.updateTimer();
}
componentDidMount() {
this.countdown("count", 1, 30);
}
render() {
return(
<div id="count">
</div>
);
}
}
export default CustomCountDown;
I can't figure out why I am getting the following error:
When you pass this.updateTimer to setTimeout you loose context, i.e. this no longer points to your component instance. You need to keep the context either way:
setTimeout( this.updateTimer.bind(this), this.time.getUTCMilliseconds() + 500 );
setTimeout( () => this.updateTimer(), this.time.getUTCMilliseconds() + 500 );
As a better alternative, you can bind updateTimer in the constructor. This won't create new function every time updateTimer is called:
constructor(props) {
// ...
this.updateTimer = this.updateTimer.bind(this);
}

Javascript not able to make common functions for a prototype

I am trying to make timer in javascript using a prototype. Each time a new timer is created, a object of prototype is created. There are methods to increase time and print each second. The whole code snippet is as follows:
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.prototype.start = function() {
var self = this;
if (!self.interval) {
self.interval = setInterval(update, 1000);
}
function update() {
incrementTime();
render();
}
function render() {
self.elem.innerText = getPrintableTime();
}
function incrementTime() {
self.currentTime["min"] += Math.floor((++self.currentTime["sec"]) / 60);
self.currentTime["hr"] += Math.floor(self.currentTime["min"] / 60);
self.currentTime["sec"] = self.currentTime["sec"] % 60;
self.currentTime["min"] = self.currentTime["min"] % 60;
}
function getPrintableTime() {
var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
};
module.exports = Timer;
I have all methods in start function. The problem is that for each new object of Timer, new space for each method will be used which is very inefficient. But when I try to put methods outside of start function, they lose access to self variable. You can see that there is setInterval function used which will be calling these methods per second. I cannot use this also as this will be instance of Window in subsequent calls.
How can I solve this situation by only keeping one instance of all the interior methods?
You don't need to have all methods in the start function. Yes, for each new Timer instance, new space for each function will be used, but that is necessary when you want to work with setInterval as you need a function which closes over the instance. However, you need only one such closure, the other methods can be standard prototype methods.
function getTwoDigitNumber(number) {
return (number > 9 ? "" : "0") + number;
}
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.prototype.start = function() {
var self = this;
if (!this.interval) {
this.interval = setInterval(function update() {
self.incrementTime();
self.render();
}, 1000);
}
};
Timer.prototype.render() {
this.elem.innerText = this.getPrintableTime();
};
Timer.prototype.incrementTime = function() {
this.currentTime.sec += 1;
this.currentTime.min += Math.floor(this.currentTime.sec / 60);
this.currentTime.hr += Math.floor(this.currentTime.min / 60);
this.currentTime.sec = this.currentTime.sec % 60;
this.currentTime.min = this.currentTime.min % 60;
};
Timer.prototype.getPrintableTime = function() {
var text = getTwoDigitNumber(this.currentTime.hr) + ":"
+ getTwoDigitNumber(this.currentTime.min) + ":"
+ getTwoDigitNumber(self.currentTime.sec);
return text;
};
module.exports = Timer;
Btw, regarding your incrementTime pattern, you should have a look at How to create an accurate timer in javascript?.
You can use apply to use functions defined outside of prototype with correct this context.
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
function update() {
incrementTime.apply(this);
render.apply(this);
}
function render() {
this.elem.innerText = getPrintableTime.apply(this);
}
function incrementTime() {
this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
this.currentTime["sec"] = this.currentTime["sec"] % 60;
this.currentTime["min"] = this.currentTime["min"] % 60;
}
function getPrintableTime() {
var text = getTwoDigitNumber(this.currentTime["hr"]) + ":" + getTwoDigitNumber(this.currentTime["min"]) + ":" + getTwoDigitNumber(this.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
Timer.prototype.start = function() {
var self = this;
if (!self.interval) {
self.interval = setInterval(function() {
update.apply(self);
}, 1000);
}
};
document.addEventListener('DOMContentLoaded', function() {
var timer = new Timer(document.getElementById('timer'));
timer.start();
}, false);
<div id="timer"></div>
If I understand correctly, you're wanting to only create one interval.
One possible solution would be to create a static method and variable to manage the setInterval. I would note that while this may be more performance friendly, the timers will always start and run on the same count...not from the moment each timer is created. (See example)
Of course, you could capture the current timestamp and calculate the elapsed time from there. But, that's another thread ;)
function Timer(elem) {
this.interval = null;
this.currentTime = {
sec: 0,
min: 0,
hr: 0
};
this.elem = elem;
};
Timer.subscribe = function(timer) {
Timer.subscribers = Timer.subscribers || [];
if (Timer.subscribers.indexOf(timer) === -1) {
Timer.subscribers.push(timer);
timer.update.call(timer);
}
Timer.checkInterval();
};
Timer.unsubscribe = function(timer) {
Timer.subscribers = Timer.subscribers || [];
if (Timer.subscribers.indexOf(timer) !== -1) {
Timer.subscribers.splice(Timer.subscribers.indexOf(timer), 1);
}
Timer.checkInterval();
};
Timer.checkInterval = function() {
if (!Timer.interval && Timer.subscribers.length > 0) {
Timer.interval = setInterval(function() {
Timer.subscribers.forEach(function(item) {
item.update.call(item);
});
}, 1000);
} else if (Timer.interval && Timer.subscribers.length === 0) {
clearInterval(Timer.interval);
Timer.interval = null;
}
};
Timer.prototype = {
start: function() {
Timer.subscribe(this);
},
stop: function() {
Timer.unsubscribe(this);
},
update: function() {
this.incrementTime();
this.render();
},
incrementTime: function() {
this.currentTime["min"] += Math.floor((++this.currentTime["sec"]) / 60);
this.currentTime["hr"] += Math.floor(this.currentTime["min"] / 60);
this.currentTime["sec"] = this.currentTime["sec"] % 60;
this.currentTime["min"] = this.currentTime["min"] % 60;
},
render: function() {
var self = this;
function getPrintableTime() {
var text = getTwoDigitNumber(self.currentTime["hr"]) + ":" + getTwoDigitNumber(self.currentTime["min"]) + ":" + getTwoDigitNumber(self.currentTime["sec"]);
return text;
}
function getTwoDigitNumber(number) {
if (number > 9) {
return "" + number;
} else {
return "0" + number;
}
}
this.elem.innerText = getPrintableTime();
}
};
/**
*
*/
var timers = document.getElementById('timers');
function addTimer() {
var el = document.createElement('div');
var tmr = document.createElement('span');
var btn = document.createElement('button');
var t = new Timer(tmr);
btn.innerText = 'Stop';
btn.onclick = function() {
t.stop();
};
el.appendChild(tmr);
el.appendChild(btn);
timers.appendChild(el);
t.start();
};
<div id="timers"></div>
<button onclick="addTimer()">Add Timer</button>

Categories

Resources