Skip to content

Instantly share code, notes, and snippets.

@krhoyt
Last active November 14, 2020 20:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krhoyt/4fdb6feb68c65ba0c2bcc8c8d8790e72 to your computer and use it in GitHub Desktop.
Save krhoyt/4fdb6feb68c65ba0c2bcc8c8d8790e72 to your computer and use it in GitHub Desktop.
Countdown clock component based on work of Chris Bongers. https://dev.to/dailydevtips1/vanilla-javascript-countdown-clock-49h7
export default class CountdownClock extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
align-items: center;
display: flex;
flex-direction: column;
font-family: 'Roboto', sans-serif;
justify-content: center;
text-align: center;
}
h1 {
color: var( --heading-color, #000000 );
cursor: default;
font-size: 3rem;
margin-top: 0;
}
p {
cursor: default;
}
p:first-of-type {
color: var( --label-color, #000000 );
}
p:last-of-type {
color: var( --done-color, #000000 );
display: none;
}
ul {
display: flex;
list-style: none;
margin-top: 2rem;
padding: 0;
}
ul li {
background: var( --digit-background, #7c77b9 );
border-radius: 10px;
color: var( --digit-color, #8fbfe0 );
cursor: default;
display: flex;
flex-direction: column;
margin: 0 0.50rem 0 0.50rem;
padding: 1rem;
}
ul li span {
font-size: 2rem;
}
:host( [capitalize] ) h1 {
text-transform: capitalize;
}
:host( [capitalize] ) p:first-of-type {
text-transform: capitalize;
}
:host( [capitalize] ) ul li {
text-transform: capitalize;
}
:host( [shadow] ) ul li {
box-shadow: 0px 0px 5px rgba( 0, 0, 0, 0.50 );
}
</style>
<h1></h1> <!-- Heading -->
<p></p> <!-- Label -->
<p></p> <!-- Done -->
<ul>
<li><span id="days">0</span> days</li>
<li><span id="hours">0</span> hours</li>
<li><span id="minutes">0</span> minutes</li>
<li><span id="seconds">0</span> seconds</li>
</ul>
`;
// Properties
// Private-ish
this._end = null;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
// Elements
this.$heading = this.shadowRoot.querySelector( 'h1' );
this.$label = this.shadowRoot.querySelector( 'p:first-of-type' );
this.$done = this.shadowRoot.querySelector( 'p:last-of-type' );
this.$digits = this.shadowRoot.querySelectorAll( 'ul li' );
this.$days = this.shadowRoot.querySelector( '#days' );
this.$hours = this.shadowRoot.querySelector( '#hours' );
this.$minutes = this.shadowRoot.querySelector( '#minutes' );
this.$seconds = this.shadowRoot.querySelector( '#seconds' );
// Initialize end date
// Fix time to zeroes
this._end = new Date();
this._end.setHours( 0 );
this._end.setMinutes( 0 );
this._end.setSeconds( 0 );
this._end.setMilliseconds( 0 );
// Start the countdown
window.requestAnimationFrame( this.tick.bind( this ) );
}
// Counting down
// Figure date difference
// Display as necessary
tick( timestamp ) {
// Difference
const difference = this._end.getTime() - Date.now();
if( difference < 0 ) {
// Done
// Display message
this.$done.style.display = 'block';
} else {
// Still counting
// Populate timing
this.$days.innerText = Math.floor( difference / CountdownClock.DAYS );
this.$hours.innerText = Math.floor( ( difference % CountdownClock.DAYS ) / CountdownClock.HOURS );
this.$minutes.innerText = Math.floor( ( difference % CountdownClock.HOURS ) / CountdownClock.MINUTES );
this.$seconds.innerText = Math.floor( ( difference % CountdownClock.MINUTES ) / CountdownClock.SECONDS );
// Moar animationz!
window.requestAnimationFrame( this.tick.bind( this ) );
}
}
// When things change
_render() {
// Populate labels
this.$heading.innerText = this.heading === null ? '' : this.heading;
this.$label.innerText = this.label === null ? '' : this.label;
this.$done.innerText = this.done === null ? '' : this.done;
// Set any provided details
if( this.year !== null ) this._end.setFullYear( this.year );
if( this.month !== null ) this._end.setMonth( this.month - 1 );
if( this.day !== null ) this._end.setDate( this.day );
}
// Properties set before module loaded
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Default render
// No attributes set
connectedCallback() {
// Check data property before render
// May be assigned before module is loaded
this._upgrade( 'capitalize' );
this._upgrade( 'day' );
this._upgrade( 'done' );
this._upgrade( 'heading' );
this._upgrade( 'label' );
this._upgrade( 'month' );
this._upgrade( 'shadow' );
this._upgrade( 'year' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'capitalize',
'day',
'done',
'heading',
'label',
'month',
'shadow',
'year'
];
}
// Observed tag attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Arbitrary storage
// For your convenience
// Not used in component
get data() {
return this._data;
}
set data( value ) {
this._data = value;
}
// Reflect attributes
// Return typed value (Number, Boolean, String, null)
get capitalize() {
return this.hasAttribute( 'capitalize' );
}
set capitalize( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'capitalize' );
} else {
this.setAttribute( 'capitalize', '' );
}
} else {
this.removeAttribute( 'capitalize' );
}
}
get day() {
if( this.hasAttribute( 'day' ) ) {
return parseInt( this.getAttribute( 'day' ) );
}
return null;
}
set day( value ) {
if( value !== null ) {
this.setAttribute( 'day', value );
} else {
this.removeAttribute( 'day' );
}
}
get done() {
if( this.hasAttribute( 'done' ) ) {
return this.getAttribute( 'done' );
}
return null;
}
set done( value ) {
if( value !== null ) {
this.setAttribute( 'done', value );
} else {
this.removeAttribute( 'done' );
}
}
get heading() {
if( this.hasAttribute( 'heading' ) ) {
return this.getAttribute( 'heading' );
}
return null;
}
set heading( value ) {
if( value !== null ) {
this.setAttribute( 'heading', value );
} else {
this.removeAttribute( 'heading' );
}
}
get label() {
if( this.hasAttribute( 'label' ) ) {
return this.getAttribute( 'label' );
}
return null;
}
set label( value ) {
if( value !== null ) {
this.setAttribute( 'label', value );
} else {
this.removeAttribute( 'label' );
}
}
get month() {
if( this.hasAttribute( 'month' ) ) {
return parseInt( this.getAttribute( 'month' ) );
}
return null;
}
set month( value ) {
if( value !== null ) {
this.setAttribute( 'month', value );
} else {
this.removeAttribute( 'month' );
}
}
get shadow() {
return this.hasAttribute( 'shadow' );
}
set shadow( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'shadow' );
} else {
this.setAttribute( 'shadow', '' );
}
} else {
this.removeAttribute( 'shadow' );
}
}
get year() {
if( this.hasAttribute( 'year' ) ) {
return parseInt( this.getAttribute( 'year' ) );
}
return null;
}
set year( value ) {
if( value !== null ) {
this.setAttribute( 'year', value );
} else {
this.removeAttribute( 'year' );
}
}
}
CountdownClock.SECONDS = 1000;
CountdownClock.MINUTES = 60 * 1000;
CountdownClock.HOURS = 60 * 60 * 1000;
CountdownClock.DAYS = 24 * 60 * 60 * 1000;
window.customElements.define( 'dt-countdown', CountdownClock );
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Countdown Clock</title>
<!-- Google Fonts -->
<!-- Roboto (Regular 400, Bold 700) -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<!-- Styles -->
<style>
body {
align-items: center;
background-color: #8fbfe0;
display: flex;
flex-direction: column;
height: 100vh;
justify-content: center;
}
/*
dt-countdown {
--digit-background: #eaeaea;
--digit-color: #4b4b4b;
--done-color: #ff0000;
--heading-color: #4b4b4b;
--label-color: #a9a9a9;
}
*/
</style>
</head>
<body>
<!-- Tag -->
<!-- One-based month -->
<!-- Unused attribute: capitalize -->
<dt-countdown
day="3"
heading="The big day"
label="Nicole & Chris wedding"
done="We are married! 🎉"
month="5"
shadow
year="2021">
</dt-countdown>
<!-- Alternate -->
<noscript>You need to enable JavaScript to run down Memory Lane.</noscript>
<!-- Logic -->
<script type="module" src="countdown.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment