WildflowerJS Demos
World Clock: data-bind, data-show, data-model, data-bind-style, computed

World Clock

One file. Reactive state on requestAnimationFrame.

Showing
Time zone

World clocks

in state

Built with wildflower.nano.min.js

HTML

<div data-component="world-clock">
    <button data-action="toggleView" data-bind="toggleLabel"></button>

    <!-- Analog: three hands, each positioned by a computed style -->
    <div class="face" data-show="analog">
        <div class="hand hour"   data-bind-style="hourStyle"></div>
        <div class="hand minute" data-bind-style="minuteStyle"></div>
        <div class="hand second" data-bind-style="secondStyle"></div>
    </div>

    <!-- Digital -->
    <div class="digital" data-show="!analog">
        <div data-bind="digital"></div>
        <div data-bind="dateStr"></div>
    </div>

    <select data-model="zone"> ...time zones... </select>

    <div id="clock-wall"></div>   <!-- cloned city cards land here -->
</div>

<!-- One city, written once. cloneNode() stamps copies. -->
<template id="city-template">
    <div class="city" data-component="city-clock">
        <button data-action="remove">&times;</button>
        <div data-bind="label"></div>
        <div data-bind="time"></div>
        <div data-bind="date"></div>
    </div>
</template>

JavaScript

// The main clock: a reactive `now` updated every animation frame gives the
// second hand a smooth sweep; every computed below is derived from it.
wildflower.component('world-clock', {
    state: {
        now: Date.now(), zone: 'America/New_York',
        analog: true, cities: [], newCity: 'Europe/London'
    },
    computed: {
        zParts() { return zonedParts(this.now, this.zone); },
        secondAngle() { return (this.zParts.s + (this.now % 1000) / 1000) * 6; },
        minuteAngle() { return (this.zParts.m + this.zParts.s / 60) * 6; },
        hourAngle() { return ((this.zParts.h % 12) + this.zParts.m / 60) * 30; },
        hourStyle() { return { transform: `translateX(-50%) rotate(${this.hourAngle}deg)` }; },
        minuteStyle() { return { transform: `translateX(-50%) rotate(${this.minuteAngle}deg)` }; },
        secondStyle() { return { transform: `translateX(-50%) rotate(${this.secondAngle}deg)` }; },
        digital() { return fmtTime(this.now, this.zone); },
        toggleLabel() { return this.analog ? 'Digital' : 'Analog'; },
        cityCount() { return this.cities.length; }
    },
    init() {
        const tick = () => { this.now = Date.now(); this._raf = requestAnimationFrame(tick); };
        this._raf = requestAnimationFrame(tick);
    },
    destroy() { cancelAnimationFrame(this._raf); },
    toggleView() { this.analog = !this.analog; },
    // Array in state stays the source of truth; reassign to guarantee reactivity.
    addCity() {
        const zone = this.newCity;
        if (!zone || this.cities.includes(zone)) return;
        this.cities = this.cities.concat(zone);
        // Clone the <template> and append it. The mutation observer
        // initializes the new city-clock component automatically.
        const el = document.getElementById('city-template').content.firstElementChild.cloneNode(true);
        el.dataset.zone = zone;
        this.element.querySelector('#clock-wall').appendChild(el);
    },
    dropCity(zone) { this.cities = this.cities.filter(z => z !== zone); }
});

// Each city on the wall is its own reactive component, self-ticking once a second.
wildflower.component('city-clock', {
    state: { now: Date.now() },
    computed: {
        zone() { return this.element ? this.element.dataset.zone : 'UTC'; },
        label() { return cityLabel(this.zone); },
        time() { return fmtTime(this.now, this.zone); },
        date() { return fmtDate(this.now, this.zone); }
    },
    init() { this._timer = setInterval(() => { this.now = Date.now(); }, 1000); },
    // The mutation observer fires destroy() when the element leaves the DOM.
    destroy() { clearInterval(this._timer); },
    remove() {
        const main = wildflower.getComponents('world-clock')[0];
        if (main) main.dropCity(this.zone);
        this.element.remove();
    }
});