How to start a timer without page refresh (Rails/JavaScript)? - javascript

I have a model named 'Deal' which has start_at and end_at attributes. I have implemented a countdown timer using hotwire/stimulus JS.
When the deal starts (start date is in the past, end date is in the future), the countdown timer displaying time left to deal will be shown. e.g Time left to deal: 2 hours, 4 minutes, 30 seconds and so on. It will decrement by 1 second.
If the deal has not yet started (start date is in the future), the page will show "Deal is going to start on #{datetime}".
However, the user needs to refresh the page they are currently on to see a timer if the deal has started in the meantime (i.e. transitioning from "Deal is going to start on #{datetime}" to a countdown timer). I am wondering what's the best way to start the timer without refreshing the page. Thanks.

The way to manage a 'timer' that runs some function every X milliseconds is via the browser's setInterval function.
This function can be used like this - const intervalID = setInterval(myCallback, 500); - where myCallback is the function that will attempt to run every 500ms.
The timer can be 'cancelled' by calling clearInterval and giving it the interval ID that is created as the result of setInterval.
Example HTML
Here we have a basic HTMl structure where we set our controller timer and set the from/to times along with targets that hold the messages based on three states.
These three states are 'before', 'during' (when the current time is between the two times) and 'after'.
<section class="prose m-5">
<div
data-controller="timer"
data-timer-from-value="2022-03-08T10:41:32.111Z"
data-timer-to-value="2022-03-09T11:10:32.111Z"
>
<div style="display: none" data-timer-target="before">
Deal will start on <time data-timer-target="fromTime"></time>
</div>
<div style="display: none" data-timer-target="during">
Deal is active <time data-timer-target="toTimeRelative"></time>
</div>
<div style="display: none" data-timer-target="after">
Deal ended on <time data-timer-target="toTime"></time>
</div>
</div>
</section>
Example Stimulus Controller
This timerController accepts the to and from times as strings (ISO strings are best to use, and remember the nuances of time-zones can be complex).
When the controller connects we do three things; 1. set up a timer to run this.update every X milliseconds and put the timer ID on the class for clearing later as this._timer. 2. Set the time values (the inner time labels for messaging). 3. Run the this.update method the initial time.
this.getTimeData parses the from/to datetime strings and does some basic validation, it also returns these date objects along with a status string which will be one of BEFORE/DURING/AFTER.
this.update - this shows/hides the relevant message parts based on the resolved status.
import { Controller } from '#hotwired/stimulus';
const BEFORE = 'BEFORE';
const DURING = 'DURING';
const AFTER = 'AFTER';
export default class extends Controller {
static values = {
interval: { default: 500, type: Number },
locale: { default: 'en-GB', type: String },
from: String,
to: String,
};
static targets = [
'before',
'during',
'after',
'fromTime',
'toTime',
'toTimeRelative',
];
connect() {
this._timer = setInterval(() => {
this.update();
}, this.intervalValue);
this.setTimeValues();
this.update();
}
getTimeData() {
const from = this.hasFromValue && new Date(this.fromValue);
const to = this.hasToValue && new Date(this.toValue);
if (!from || !to) return;
if (from > to) {
throw new Error('From time must be after to time.');
}
const now = new Date();
const status = (() => {
if (now < from) return BEFORE;
if (now >= from && now <= to) return DURING;
return AFTER;
})();
return { from, to, now, status };
}
setTimeValues() {
const { from, to, now } = this.getTimeData();
const locale = this.localeValue;
const formatter = new Intl.DateTimeFormat(locale, {
dateStyle: 'short',
timeStyle: 'short',
});
this.fromTimeTargets.forEach((element) => {
element.setAttribute('datetime', from);
element.innerText = formatter.format(from);
});
this.toTimeTargets.forEach((element) => {
element.setAttribute('datetime', to);
element.innerText = formatter.format(to);
});
const relativeFormatter = new Intl.RelativeTimeFormat(locale, {
numeric: 'auto',
});
this.toTimeRelativeTargets.forEach((element) => {
element.setAttribute('datetime', to);
element.innerText = relativeFormatter.format(
Math.round((to - now) / 1000),
'seconds'
);
});
}
update() {
const { status } = this.getTimeData();
[
[BEFORE, this.beforeTarget],
[DURING, this.duringTarget],
[AFTER, this.afterTarget],
].forEach(([key, element]) => {
if (key === status) {
element.style.removeProperty('display');
} else {
element.style.setProperty('display', 'none');
}
});
this.setTimeValues();
if (status === AFTER) {
this.stopTimer();
}
}
stopTimer() {
const timer = this._timer;
if (!timer) return;
clearInterval(timer);
}
disconnect() {
// ensure we clean up so the timer is not running if the element gets removed
this.stopTimer();
}
}

Related

Is it possible to keep setInterval on multiple devices 'synced' by compensating for drift, or do I need server synchronisation?

I am building a React Native app where my entire back end is provided for by services like Firebase etc.
The app requires clocks on multiple devices to start and end at the same time which can run for up to an hour.
Given a shared starting point in time between devices I have observed drift in the accuracy of setInterval in this 20 seconds of data:
I am attempting to compensate for this deviation in clock timing by measuring it and then compensating for it - here is a code sandbox with my solution.
useTimer hook:
import { useState, useEffect, useRef } from "react";
import moment from "moment";
export const convertMsToMinsAndSecs = (countDown) => {
const seconds = moment
.duration(countDown)
.seconds()
.toString()
.padStart(2, "0");
const minutes = moment
.duration(countDown)
.minutes()
.toString()
.padStart(2, "0");
const minsAndSecs = `${minutes.toString()}:${seconds.toString()}`;
return countDown > 0 ? minsAndSecs : "00:00";
};
const roundTimeStamp = (timeStamp) =>
timeStamp === 0 ? 0 : timeStamp + (1000 - (timeStamp % 1000));
export const useTimer = (
started,
startTime,
length,
resetClock,
clockIntialState
) => {
const initialTimerState = {
start: 0,
end: 0,
timeNow: 0,
remaining: length,
clock: convertMsToMinsAndSecs(length),
internalClockDeviation: 0
};
const [timeData, setTimeData] = useState(initialTimerState);
const intervalId = useRef(null);
const deviation = useRef(null);
useEffect(() => {
setTimeData((prevState) => ({
...prevState,
start: roundTimeStamp(startTime),
end: roundTimeStamp(startTime) + length
}));
if (started) {
intervalId.current = setInterval(() => {
const intervalTime = moment().valueOf();
setTimeData((prevState) => {
return {
...prevState,
timeNow: intervalTime,
remaining: prevState.remaining - 1000,
clock: convertMsToMinsAndSecs(prevState.remaining - 1000),
internalClockDeviation:
prevState.timeNow === 0
? 0
: intervalTime - prevState.timeNow - 1000
};
});
}, 1000 - deviation.current);
}
}, [started]);
useEffect(() => {
deviation.current = timeData.internalClockDeviation;
}, [timeData.internalClockDeviation]);
if (timeData.remaining <= 0 && started) {
resetClock(clockIntialState);
clearTimeout(intervalId.current);
setTimeData(initialTimerState);
}
const compensatedLength = 1000 - deviation.current;
return {
timeData,
intervalId,
compensatedLength,
setTimeData,
initialTimerState
};
};
As I am not running my own server application I would prefer to handle this on the client side if possible. It also means that I do not need to rely on network connections or the availability of a timing server.
Will my approach work across multiple devices, and if so can it be improved, or do I need to build a server side application to effectively handle this? TIA.
When you determine time diff you can not rely on intervals being accurate. Gets worse when tab is in background/not in focus.
Typically you rely on timestamps to get the offset in time, you do not subtract a fix number.
function countDown(totalTime, onComplete, onUpdate, delay = 1000) {
let timer;
const startTime = new Date().getTime();
function next() {
const runningTime = new Date().getTime() - startTime;
let remaining = Math.max(totalTime - runningTime, 0);
onUpdate && onUpdate(remaining);
!remaining && onComplete && onComplete();
var ms = Math.min(delay, remaining);
timer = remaining && window.setTimeout(next, ms);
}
next()
return function () {
timer && window.clearTimeout(timer);
}
}
countDown(5000, function(){ console.log('done1'); }, function(x){ console.log('update1 ', x); });
const out = document.getElementById("out");
const cd = countDown(
60000,
function(){ out.textContent = 'done'; },
function(x){ out.textContent = (x/1000).toFixed(3); },
20
);
document.getElementById("btn").addEventListener('click', cd);
<div id="out"></div>
<button id="btn">stop</button>
This will fail if user changes clock, not much you can do on that. You could ping the server for time, but that also has latency with how long the call takes.
The only way you can synchronize all the clocks together is having all of them observe one single source of truth.
If you create the timestamp on one client. You need to make sure all the other clients are in sync with that data.
What you can do is have a server + client architecture where the server is the single point of truth. But if you are trying to completely synchronize without a single point of truth, you are doomed to fail because of the problems that you can not control like latency of communication between all the client applications in your case clock.

count up timer that record the time spent on the page

i have a code that count up the time spent on the page it strat count up since it clicked now my problem is how to record the time when click on submit button and send it to the database
< script >
var minutesLabel = document.getElementById("minutes");
var secondsLabel = document.getElementById("seconds");
var totalSeconds = 0;
setInterval(setTime, 1000);
function setTime() {
++totalSeconds;
secondsLabel.innerHTML = pad(totalSeconds % 60);
minutesLabel.innerHTML = pad(parseInt(totalSeconds / 60));
}
function pad(val) {
var valString = val + "";
if (valString.length < 2) {
return "0" + valString;
} else {
return valString;
}
}
</script>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<label id="minutes">00</label>:<label id="seconds">00</label><label id="houres">:00</label><li>
</ul>
</div>
<input type="submit" name="register" class="btn btn-info" value="Submit" />
Try breaking down the question. Start of by analysing your program flow:
Currently, every second (1000 ms):
the setTime function is called (update)
which increments a variable totalSeconds ((data) model / state)
transforms it into minutes and seconds
formats the values into padded strings using pad (view model)
updates the view (Document Object Model, DOM, HTML) with the new values (render the view)
Now, you want to send some data to the database... lets break it down:
starting in the client (eg. a webpage)
user clicks on submit button (interaction in view triggers action/event)
(before this, a handler/listener for that event should have been registered)
the handler function that was registered as a listener for the event is invoked
in the handler, the data is formatted (in a way the server likes it, eg. as JSON, or plain text in the query part of the url)
the data is sent to the server (eg. as an http request using AJAX through XMLHTTPRequest through the new fetch api)
the server receives the request, and passes it to a "handler"
in PHP, this is can be done using ".htaccess" (optional) to route the request to a PHP file, in which some variables containing information about the request are provided/pre-set
see https://www.php.net/manual/en/reserved.variables.php
for json decode, see: PHP decode JSON POST
for simple query variables, see https://www.php.net/manual/en/reserved.variables.get.php
the "handler" can then process the request data (eg. validate it and save it in a database), and return a response
the client then receives the response and can ignore or act on it, eg. by showing an error/success message
The above-mentioned flow is quite common in the world of web development, more generally:
initial state is loaded
when state is loaded or updated:
the view model is updated (based on the state)
the view is rendered (based on the view model)
when an event is triggered (eg. user interaction, or a timer):
a handler is invoked, that
updates the state (and thus the view)
and, optionally, sends some data to the server
Following this pattern, the code could be rewritten:
<span class="time">
<span class="mm"></span>:<span class="ss"></span>
</span>
<button class="submit-time">Save time</button>
<script>
const view = {
mm: document.querySelector('.time .mm'),
ss: document.querySelector('.time .ss'),
submit: document.querySelector('.submit-time'),
}
const render = ()=> {
const viewModel = viewModelFromState(state)
view.mm.innerText = viewModel.mm
view.ss.innerText = viewModel.ss
}
const viewModelFromState = state=> ({
mm: pad(Math.floor(state.secondsTotal / 60)),
ss: pad(state.secondsTotal % 60),
})
const state = {
secondsTotal: 0,
}
const update = ()=> {
state.secondsTotal += 1
}
const eventListeners = {
submit: ()=> {
alert('value is '+state.secondsTotal)
const onSuccess = (text)=> {
alert("Saved! "+text)
}
const onFail = (error)=> {
alert("Couldn't save!\n" + error)
}
saveSecondsTotal(state.secondsTotal)
.then(res=> res.text()).then(onSuccess)
.catch(err=> onFail(err))
},
}
const setupEventListeners = ()=> {
view.submit.addEventListener('click', eventListeners.submit)
}
// api
const saveSecondsTotal = (secondsTotal)=> {
const val = ''+secondsTotal // explicitly convert number to string (unnecessary)
const url = '/save-seconds-total'
const queryString = 'val='+val
const fullUrl = url+'?'+queryString
return fetch(fullUrl)
}
// helpers
const pad = (val)=> {
const str = ''+val
return str.length < 2? '0' + str: str
}
// start
setupEventListeners()
setInterval(()=> {
update()
render()
}, 1000)
</script>
I "simplified" the example by removing unused (eg. hour and some markup). I'm also using some language features like arrow functions, objects, const, ternary operator, promises, etc, that might be nice to look up.
Though, for this example, a "simpler" and explicit solution could be:
<span>
<span id="mm"></span>:<span id="ss"></span>
</span>
<button onclick="submitTime">Save time</button>
<script>
// state + render
var mmElement = document.getElementById('mm')
var ssElement = document.getElementById('ss')
var secondsTotal = 0
function update () {
secondsTotal += 1
var mm = pad(Math.floor(secondsTotal / 60))
var ss = pad(secondsTotal % 60)
mmElement.innerText = mm
ssElement.innerText = ss
}
// submit
function onSubmitSuccess (text) {
alert("Saved! "+text)
}
function onSubmitFail (error) {
alert("Couldn't save!\n" + error)
}
function saveSecondsTotal (secondsTotal) {
return fetch('/save-seconds-total?val='+secondsTotal)
.then(function (response) {return response.text()})
}
function submitTime () {
alert('value is '+secondsTotal)
saveSecondsTotal(state.secondsTotal)
.then(onSuccess)
.catch(onFail)
}
// helpers
function pad (val) {
var str = ''+val
if (str.length < 2) {
return '0' + str
}
return str
}
// start
setInterval(update, 1000)
</script>
The interesting part here, to answer your question, is the "submit" section. The PHP side would look something like:
<?php // in http-root/save-seconds-total.php
$val = $_GET["val"];
$secondsTotal = intval($val);
// ... database specific code ...
echo "Saved ".$secondsTotal." seconds!"
?>
I would recommend you to read up and work through more examples/tutorials. When asking questions here, please show what you've tried, and possibly some hypotheses. Try to strip away all unnecessary details to get to the core of the question. Hopefully you're asking because your interested in understanding, and not because you want some homework done for you...

removing cookies using javascript is not done immediatly

I am currently working with a javascript that is supposed to remove some unwanted cookies, but for some reason aren't they removed when told to?..
only after certain amount of times trying to remove them, they seem to be removed.. some sort of delayed effect?
here is the code:
const name = 'test_u';
const name1 = 'test_te_s';
function eraseCookie(name) {
document.cookie = name+'=; Max-Age=-99999999;';
}
function removeCookies(cookieA, cookieB) {
setInterval(function() {
if (document.cookie.includes(cookieA) || document.cookie.includes(cookieB))
{
eraseCookie(cookieA);
eraseCookie(cookieB);
var date = new Date();
var timestamp = date.getTime();
console.log(timestamp)
}
},10000);
}
removeCookies(name, name1);
example from console log output:
1555420706478
1555420716477
1555420726487
1555420736487
1555420746497
1555420756487
It runs 6 times before its removed? but why though?
why aren't they removed immediately?
Because you have setInterval which means that that code will be run after some time that you provide, and keep repeating it by that interval. So just remove that setInterval:
function removeCookies(cookieA, cookieB) {
if (document.cookie.includes(cookieA) || document.cookie.includes(cookieB)) {
eraseCookie(cookieA);
eraseCookie(cookieB);
var date = new Date();
var timestamp = date.getTime();
console.log(timestamp)
}
}
And if you want to keep repeating it try this one:
removeCookies(name, name1);
setInterval(() => {
removeCookies(name, name1);
}, 10000);
or
function removeCookies(cookieA, cookieB) {
if (document.cookie.includes(cookieA) || document.cookie.includes(cookieB)) {
eraseCookie(cookieA);
eraseCookie(cookieB);
var date = new Date();
var timestamp = date.getTime();
console.log(timestamp)
}
setInterval(() => {
removeCookies()
}, 10000);
}
removeCookies(name, name1);
so it will first call removeCookies, and then it will keep repeating.

javascript || Angular2/6: Calling setInterval multiple times but last timerId is not stopping even though clearInterval called

Requirement:
User scans multiple job numbers, for each job number , I need to
call one API and get the total job details and show it in a table
below the scanned text box.
User don't want to wait until API call finishes. He will scan continuously irrespective of details came or not.
What I have done:
I have taken one variable jobNumberList which stores all the job numbers the user scanned
I am continuously calling the API, with those job numbers.
When API, gives response , then I am adding to the table
I created one method called m1()
m1() =>
this method will compare the jobNumberList with the Table Rows data.
if any item not found in the table rows data when compare to jobNumberList data, then those are still loading jobs ( means API , not given response to those jobs )
I am using setInterval() to call m1() (time inteval = 1000 )
When jobNumberList and the Table rows data matches then I am cancelling the interval
Issue:
Even though I am stopping the polling, still the interval is executing. What will be the issue?
*
The issue is intermittent (not continuous). When I scan job numbers
fast, then its coming. Normal speed not coming.
*
My Code is as follows
// START INTERVAL
startPolling() {
var self = this;
this.isPollingNow = true;
this.poller = setInterval(() => {
let jobsWhichAreScannedButNotLoadedInsideTableStill = self.m1();
if (self.isPollingNow && jobsWhichAreScannedButNotLoadedInsideTableStill.length === 0) {
self.stopPolling();
self.isPollingNow = false;
}
}, 1000);
}
//STOP INTERVAL
stopPolling() {
var self = this;
setTimeout(function () {
window.clearInterval(self.poller); // OR //clearInterval(self.poller) //
}, 0);
}
REQUIREMENT
DEBUGGING IMAGES
The following code will create and auto cancellable interval when there is not more jobs to scan, this principle will do the trick that you need, please ask if you have any doubt.
Use the Clear Jobs button to remove jobs into the array that keep all the existing jobs
var globalsJobs = [1,2,3,4];
function scannableJobs () {
return globalsJobs;
}
function generateAutoCancellableInterval(scannableJobs, intervalTime) {
var timer;
var id = Date.now();
timer = setInterval(() => {
console.log("interval with id [" + id + "] is executing");
var thereUnresponseJobs = scannableJobs();
if (thereUnresponseJobs.length === 0) {
clearInterval(timer);
}
}, intervalTime)
}
const btn = document.getElementById('removejobs');
const infobox = document.getElementById('infobox');
infobox.innerHTML = globalsJobs.length + ' jobs remainings'
btn.addEventListener('click', function() {
globalsJobs.pop();
infobox.innerHTML = globalsJobs.length + ' jobs remainings'
}, false);
setTimeout(() => generateAutoCancellableInterval(scannableJobs, 1000), 0);
setTimeout(() => generateAutoCancellableInterval(scannableJobs, 1000), 10);
<button id="removejobs">
clear jobs
</button>
<div id="infobox">
</div>
</div>

how can I add the ticking clock to my div in an existing code?

It's actually a follow up to this question I want to display elements from json based on their time and duration and interval is interupted by settimeout - I accepted the answer there made by #Daniel Flint - his code is quite clear and can be found here http://jsfiddle.net/nauzilus/rqctam5r/
However, there's one more thing that I wanted to add - a simple div <div id="time"></div> that would contain a new date time object initialized during opening the page and then it being incremented every second just to show the current time constantly. I thought about writing there a javascript:
var actualTime = new Date(substractedDate); // taken from the server
function updateTimeBasedOnServer(timestamp) {
var calculatedTime = moment(timestamp);
var dateString = calculatedTime.format('h:mm:ss A');
$('#time').html(dateString + ", ");
};
var timestamp = actualTime.getTime();
updateTimeBasedOnServer(timestamp);
setInterval(function () {
timestamp += 1000; // Increment the timestamp at every call.
updateTimeBasedOnServer(timestamp);
}, 1000);
(I provide the time of the server as a timestamp there).
I just noticed that there is a slight mismatch between displaying the time in my div and between the text appearing on the screen, possibly because I increment both of the values in two different places.
So my question is - how can I "merge" #Daniel Flint's code with mine and increment both values only in one place?
One thing that jumps out here:
timestamp += 1000;
setTimeout/setInterval aren't guaranteed to run at precisely the delay you've entered. Run this in your browsers console:
var last = Date.now(),
time = function() {
var now = Date.now();
console.log(now - last);
last = now;
},
id = setInterval(time, 1000);
On my Mac at home (Chrome/FireFox) it was anywhere from 990 to 1015. Windows machine at work is a bit better (995-1002), but IE was getting up to 1020. It's not a huge difference, but it's not nothing.
So code needs to be able to handle not running exactly every 1000ms. That's why I was running the timer at 500ms intervals, and checking if the start time was less-than-equal to the current time.
I've rejigged the demo to show the time and message in sync:
(function() {
var messages = [];
var time = document.getElementById("current-time");
var display = document.getElementById("message");
function check() {
showMessage(currentMessage());
showTime();
}
function currentMessage() {
var message = null;
if (messages.length) {
var now = toTheSecond(new Date());
var start = toTheSecond(new Date(messages[0].start_time));
var end = toTheSecond(new Date(start.getTime() + ( messages[0].text_duration * 1000 )));
if (start <= now) {
if (end <= now) {
// done with the current message...
messages = messages.slice(1);
// ...but check if there's another one ready to go right now
message = currentMessage();
}
else {
message = messages[0];
}
}
}
return message;
}
function toTheSecond(date) {
date.setMilliseconds(0);
return date;
}
function showMessage(message) {
if (message) {
display.textContent = message.text_content;
}
else {
display.textContent = "no messages";
}
}
function showTime() {
time.textContent = new Date().toLocaleString()
}
function getMessages() {
setTimeout(function() {
var now = new Date();
messages.push(
{"text_content":"aaaaa","text_duration":5,"start_time": new Date(now.getTime() + 3000).toISOString()},
{"text_content":"aawwaaaaa","text_duration":5,"start_time": new Date(now.getTime() + 10000).toISOString()},
{"text_content":"bbaawwaaaaa","text_duration":5,"start_time": new Date(now.getTime() + 15000).toISOString()}
);
}, 1000);
}
setInterval(check, 500);
getMessages();
})();
<span id="current-time"></span> <span id="message">Hello there!</span>
(Putting the code here as well because I recall SO want code in the answers so it's controlled, but there's a demo here: http://jsfiddle.net/nauzilus/ymp0615g/).
This probably isn't as efficient as it could be; the message text is being set every iteration which might cause repaints/reflow. But then again setting the time stamp is going to do that anyway, so meh :)

Categories