@allensulzen/countdown-timer
Version:
A Web Component that displays a timer counting down to a specified date.
314 lines (274 loc) • 10.1 kB
JavaScript
/**
* Countdown Timer Web Component
*
* Props
* - date: The ending date of the countdown timer. Format: YYYY-MM-DDTHH:MM:SS
* - heading: The heading of the countdown timer
* - subheading: The subheading of the countdown timer
* - theme: The theme of the countdown timer. Options: light, dark, (or optionally add your own)
* - message: The message to display when the countdown timer is complete
* - link: The link to display when the countdown timer is complete
* - linktext: The text to display for the link when the countdown timer is complete
* Events
* - countdownComplete: Dispatched when the countdown timer is complete
* - updateTimer: Dispatched every second when the countdown timer is running
* - startTimer: Dispatched when the countdown timer starts
* - stopTimer: Dispatched when the countdown timer stops
*/
class CountdownTimer extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return ['date', 'heading', 'subheading', 'theme', 'message', 'link', 'linktext'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.render();
}
}
connectedCallback() {
if (!this.hasAttribute('message')) {
this.setAttribute('message', 'Countdown Complete!');
}
if (!this.hasAttribute('linktext')) {
this.setAttribute('linktext', 'Learn More');
}
if (!this.hasAttribute('theme')) {
this.setAttribute('theme', 'light');
}
this.render();
this.startTimer();
}
disconnectedCallback() {
this.stopTimer();
}
get styles() {
return`
<style>
.countdown-timer__title {
text-align: center;
font-family: inherit;
}
.countdown-timer__content {
display: flex;
flex-direction: row;
gap: 0.5rem;
justify-content: center;
align-items: center;
font-family: inherit;
min-height: 100px;
}
.countdown-timer__content .figure {
font-size: 28px;
font-weight: bold;
font-family: inherit;
}
.countdown-timer__content__counter.skeleton {
animation: pulse 3s ease-in-out infinite;
}
.countdown-timer__content__counter {
position: relative;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
padding: 1rem;
min-width: 50px;
text-align: center;
padding-bottom: 1.5rem;
font-family: inherit;
}
.countdown-timer__content__counter .label {
position: absolute;
left: 0;
right: 0;
margin: auto;
bottom: 0.5rem;
font-size: 12px;
text-align: center;
font-family: inherit;
}
.countdown-timer__label {
text-align: center;
margin-top: 1rem;
font-family: inherit;
}
.countdown-timer__label a {
color: inherit;
}
.countdown-timer.theme--dark .countdown-timer__content__counter {
background-color: #000;
color: #fff;
border-color: #ccc;
}
.countdown-timer.theme--dark .countdown-timer__title {
color: white;
}
.countdown-timer.theme--dark .countdown-timer__label {
color: white;
}
only screen and (max-width: 386px) {
.countdown-timer__content__counter {
min-width: 40px;
}
.countdown-timer__content__counter .figure {
font-size: 16px;
}
}
only screen and (max-width: 346px) {
.countdown-timer__content__counter {
min-width: 23px;
}
}
pulse {
0%, 100% { opacity: 0.8; }
25% { opacity: 0.3; }
50% { opacity: 0.8; }
75% { opacity: 0.3; }
}
</style>
`;
}
get loadingState() {
if (this.days === undefined) {
return `
<div class="countdown-timer__content__counter skeleton">
<span class="figure">--</span>
<span class="label">days</span>
</div>
<div class="countdown-timer__content__counter skeleton">
<span class="figure">--</span>
<span class="label">hours</span>
</div>
<div class="countdown-timer__content__counter skeleton">
<span class="figure">--</span>
<span class="label">minutes</span>
</div>
<div class="countdown-timer__content__counter skeleton">
<span class="figure">--</span>
<span class="label">seconds</span>
</div>
`;
}
return '';
}
get headingTemplate() {
if (this.heading) {
return`
<div class="countdown-timer__title">
<h2>${this.heading}</h2>
</div>
`;
}
return '';
}
get subheadingTemplate() {
if (this.subheading) {
return`
<div class="countdown-timer__label">
<span>${this.subheading}</span>
</div>
`;
}
return '';
}
get completeTemplate() {
return `
<div class="countdown-timer theme--${this.theme}">
<div class="countdown-timer__title">
<h2>${this.message}</h2>
${this.link ? `
<div class="countdown-timer__label">
<a href="${this.link}">${this.linktext}</a>
</div>
` : ''}
</div>
</div>
`;
}
counter(timeProp) {
return `
<div class="countdown-timer__content__counter ${timeProp}">
<span class="figure">${this[timeProp]}</span>
<span class="label">${this[timeProp] !== 1 ? timeProp : timeProp.replace(/s$/, '')}</span>
</div>
`;
}
generateContent() {
return `
<div class="countdown-timer theme--${this.theme}">
${this.headingTemplate}
<div class="countdown-timer__content">
${this.loadingState}
${this.days > 0 ? this.counter('days'): ''}
${this.days > 0 || this.hours > 0 ? this.counter('hours') : ''}
${this.days > 0 || this.hours > 0 || this.minutes > 0 ? this.counter('minutes') : ''}
${this.seconds > -1 ? this.counter('seconds') : ''}
</div>
${this.subheadingTemplate}
</div>
`;
}
updateTime() {
if (!!this.date && new Date(this.date) == 'Invalid Date') {
this.stopTimer();
throw Error('Invalid date format. Please use the format: YYYY-MM-DDTHH:MM:SS');
}
this.endTime = new Date(this.date);
this.currentTime = new Date();
this.timeRemaining = this.endTime - this.currentTime;
this.days = Math.floor(this.timeRemaining / (1000 * 60 * 60 * 24));
this.hours = Math.floor((this.timeRemaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
this.minutes = Math.floor((this.timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
this.seconds = Math.floor((this.timeRemaining % (1000 * 60)) / 1000);
}
renderTime() {
if (this.timeRemaining <= 0) {
this.stopTimer();
this.querySelector('.ct__container').innerHTML = this.completeTemplate;
this.dispatchEvent(new CustomEvent('countdownComplete', { bubbles: true }));
return;
}
this.querySelector('.ct__container').innerHTML = this.generateContent();
}
stopTimer() {
clearInterval(this.timerInterval);
this.dispatchEvent(new CustomEvent('stopTimer', {
detail: {
days: this.days,
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds
},
bubbles: true
}));
}
startTimer() {
this.updateTime();
this.dispatchEvent(new CustomEvent('startTimer', { bubbles: true }));
this.timerInterval = setInterval(() => {
this.updateTime();
this.renderTime();
this.dispatchEvent(new CustomEvent('updateTimer', {
detail: {
days: this.days,
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds
},
bubbles: true
}));
}, 1000);
}
render() {
this.innerHTML = `
${this.styles}
<div class="ct__container">
${this.generateContent()}
</div>
`;
this.renderTime();
}
}
customElements.define('countdown-timer', CountdownTimer);