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();
}
}