> For the complete documentation index, see [llms.txt](https://lochiwei.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lochiwei.gitbook.io/web/js/proj/calculator.md).

# Calculator ❤️

🔸 [心得](/web/js/proj/calculator.md#tips)┊ [1. Table layout](/web/js/proj/calculator.md#v1) ┊ [2. Grid layout](/web/js/proj/calculator.md#v2) ┊ [3. Web Component](/web/js/proj/calculator.md#v-3-componet) ❤️

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

* MakeUseOf ⟩ [How to Build a Simple Calculator Using HTML, CSS, and JavaScript](https://www.makeuseof.com/build-a-simple-calculator-using-html-css-javascript/)
* CSSTricks ⟩&#x20;
  * [Fitting Text to a Container](https://css-tricks.com/fitting-text-to-a-container/)
  * [Viewport Sized Typography](https://css-tricks.com/viewport-sized-typography/)
  * [A Complete Guide to Grid](https://css-tricks.com/snippets/css/complete-guide-grid/)
* NewbeDev ⟩ [How to make font-size relative to parent div?](https://newbedev.com/how-to-make-font-size-relative-to-parent-div) - use React.
  {% endtab %}

{% tab title="📘 手冊" %}

* MDN ⟩&#x20;
  * [\<input>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) - [input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types)
  * [\<template>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) : [HTMLTemplateElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement)
  * [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) - error.message
    {% endtab %}

{% tab title="👥 相關" %}

* [Grid](/web/css/layout/grid.md#cheatsheet)
* [Web Components](/web/component.md)
  {% endtab %}

{% tab title="💎 資源" %}

* Google Fonts ⟩ [Orbitron](https://fonts.google.com/specimen/Orbitron)
  {% endtab %}

{% tab title="🗣 討論" %}

* [Why isn't my button function clear() working?](https://stackoverflow.com/questions/17756464/why-isnt-my-button-function-clear-working)
* [Is "clear" a reserved word in Javascript?](https://stackoverflow.com/questions/7165570/is-clear-a-reserved-word-in-javascript)
* [Font scaling based on width of container](https://stackoverflow.com/questions/16056591/font-scaling-based-on-width-of-container)
  {% endtab %}
  {% endtabs %}

{% embed url="<https://codesandbox.io/embed/calculator-component-orw2u?fontsize=14&hidenavigation=1&theme=dark>" %}
simple calculator
{% endembed %}

## 心得 <a href="#tips" id="tips"></a>

{% tabs %}
{% tab title="Module" %}

```markup
<!-- 📁 index.html -->
<script type="module" src="calc-app/calc-app.js"></script>
```

{% hint style="info" %}
由 [**module**](/web/js/module/es.md) 透過 **customElements.define**( ) 讀進來的 class **CalcApp**，可以直接用嗎❓例如：`const app = new CalcApp( );`&#x20;

答案是：可以❗️但 **export**/**import** 還是要先做❗️\
👉🏻 [ES Modules](/web/js/module/es.md#cheatsheet)
{% endhint %}

```javascript
// ⭐️ 1. export class (📁 calc-app.js)
export default class CalcApp extends HTMLElement { ... }

// ⭐️ 2. import class (📁 main.js)
import CalcApp from './calc-app/calc-app.js';
```

{% endtab %}

{% tab title="template.content" %}
如果我們有：

```markup
<template>
  <div></div>
</template>
```

{% hint style="danger" %}
這時注意❗️\
**template**.**querySelector**('div') === **null** ❗️\
💾 [codepen](https://codepen.io/lochiwei/pen/rNwRgLB?editors=0012)
{% endhint %}

```javascript
const {log} = console;

const template = $('template');
const div1 = $('div', template);
const div2 = $('div', template.content);

log(`template: ${template}`);     // HTMLTemplateElement
log(`div1: ${div1}`);             // ⭐️ null ❗️
log(`div2: ${div2}`);             // HTMLDivElement

// ⭐️ $(): select first element
function $(selector, parent = document){
  return parent.querySelector(selector);
}
```

{% hint style="info" %}
&#x20;[**HTMLTemplateElement**](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement) has a [**content**](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement/content) property, which is a **read-only** [**DocumentFragment**](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) containing the **DOM subtree** which the template represents. 📘MDN ⟩ \<template> ⟩ [Attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#attributes)
{% endhint %}

換句話說：template.**content** 才有 **DOM subtree**，template 本身並沒有❗️
{% endtab %}

{% tab title="怪事❗️" %}
{% hint style="danger" %}
練習時遇到的怪事：

```markup
<input onclick="clear()">
```

**onclick**="..." 裡面竟然不能寫 "**clear**( )"❗️就算寫了，也沒用❗️\
👉🏻 參看上方「🗣 討論」頁。
{% endhint %}

* ⭐️ 解釋：\
  自己寫的 **clear**() 是 **global function**，事實上放在 **window\.clear**()，所以當 "**click**" event 發生時，透過 **event bubbling** 的機制，**document.clear**() 會先收到 "click" event，所以就把 **window\.clear**() 也**屏蔽掉**了。
  {% endtab %}

{% tab title="📘 參考" %}
{% hint style="warning" %}
**Document.clear()** does **nothing**, but **doesn't raise any error**.\
📘 [Document.clear()](https://developer.mozilla.org/en-US/docs/Web/API/Document/clear) - **Deprecated❗️**
{% endhint %}
{% endtab %}
{% endtabs %}

## Code

### v.1：Table layout (original) <a href="#v1" id="v1"></a>

{% tabs %}
{% tab title="codepen" %}
{% embed url="<https://codepen.io/lochiwei/pen/eYRbXJy>" %}
Table layout
{% endembed %}
{% endtab %}

{% tab title="Calculator.js" %}

```javascript
// State
const State = {
    good: 0,
    bad: 1,
    error: 2,
};

// Calculator
class Calculator {
    
    // init
    constructor(){
        this._memory = '';
        this._lastInput = undefined;
        this._debugMode = true;
    }

    // enter key into memory
    input(key){

        if (key == "×") key = '*';
        if (key == "+") key = '+';
        if (key == "-") key = '-';
        if (key == "÷") key = '/';

        // if (this.debugMode) console.log(`key: ${key}`);

        switch(key){

            // clear
            case 'C': 
                this._memory = ''; 
                break;

            // execute
            case '=':
                this._memory = this.isCurrentResultValid
                    ? `${this._currentResult}`
                    : '💥 Error';
                break;

            // in the middle of entering
            default:
                this._memory += key;
                if (this.state == State.error) this._memory = key;
        }
        
        this._lastInput = key;
        this._calcCurrentResult();

        if (this.debugMode) this.log();
    }

    get memory(){
        return this._memory;
    }

    get debugMode(){
        return this._debugMode;
    }

    set debugMode(bool){
        this._debugMode = bool;
    }

    get isCurrentResultValid(){
        return Number.isFinite(this._currentResult);
    }

    get currentResult(){
        return this._currentResult;
    }

    log(){
        console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
    }

    // calculate current result based on memory
    _calcCurrentResult(){

        // empty memory
        if(this._memory == ''){
            this._currentResult = 0;
            return;
        }

        // memory not empty, try to calculate current result
        try {
            // if success, return the result
            const result = eval(this._memory);
            this._currentResult = result;
        } catch(e) {
            // else set current result `undefined`
            if (this.debugMode) console.log(`${e.name}: ${e.message}`);
            this._currentResult = undefined;
        }
    }

    get state(){
        if (this.isCurrentResultValid) return State.good;
        if (this._lastInput == '=') return State.error;
        return State.bad;
    }

}
```

{% endtab %}

{% tab title="main.js" %}

```javascript
const { log } = console;

const display = $('#display');
const calcUI = $('.calculator');

const calculator = new Calculator();
// calculator.debugMode = false;

// event listener ---------------------
calcUI.addEventListener('click', e => {

    // make sure target is <input>
    let t = e.target;
    if (t.nodeName !== 'INPUT') return;

    // entering `key` into calculator
    let key = t.value;
    calculator.input(key);
    display.value = calculator.memory;

    // update UI state
    let state = calculator.state;
    display.classList.toggle('bad', state === State.bad);
    display.classList.toggle('error', state === State.error);
});

// helpers ------------------------------

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

```

{% endtab %}

{% tab title="css" %}

```css
/* Google Fonts: Orbitron */
@import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');

/* css reset */

* {
  box-sizing: border-box;
}

.calculator {
  width: 400px;
  height: 380px;
  padding: 10px;
  border-radius: 1em;
  margin: auto;

  background: #191b28;
  box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
}

/* highlight cells */
/* tr {
  background: hsl(60, 85%, 70%);
} */

.display-box,
.button {
  font-family: 'Orbitron', sans-serif;
  font-size: 2em;

  width: 100%;
  height: 100%;
  border: 0.5px solid black;
  border-radius: 5px;
}

.display-box {
  background: #dcdbe1;
  color: black;
  text-align: right;
  padding: 0 .5em;
}

.display-box.bad {
    background: hsl(45, 80%, 80%);
}

.display-box.error {
    background: hsl(0, 80%, 50%);
    color: yellow;
}

.button {
  background: hsl(270, 80%, 40%);
  color: white;
}

.button:active {
  background: hsl(270, 80%, 50%);
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
}
```

{% endtab %}

{% tab title="html" %}

```markup
<table class="calculator">
  
  <tr>
    <td colspan="3">
      <input class="display-box" type="text" id="display" value="" disabled />
    </td>

    <td>
      <input class="button" type="button" value="C" style="background-color: #fb0066;" />
    </td>
  </tr>

  <tr>
    <td>
      <input class="button" type="button" value="1" />
    </td>
    <td>
      <input class="button" type="button" value="2" />
    </td>
    <td>
      <input class="button" type="button" value="3" />
    </td>
    <td>
      <input class="button" type="button" value="+" />
    </td>
  </tr>

  <tr>
    <td>
      <input class="button" type="button" value="4" />
    </td>
    <td>
      <input class="button" type="button" value="5" />
    </td>
    <td>
      <input class="button" type="button" value="6" />
    </td>
    <td>
      <input class="button" type="button" value="-" />
    </td>
  </tr>

  <tr>
    <td>
      <input class="button" type="button" value="7" />
    </td>
    <td>
      <input class="button" type="button" value="8" />
    </td>
    <td>
      <input class="button" type="button" value="9" />
    </td>
    <td>
      <input class="button" type="button" value="×" />
    </td>
  </tr>

  <tr>
    <td>
      <input class="button" type="button" value="." />
    </td>
    <td>
      <input class="button" type="button" value="0" />
    </td>

    <td>
      <input class="button" type="button" value="=" style="background-color: #fb0066;" />
    </td>
    <td>
      <input class="button" type="button" value="÷" />
    </td>
  </tr>
</table>
```

{% endtab %}

{% tab title="💾 程式" %}

* replit ⟩ [calculator v.2](https://replit.com/@pegasusroe/calculator-v2#Calculator.js)
* codepen ⟩ [calculator](https://codepen.io/lochiwei/pen/eYRbXJy)
  {% endtab %}
  {% endtabs %}

### v.2：Grid layout <a href="#v2" id="v2"></a>

{% tabs %}
{% tab title="codepen" %}
{% embed url="<https://codepen.io/lochiwei/pen/WNOmxeb>" %}
Grid layout
{% endembed %}
{% endtab %}

{% tab title="Calculator.js" %}

```javascript
// Calculator State
const State = {
    good : 0,
    bad  : 1,
    error: 2,
};

// Model: Calculator
class Calculator {
    
    // init
    constructor(){
        this._memory = '';
        this._lastInput = undefined;
        this._debugMode = true;
    }

    // enter `key` into memory
    input(key){

        switch(key){
            case "×": key = '*'; break;
            case "+": key = '+'; break;
            case "−": key = '-'; break;
            case "÷": key = '/'; break;
        }

        switch(key){

            // clear
            case 'C': 
                this._memory = ''; 
                break;

            // execute
            case '=':
                this._memory = this.isCurrentResultValid
                    ? `${this._currentResult}`
                    : '💥 Error';
                break;

            // in the middle of entering
            default:
                // append `key` to memory
                this._memory += key;

                // ⭐️ if in error state, clear memory and start over.
                if (this.state == State.error) {
                    this._memory = key;
                }
        }
        
        this._lastInput = key;
        this._calcCurrentResult();

        if (this.debugMode) this.log();
    }

    // this.memory
    get memory(){
        return this._memory;
    }

    // this.debugMode
    get debugMode(){
        return this._debugMode;
    }

    // this.debugMode = true | false
    set debugMode(bool){
        this._debugMode = bool;
    }

    // this.isCurrentResultValid
    get isCurrentResultValid(){
        return Number.isFinite(this._currentResult);
    }

    // this.currentResult
    get currentResult(){
        return this._currentResult;
    }

    // this.log()
    log(){
        console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
    }

    // calculate current result based on `memory`
    _calcCurrentResult(){

        // empty memory
        if (this._memory == '') {
            this._currentResult = 0;
            return;
        }

        // memory not empty, try to calculate current result
        try {
            // if success, return the result
            const result = eval(this._memory);
            this._currentResult = result;
        } catch(e) {
            // else set current result `undefined`
            if (this.debugMode) console.log(`${e.name}: ${e.message}`);
            this._currentResult = undefined;
        }
    }

    // this.state
    get state(){
        // result is OK
        if (this.isCurrentResultValid) return State.good;
        // hit '=' and result is Not OK
        if (this._lastInput == '=') return State.error;
        // still in the middle of entering something
        return State.bad;
    }

    // this.inGoodState
    get inGoodState() {
        return this.state === State.good;
    }

    // this.inBadState
    get inBadState() {
        return this.state === State.bad;
    }

    // this.inErrorState
    get inErrorState() {
        return this.state === State.error;
    }

}
```

{% endtab %}

{% tab title="main.js" %}

```javascript
const { log } = console;

const buttonLabels = [
    'C', '1', '2', '3', '+',
    '4', '5', '6', '−',
    '7', '8', '9', '×',
    '.', '0', '=', '÷',
];

// views
const display = $('.display-box');
const calcUI = $('.calculator');

// for each button label, create new button
buttonLabels.forEach(lbl => {

    // append <button> to calculator
    let btn = tag('button', {
        textContent: lbl,
        parentNode: calcUI
    });

    // highlight "command" buttons
    if (lbl === 'C' || lbl === '=') {
        btn.classList.add('command');
    }
    
});

// model
const calculator = new Calculator();
// calculator.debugMode = false;

// event listener ---------------------

calcUI.addEventListener('click', e => {

    // make sure target is <button>
    let t = e.target;
    if (t.nodeName !== 'BUTTON') return;

    // get `key` from UI
    let key = t.textContent;

    // update model
    calculator.input(key);
    
    // update UI
    display.value = calculator.memory;
    display.classList.toggle('bad', calculator.inBadState);
    display.classList.toggle('error', calculator.inErrorState);
});

// helpers ------------------------------

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

// ⭐️ tag()
function tag(name, {
    textContent,
    attributes = {},
    parentNode,
}={}){

    // create element
    let elem = document.createElement(name);

    // set text content if necessary
    if (textContent) {
        elem.textContent = textContent;
    }
    
    // set attributes
    for (const [key, value] of Object.entries(attributes)) {
        elem.setAttribute(key, value);
    }

    // append to parent node if present
    if(parentNode){
        parentNode.appendChild(elem);
    }
    
    // return element
    return elem;
}
```

{% endtab %}

{% tab title="CSS" %}

```css
/* Google Fonts: Orbitron */
@import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');

/* css reset */

* {
  box-sizing: border-box;
}

/* calculator */

.calculator {

    /* ⭐️ configurable css vars */
    --calculator-user-width: 80vw;
    --calculator-user-height: 80vh;
    --calculator-padding: 10px;

    /* ⭐️ computed css vars */
    --calculator-width: max(var(--calculator-user-width), 115px);
    --calculator-height: max(var(--calculator-user-height), 120px);
    --calculator-cell-min-side: min(
        calc((var(--calculator-width)  - 2 * var(--calculator-padding))/4), 
        calc((var(--calculator-height) - 2 * var(--calculator-padding))/5)
    );

  width: var(--calculator-width);
  height: var(--calculator-height);

  padding: var(--calculator-padding);
  border-radius: 10px;
  margin: auto;

  background: #191b28;
  box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
}

.display-box,
button {

  font-family: 'Orbitron', sans-serif;
  font-size: calc(var(--calculator-cell-min-side) * 0.4);
  font-weight: bold;
  /* font-stretch: ultra-expanded; */

  width: 100%;
  height: 100%;
  border: 0.5px solid black;
  border-radius: 5px;
}

.display-box {
  background: #dcdbe1;
  color: black;
  text-align: right;
  padding: 0 .5em;
}

.display-box.bad {
    background: hsl(45, 80%, 80%);
}

.display-box.error {
    background: hsl(0, 80%, 50%);
    color: yellow;
}

button {
  background: hsl(270, 80%, 40%);
  color: white;
}

button.command {
    background-color: #fb0066;
}

button:active {
  background: hsl(270, 80%, 50%);
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
}

/* Grid layout */

.calculator {
    display: grid;
    /* rows / cols */
    grid-template: repeat(5, 1fr) / repeat(3, auto) 1fr;
}

.calculator .display-box {
    /*  r1 /  c1 /   r2   /  c2   */
    /* row / col / height / width */
    grid-area: 1 / 1 / 1 / span 3;
}

```

{% endtab %}

{% tab title="HTML" %}

```markup
<div class="calculator">
  <!-- display-box -->
  <input class="display-box" type="text" value="" disabled />
  <!-- ⭐️ other buttons added by script -->
</div>
```

{% endtab %}

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

{% endtab %}
{% endtabs %}

## v.3：Web Componet <a href="#v-3-componet" id="v-3-componet"></a>

{% tabs %}
{% tab title="CodeSandbox" %}
{% embed url="<https://codesandbox.io/embed/calculator-component-orw2u?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark>" %}
CodeSandbox
{% endembed %}

{% endtab %}

{% tab title="HTML" %}

```markup
<div class="container centered-element">
  <!-- web component (custom element) -->
  <calc-app width="200px" height="240px"></calc-app>
  <calc-app></calc-app>
</div>

<!-- scripts -->
<script type="module" src="calc-app/calc-app.js"></script>
<script type="module" src="main.js"></script>
```

{% endtab %}

{% tab title="CalcApp.js" %}

```javascript
import Calculator from './Calculator.js';

const { log } = console;

// ⭐️ 1. custom web component
export default class CalcApp extends HTMLElement {

    // constructor
    constructor(width, height, debugMode=true) {

        // tell HTMLElement to initialize itself.
        super();

        // ⭐️ get `width`, `height` 
        //   from elem attributes or function arguments
        const w = this.getAttribute('width') || width || '120px';
        const h = this.getAttribute('height') || height || '150px';

        // ⭐️ set user's preferred size
        this.style.setProperty('--calculator-user-width', w);
        this.style.setProperty('--calculator-user-height', h);

        // model
        this.calculator = new Calculator();
        this.calculator.debugMode = debugMode;
    }

    // connected to document
    connectedCallback() {

        // attach shadow root
        let root = this.attachShadow({mode: 'open'});

        // ⭐️ clone nodes from <template>
        let template = CalcApp.template;
        let clone = CalcApp.template.content.cloneNode(true);
        root.append( clone );

        // connect views
        this.display = $('.display-box', root);
        this.UI = $('.calculator', root);

        // add event listener ---------------------
        this.UI.addEventListener('click', e => {

            // make sure target is <button>
            let t = e.target;
            if (t.nodeName !== 'BUTTON') return;

            // get `key` from UI
            let key = t.textContent;

            // update model
            this.calculator.input(key);
            
            // update UI
            this.display.value = this.calculator.memory;
            this.display.classList.toggle('bad', this.calculator.inBadState);
            this.display.classList.toggle('error', this.calculator.inErrorState);
        });
    }

    // template HTML
    static get template() {
        if (!CalcApp._template) {

            // create <template> element
            const template = document.createElement('template');
            
            template.innerHTML = ` 
                ${CalcApp.styles}

                <div class="calculator">
                    <input class="display-box" type="text" value="" disabled />
                </div>
            `;

            // append buttons to template
            const buttonLabels = [
                'C', '1', '2', '3', '+',
                '4', '5', '6', '−',
                '7', '8', '9', '×',
                '.', '0', '=', '÷',
            ];

            const calcUI = $('.calculator', template.content);

            // for each button label
            buttonLabels.forEach(lbl => {

                // append <button> to calculator
                let btn = tag('button', {
                    textContent: lbl,
                    parentNode: calcUI
                });

                // highlight "command" buttons
                if (lbl === 'C' || lbl === '=') {
                    btn.classList.add('command');
                }
                
            });

            // save template in a static private var
            CalcApp._template = template;
        }
        
        return CalcApp._template;
    }

    // template styles
    static get styles(){
        return `<style>
        /* Google Fonts: Orbitron */
        @import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');

        /* css reset */

        .calculator {
        box-sizing: border-box;
        }

        .calculator * {
            box-sizing: inherit;
        }

        /* calculator */

        .calculator {

            /* ⭐️ configurable css vars */
            --calculator-padding: 10px;

            /* ⭐️ computed css vars */
            --calculator-width: max(var(--calculator-user-width), 115px);
            --calculator-height: max(var(--calculator-user-height), 120px);
            --calculator-cell-min-side: min(
                calc((var(--calculator-width)  - 2 * var(--calculator-padding))/4), 
                calc((var(--calculator-height) - 2 * var(--calculator-padding))/5)
            );

            width: var(--calculator-width);
            height: var(--calculator-height);

            padding: var(--calculator-padding);
            border-radius: 10px;
            margin: auto;

            background: #191b28;
            box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
        }

        .display-box,
        button {

        font-family: 'Orbitron', sans-serif;
        font-size: calc(var(--calculator-cell-min-side) * 0.4);
        font-weight: bold;
        /* font-stretch: ultra-expanded; */

        width: 100%;
        height: 100%;
        border: 0.5px solid black;
        border-radius: 5px;
        }

        .display-box {
        background: #dcdbe1;
        color: black;
        text-align: right;
        padding: 0 .5em;
        }

        .display-box.bad {
            background: hsl(45, 80%, 80%);
        }

        .display-box.error {
            background: hsl(0, 80%, 50%);
            color: yellow;
        }

        button {
        background: hsl(270, 80%, 40%);
        color: white;
        }

        button.command {
            background-color: #fb0066;
        }

        button:active {
        background: hsl(270, 80%, 50%);
        box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
        }

        /* Grid layout */

        .calculator {
            display: grid;
            /* rows / cols */
            grid-template: repeat(5, 1fr) / repeat(3, auto) 1fr;
        }

        .calculator .display-box {
            /*  r1 /  c1 /   r2   /  c2   */
            /* row / col / height / width */
            grid-area: 1 / 1 / 1 / span 3;
        }
        </style>`;
    }
} 

// ⭐️ 2. custom tag <calc-app>
// ⭐️⭐️ custom element names must contain a "hyphen".
customElements.define('calc-app', CalcApp);

// helpers ------------------------------

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

// ⭐️ tag()
function tag(name, {
    textContent,
    attributes = {},
    parentNode,
}={}){

    // create element
    let elem = document.createElement(name);

    // set text content if necessary
    if (textContent) {
        elem.textContent = textContent;
    }
    
    // set attributes
    for (const [key, value] of Object.entries(attributes)) {
        elem.setAttribute(key, value);
    }

    // append to parent node if present
    if(parentNode){
        parentNode.appendChild(elem);
    }
    
    // return element
    return elem;
}
```

{% endtab %}

{% tab title="Calculator.js" %}

```javascript
// Calculator State
const State = {
    good : 0,
    bad  : 1,
    error: 2,
};

// Model: Calculator
export default class Calculator {
    
    // init
    constructor(){
        this._memory = '';
        this._lastInput = undefined;
        this._debugMode = true;
    }

    // enter `key` into memory
    input(key){

        switch(key){
            case "×": key = '*'; break;
            case "+": key = '+'; break;
            case "−": key = '-'; break;
            case "÷": key = '/'; break;
        }

        switch(key){

            // clear
            case 'C': 
                this._memory = ''; 
                break;

            // execute
            case '=':
                this._memory = this.isCurrentResultValid
                    ? `${this._currentResult}`
                    : '💥 Error';
                break;

            // in the middle of entering
            default:
                // append `key` to memory
                this._memory += key;

                // ⭐️ if in error state, clear memory and start over.
                if (this.state == State.error) {
                    this._memory = key;
                }
        }
        
        this._lastInput = key;
        this._calcCurrentResult();

        if (this.debugMode) this.log();
    }

    // this.memory
    get memory(){
        return this._memory;
    }

    // this.debugMode
    get debugMode(){
        return this._debugMode;
    }

    // this.debugMode = true | false
    set debugMode(bool){
        this._debugMode = bool;
    }

    // this.isCurrentResultValid
    get isCurrentResultValid(){
        return Number.isFinite(this._currentResult);
    }

    // this.currentResult
    get currentResult(){
        return this._currentResult;
    }

    // this.log()
    log(){
        console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
    }

    // calculate current result based on `memory`
    _calcCurrentResult(){

        // empty memory
        if (this._memory == '') {
            this._currentResult = 0;
            return;
        }

        // memory not empty, try to calculate current result
        try {
            // if success, return the result
            const result = eval(this._memory);
            this._currentResult = result;
        } catch(e) {
            // else set current result `undefined`
            if (this.debugMode) console.log(`${e.name}: ${e.message}`);
            this._currentResult = undefined;
        }
    }

    // this.state
    get state(){
        // result is OK
        if (this.isCurrentResultValid) return State.good;
        // hit '=' and result is Not OK
        if (this._lastInput == '=') return State.error;
        // still in the middle of entering something
        return State.bad;
    }

    // this.inGoodState
    get inGoodState() {
        return this.state === State.good;
    }

    // this.inBadState
    get inBadState() {
        return this.state === State.bad;
    }

    // this.inErrorState
    get inErrorState() {
        return this.state === State.error;
    }

}
```

{% endtab %}

{% tab title="main.js" %}

```javascript
// ⭐️ import CalcApp first
import CalcApp from './calc-app.js';

// ⭐️ new <calc-app>
const app = new CalcApp('160px', '200px');

// calculator apps container
const div = $('.container');
div.append(app);

// helpers ------------------------------

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

```

{% endtab %}

{% tab title="style.css" %}

```css
.container {
    display: inline-flex;
    gap: 10px;
}

.centered-element {

    margin: 0;
    /* border: 1px solid black; */
    
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
```

{% endtab %}

{% tab title="💾 程式" %}

* replit ⟩ [calculator (componet)](https://replit.com/@pegasusroe/calculator-component#CalcApp/CalcApp.js)
  {% endtab %}
  {% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://lochiwei.gitbook.io/web/js/proj/calculator.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
