Last active
November 14, 2020 20:32
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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