# \<live-timer>

{% tabs %}
{% tab title="📗 參考" %}
* Exercise: [\<live-timer> element](https://javascript.info/custom-elements#live-timer-element) - JS.info
* [solution](https://plnkr.co/edit/5A1yvhPjyH9RbdoX?p=preview\&preview) to the exercise
{% endtab %}

{% tab title="📘 手冊" %}
* [element.firstElementChild()](https://developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild)
{% endtab %}
{% endtabs %}

:floppy\_disk: [replit](https://replit.com/@pegasusroe/web-component-live-timer#js/LiveTimer.js)

{% tabs %}
{% tab title="LiveTimer" %}
```javascript
// 1. define new element
class LiveTimer extends HTMLElement {

    /* ---------- life cycle callbacks ---------- */

    // triggers when element is added to page
    // (or when HTML parser detects it)
    connectedCallback() {

        this.render();

        // ⭐️ start timer
        this.startTimer();
    }

    disconnectedCallback() {
        // ⭐️ important to let the element be garbage-collected
        this.cancelTimer(); 
    }

  /* ---------- helper methods ---------- */
  
    render() {

        // show a nicely formatted time.
        // ⭐️ 注意：這裡設定的東西屬於 light DOM❗️
        this.innerHTML = `
            <time-formatted 
                year="numeric" 
                month="long" 
                day="numeric" 
                hour="numeric" 
                minute="numeric" 
                second="numeric"
                time-zone-name="short">
            </time-formatted>
        `;

        // ⭐️ reference to <time-formatted> child
        this.timeChild = this.firstElementChild;
    }

    // update
    update(){

        this.date = new Date();

        // ⭐️ update child
        this.timeChild.attr('datetime', this.date);

        // ⭐️ dispatch 'tick'
        let tick = new CustomEvent('tick', {
            detail: this.date
        });

        this.dispatchEvent(tick);
    }

    // start timer
    startTimer(){
        // ⭐️ 注意：不要寫成「 setInterval(this.update, 1000) 」❗️
        //         不然會失去 `this` context❗️
        this.timer = setInterval(() => this.update(), 1000);
    }

    // cancel timer
    cancelTimer(){
        // ⭐️ 注意：不是 this.timer = null❗️
        clearInterval(this.timer);
    }
}

// 2. register new element
customElements.define("live-timer", LiveTimer);
```
{% endtab %}

{% tab title="TimeFormatted" %}
```javascript
// 1. define new element
class TimeFormatted extends HTMLElement {

    /* ---------- life cycle callbacks ---------- */

    constructor(){
        super();
        this.initialized = false;
    }

    // triggers when <time-formatted> element is added to page
    // (or when HTML parser detects it)
    connectedCallback() {
        this.render();
        this.initialized = true;
    }

    // observed attributes
    static get observedAttributes() { // (3)
        return [
            'datetime', 'year', 'month', 'day', 
            'hour', 'minute', 'second', 'time-zone-name'
        ];
    }

    attributeChangedCallback(name, oldValue, newValue) { 
        // re-render when attributes changed
        this.render();
        // if(this.initialized) log(`${name} = ${newValue}`);
    }

  /* ---------- helper methods ---------- */
  
    render() {

        // let date = new Date(this.attr('datetime') || Date.now());
        let date = new Date();

        // built-in Intl.DateTimeFormat formatter
        let formatter = new Intl.DateTimeFormat("default", {
            year  : this.attr('year'),
            month : this.attr('month'),
            day   : this.attr('day'),
            hour  : this.attr('hour'),
            minute: this.attr('minute'),
            second: this.attr('second'),
            timeZoneName: this.attr('time-zone-name'),
        });

        // show a nicely formatted time.
        // ⭐️ 注意：這裡設定的東西屬於 light DOM❗️
        this.innerHTML = formatter.format(date);
    }
}

// 2. register new element
customElements.define("time-formatted", TimeFormatted);
```
{% endtab %}

{% tab title="helper" %}
```javascript
const {log} = console;

// ⭐️ $()
function $(selector, parent = document){
  return parent.querySelector(selector);
}

// ⭐️ get/set attribute
//    get: elem.attr(name)
//    set: elem.attr(name, value)
Element.prototype.attr = function(name, value=undefined){
    if(value !== undefined){ this.setAttribute(name, value) };
    return this.getAttribute(name) || undefined;
};
```
{% endtab %}

{% tab title="main" %}
```javascript
// change attribute 'datetime' per second.
let timer1 = setInterval(
    () => elem.attr('datetime', new Date()), 
    1000
);

// cancel above timer
function cancelTimer1(){
    clearInterval(timer1);
}
```
{% endtab %}

{% tab title="HTML" %}
```markup
<h3>&lt;time-formatted&gt;</h3>

<time-formatted 
  id="elem"
  datetime="2019-12-01" 
  year="numeric" 
  month="long" 
  day="numeric" 
  hour="numeric" 
  minute="numeric" 
  second="numeric"
  time-zone-name="short">
</time-formatted>

<h3>&lt;live-timer&gt;</h3>

<live-timer id="timer"></live-timer>

<br><br>
<button onclick="cancelTimer1()">cancel timer 1</button>
<button onclick="timer.cancelTimer()">cancel timer 2</button>

<script src="js/helper.js"></script>
<script src="js/TimeFormatted.js"></script>
<script src="js/LiveTimer.js"></script>
<script src="js/main.js"></script>
```
{% endtab %}

{% tab title="📗 參考" %}

{% endtab %}
{% endtabs %}
