JS 實作 #12 | 每筆花費的百分比計算;NodeList Array 實作 forEach()

最後剩下幾個小東西的收尾,包括每筆花費的百分比計算、顯示當月、以及輸入欄位的 UX 調整。

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/02ca6c15-a825-48c7-b6d1-76b2e76e36e3/_2020-03-31_21.46.02.jpg

開始實作

1. 在 GLOBAL APP CONTROLLER 裡新增一個私有方法 updatePercentages(),在新增/刪除 item 時都呼叫它。

// GLOBAL APP CONTROLLER
var controller = (function(budgetCtrl, UICtrl){

    var setupEventListeners = function() {
        ...
    };
    var updateBudget = function() {

        ...
    };

    **var updatePercentages = function() {

        // 1. Calculate percentages
					               
        // 2. Read percentages from the budget controller

        // 3. Update the UI with the new percentages
    };**

    var ctrlAddItem = function() {

        var input,newItem;

        // 1. Get the field input data
        input = UICtrl.getInput();

        if (input.description != "" && !isNaN(input.value) && input.value > 0) {
            // 2. add the item to the budget controller
            newItem = budgetController.addItem(input.type, input.description, input.value);

            // 3. add the item to the UI
            UICtrl.addListItem(newItem, input.type);

            // 4. clear the fields;
            UICtrl.clearFields();

            // 5. calcaulate & update the budget on the UI
            updateBudget();

            **// 6. update the percentage
            updatePercentages();**
        }
        
    }

    var ctrlDeleteItem = function(event) {
        var itemID, splitID, type, ID;
        itemID = event.target.parentNode.parentNode.parentNode.parentNode.id;
        if (itemID) {
            
            splitID = itemID.split("-");
            type = splitID[0];
            ID = parseInt(splitID[1]);

            // 1. delete the item from the data structure
            budgetCtrl.deleteItem(type, ID);

            // 2. delete the item from the UI
            UICtrl.deleteListItems(itemID);

            // 3. update and show the new budget
            updateBudget();

            **// 4. update the percentage
            updatePercentages();**
        }
    }
    
    return {
        init: function() {
            console.log('Application has started.');
            UICtrl.displayBudget({
                budget: 0,
                totalInc: 0,
                totalExp: 0,
                percentage: -1
            });
            setupEventListeners();
        }
    }
})(budgetController, UIController);

controller.init();

2. 在 BUDGET CONTROLLER 裡新增 **calculatePercentages()** 的公開方法,並且讓 GLOBAL APP CONTROLLER 能夠呼叫它。

// BUDGET CONTROLLER
var budgetController = (function(){

    var Expense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
    }

    var Income = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
    }

    var calculateTotal = function(type) {
        var sum = 0;
        data.allItems[type].forEach(function(cur) {
            sum += cur.value;
        });
        data.totals[type] = sum;
    }

    var data = {
        allItems: {
            exp: [],
            inc: []
        },
        totals: {
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    return {
        addItem: function(type, des, val){
            ...
        },
        deleteItem: function(type,id) {
					  ...

        },
        calculateBudget: function() {
            
            ...
        },
        **calculatePercentages: function() {
						// todo
        },**
        getBudget: function() {
            ...
        },

        testing: function(){
            console.log(data);
        }
    }

})();
// GLOBAL APP CONTROLLER
var controller = (function(budgetCtrl, UICtrl){

    var setupEventListeners = function() {
        ...
    };
    var updateBudget = function() {

        ...
    };

    var updatePercentages = function() {

        // 1. Calculate percentages
					**budgetCtrl.calculatePercentages();**
					               
        // 2. Read percentages from the budget controller

        // 3. Update the UI with the new percentages
    };

    var ctrlAddItem = function() {
        ...
    }

    var ctrlDeleteItem = function(event) {
        ...
    }
    
    return {
        init: function() {
            console.log('Application has started.');
            UICtrl.displayBudget({
                budget: 0,
                totalInc: 0,
                totalExp: 0,
                percentage: -1
            });
            setupEventListeners();
        }
    }
})(budgetController, UIController);

controller.init();

3. 由於我們需要計算每一筆 expense 各自佔比目前的收費多少%,所以在執行 calculatePercentages() 前,先針對 Expense 這個 constructor 構建函數,新增一個變數 percentage 以及二個 prototype 方法 calcPercentage() & getPercentage()

// BUDGET CONTROLLER
var budgetController = (function(){

    var Expense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
        **this.percentage = -1;**
    }

    **Expense.prototype.calcPercentage = function(totalIncome) {
        if (totalIncome > 0) {
            this.percentage = Math.round((this.value  / totalIncome) * 100);
        } else {
            this.percentage = -1;
        }
    }
    Expense.prototype.getPercentage = function () {
        return this.percentage;
    }**

    var Income = function(id, description, value) {
        ...
    }

    var calculateTotal = function(type) {
        ...
    }

    var data = {
        allItems: {
            exp: [],
            inc: []
        },
        totals: {
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    return {
        addItem: function(type, des, val){
            ...
        },
        deleteItem: function(type,id) {
						...

        },
        calculateBudget: function() {
            ...
        },
        **calculatePercentages: function() {
					// todo
        },**
        getBudget: function() {
            ...
        },

        testing: function(){
            console.log(data);
        }
    }

})();

4. 新增好 prototype 方法後,即可在 calculatePercentages() 裡執行來計算每一筆 expense 的 % 數。

// BUDGET CONTROLLER
var budgetController = (function(){

    var Expense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
        **this.percentage = -1;**
    }

    **Expense.prototype.calcPercentage = function(totalIncome) {
        if (totalIncome > 0) {
            this.percentage = Math.round((this.value  / totalIncome) * 100);
        } else {
            this.percentage = -1;
        }
    }
    Expense.prototype.getPercentage = function () {
        return this.percentage;
    }**

    var Income = function(id, description, value) {
        ...
    }

    var calculateTotal = function(type) {
        ...
    }

    var data = {
        allItems: {
            exp: [],
            inc: []
        },
        totals: {
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    return {
        addItem: function(type, des, val){
            ...
        },
        deleteItem: function(type,id) {
						...

        },
        calculateBudget: function() {
            ...
        },
        **calculatePercentages: function() {
					data.allItems.exp.forEach(function(cur) {
                cur.calcPercentage(data.totals.inc);
	        });
        },**
        getBudget: function() {
            ...
        },

        testing: function(){
            console.log(data);
        }
    }

})();

5. 再來在 BUDGET CONTROLLER 另外新增一個 getPercentages() 的公開方法,將每筆 expense 的 % 數獲取出來(以一個 array 的方式)。

// BUDGET CONTROLLER
var budgetController = (function(){

    var Expense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
        **this.percentage = -1;**
    }

    **Expense.prototype.calcPercentage = function(totalIncome) {
        if (totalIncome > 0) {
            this.percentage = Math.round((this.value  / totalIncome) * 100);
        } else {
            this.percentage = -1;
        }
    }
    Expense.prototype.getPercentage = function () {
        return this.percentage;
    }**

    var Income = function(id, description, value) {
        ...
    }

    var calculateTotal = function(type) {
        ...
    }

    var data = {
        allItems: {
            exp: [],
            inc: []
        },
        totals: {
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    return {
        addItem: function(type, des, val){
            ...
        },
        deleteItem: function(type,id) {
						...

        },
        calculateBudget: function() {
            ...
        },
        calculatePercentages: function() {
					data.allItems.exp.forEach(function(cur) {
                cur.calcPercentage();
          });
        },
				**getPercentages: function() {
            var allPerc = data.allItems.exp.map(function(cur){
                return cur.getPercentage();
            });
            return allPerc;
        },**
        getBudget: function() {
            ...
        },

        testing: function(){
            console.log(data);
        }
    }

})();
// GLOBAL APP CONTROLLER
var controller = (function(budgetCtrl, UICtrl){

    var setupEventListeners = function() {
        ...
    };
    var updateBudget = function() {

        ...
    };

    var updatePercentages = function() {

        // 1. Calculate percentages
					budgetCtrl.calculatePercentages();
					               
        // 2. Read percentages from the budget controller
					**var percentages = budgetCtrl.getPercentages();**

        // 3. Update the UI with the new percentages
    };

    var ctrlAddItem = function() {
        ...
    }

    var ctrlDeleteItem = function(event) {
        ...
    }
    
    return {
        init: function() {
            console.log('Application has started.');
            UICtrl.displayBudget({
                budget: 0,
                totalInc: 0,
                totalExp: 0,
                percentage: -1
            });
            setupEventListeners();
        }
    }
})(budgetController, UIController);

controller.init();

6. 再來要將每筆 expense 的花費百分比呈現在 UI上。首先在 UI CONTROLLER 新增一個公開的 displayPercentages() 方法,並且在 GLOBAL APP CONTROLLER 裡呼叫它(傳入 percentages array)。

// UI CONTROLLER
var UIController = (function() {

    var DOMstrings = {
        inputType: '.add__type',
        inputDescription: '.add__description',
        inputValue: '.add__value',
        inputBtn: '.add__btn',
        incomeContainer: '.income__list',
        expenseContainer: '.expenses__list',
        budgetLabel: '.budget__value',
        incomeLabel: '.budget__income--value',
        expensesLabel: '.budget__expenses--value',
        percentageLabel: '.budget__expenses--percentage',
        container: '.container'
    };

    return {
        getInput: function() {
            ...
        },
        getDOMstrings: function() {
            return DOMstrings;
        },
        addListItem: function(obj, type) {
						...
        },
        deleteListItems: function(selectorID) {
            ...
        },
        clearFields: function () {
            ...
        },
        displayBudget: function(obj) {
            ...
        },
        **displayPercentages: function(percentages) {
            //to do
        }**
    };

})();
// GLOBAL APP CONTROLLER
var controller = (function(budgetCtrl, UICtrl){

    var setupEventListeners = function() {
        ...
    };
    var updateBudget = function() {

        ...
    };

    var updatePercentages = function() {

        // 1. Calculate percentages
					budgetCtrl.calculatePercentages();
					               
        // 2. Read percentages from the budget controller
					var percentages = budgetCtrl.getPercentages();

        // 3. Update the UI with the new percentages
					**UICtrl.displayPercentages(percentages);**
    };

    var ctrlAddItem = function() {
        ...
    }

    var ctrlDeleteItem = function(event) {
        ...
    }
    
    return {
        init: function() {
            console.log('Application has started.');
            UICtrl.displayBudget({
                budget: 0,
                totalInc: 0,
                totalExp: 0,
                percentage: -1
            });
            setupEventListeners();
        }
    }
})(budgetController, UIController);

controller.init();

7. 找到 expense 百分比的 DOM class 名稱,並且 querySelectorAll() 將其選擇起來。(此為一 NodeList Array)

// UI CONTROLLER
var UIController = (function() {

    var DOMstrings = {
        inputType: '.add__type',
        inputDescription: '.add__description',
        inputValue: '.add__value',
        inputBtn: '.add__btn',
        incomeContainer: '.income__list',
        expenseContainer: '.expenses__list',
        budgetLabel: '.budget__value',
        incomeLabel: '.budget__income--value',
        expensesLabel: '.budget__expenses--value',
        percentageLabel: '.budget__expenses--percentage',
        container: '.container',
				**expensesPercLabel: '.item__percentage'**
    };

    return {
        getInput: function() {
            ...
        },
        getDOMstrings: function() {
            return DOMstrings;
        },
        addListItem: function(obj, type) {
						...
        },
        deleteListItems: function(selectorID) {
            ...
        },
        clearFields: function () {
            ...
        },
        displayBudget: function(obj) {
            ...
        },
        **displayPercentages: function(percentages) {
						var fields = document.querySelectorAll(DOMstrings.expensesPercLabel);
            //to do
        }**
    };

})();

8. 由於 NodeList Array 中是沒有 forEach() 等Array的原生方法的,除了使用先前提到的 Array.prototype.slice.call()JS 實作 #6 | 清除已新增資料的欄位;querySelectorAll()、Array.prototype.slice.call()、forEach()、focus() 將其轉型為 arrary 之外,也可以自己寫一組 NodeList 的 forEach() 方法來使用。

// UI CONTROLLER
var UIController = (function() {

    var DOMstrings = {
        inputType: '.add__type',
        inputDescription: '.add__description',
        inputValue: '.add__value',
        inputBtn: '.add__btn',
        incomeContainer: '.income__list',
        expenseContainer: '.expenses__list',
        budgetLabel: '.budget__value',
        incomeLabel: '.budget__income--value',
        expensesLabel: '.budget__expenses--value',
        percentageLabel: '.budget__expenses--percentage',
        container: '.container',
				**expensesPercLabel: '.item__percentage'**
    };

    return {
        getInput: function() {
            ...
        },
        getDOMstrings: function() {
            return DOMstrings;
        },
        addListItem: function(obj, type) {
						...
        },
        deleteListItems: function(selectorID) {
            ...
        },
        clearFields: function () {
            ...
        },
        displayBudget: function(obj) {
            ...
        },
        displayPercentages: function(percentages) {
            //to do
            var fields = document.querySelectorAll(DOMstrings.expensesPercLabel);

            **var nodeListForEach = function(nodeList, callback) {
                for (var i = 0 ; i < nodeList.length ; i++) {
                    callback(nodeList[i], i);
                }
            };

            nodeListForEach(fields, function(current, index){
                current.textContent = percentages[index] + '%';
            });**
        }
    };

})();

仔細的觀察 nodeListForEach() 這個方法,其實就是 Array.prototype.forEach 在做的事,將每一個 nodeList Array 裡的元素逐一的進行 callback function。

9. 最後調整一下,小於等於0的百分比改為顯示 ‘—‘即完成。

// UI CONTROLLER
var UIController = (function() {

    var DOMstrings = {
        inputType: '.add__type',
        inputDescription: '.add__description',
        inputValue: '.add__value',
        inputBtn: '.add__btn',
        incomeContainer: '.income__list',
        expenseContainer: '.expenses__list',
        budgetLabel: '.budget__value',
        incomeLabel: '.budget__income--value',
        expensesLabel: '.budget__expenses--value',
        percentageLabel: '.budget__expenses--percentage',
        container: '.container',
				**expensesPercLabel: '.item__percentage'**
    };

    return {
        getInput: function() {
            ...
        },
        getDOMstrings: function() {
            return DOMstrings;
        },
        addListItem: function(obj, type) {
						...
        },
        deleteListItems: function(selectorID) {
            ...
        },
        clearFields: function () {
            ...
        },
        displayBudget: function(obj) {
            ...
        },
        displayPercentages: function(percentages) {
            //to do
            var fields = document.querySelectorAll(DOMstrings.expensesPercLabel);

            var nodeListForEach = function(nodeList, callback) {
                for (var i = 0 ; i < nodeList.length ; i++) {
                    callback(nodeList[i], i);
                }
            };

            ****nodeListForEach(fields, function(current, index){
                **if (percentages[index] > 0) {
                    current.textContent = percentages[index] + '%';
                } else {
                    current.textContent = '---';
                }**
            });
        }
    };

})();

Zeen is a next generation WordPress theme. It’s powerful, beautifully designed and comes with everything you need to engage your visitors and increase conversions.