Calculator ❤️
🔸 心得┊ 1. Table layout ┊ 2. Grid layout ┊ 3. Web Component ❤️
- NewbeDev ⟩ How to make font-size relative to parent div? - use React. 
- MDN ⟩ - Error - error.message 
 
- Google Fonts ⟩ Orbitron 
心得
<!-- 📁 index.html -->
<script type="module" src="calc-app/calc-app.js"></script>// ⭐️ 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';如果我們有:
<template>
  <div></div>
</template>這時注意❗️ template.querySelector('div') === null ❗️ 💾 codepen
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);
}換句話說:template.content 才有 DOM subtree,template 本身並沒有❗️
練習時遇到的怪事:
<input onclick="clear()">onclick="..." 裡面竟然不能寫 "clear( )"❗️就算寫了,也沒用❗️ 👉🏻 參看上方「🗣 討論」頁。
- ⭐️ 解釋: 自己寫的 clear() 是 global function,事實上放在 window.clear(),所以當 "click" event 發生時,透過 event bubbling 的機制,document.clear() 會先收到 "click" event,所以就把 window.clear() 也屏蔽掉了。 
Document.clear() does nothing, but doesn't raise any error. 📘 Document.clear() - Deprecated❗️
Code
v.1:Table layout (original)
// 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;
    }
}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);
}
/* 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;
}<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>- replit ⟩ calculator v.2 
- codepen ⟩ calculator 
v.2:Grid layout
// 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;
    }
}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;
}/* 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;
}
<div class="calculator">
  <!-- display-box -->
  <input class="display-box" type="text" value="" disabled />
  <!-- ⭐️ other buttons added by script -->
</div>v.3:Web Componet
<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>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;
}// 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;
    }
}// ⭐️ 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);
}
.container {
    display: inline-flex;
    gap: 10px;
}
.centered-element {
    margin: 0;
    /* border: 1px solid black; */
    
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}- replit ⟩ calculator (componet) 
Last updated
Was this helpful?