# Expense Tracker- 20 Web Projects With Vanilla Javascript
---
tags: Javascript relate
---
###### tags: `Javascript`
# 製作一個記帳簿
## 成品:

[成品網址](https://vanillawebprojects.com/projects/expense-tracker/)
## 成品功能:
1.在最上方yout balance處顯示所剩餘額
2.在income、expense處顯示所以收入以及花費金額並呈現綠色以及紅色
3.History的地方會記錄並顯示出花費狀態以及輸入的內容ex 書本支出 100元並且會有刪除品項的按鈕
ps.支出會顯示紅色 收入會顯示綠色

4.下方有新增各種行為的欄位並且有加入的submit按鈕
## HTML
### 上CSS之前的HTML:

### 程式碼:
```htmlembedded=
<body>
<h2>Expense Tracker</h2>
<div class="container">
<h4>Your Balance</h4>
<!-- 這邊的h1金額的部分會跟JS連動 -->
<h1 id="balance">$0.00</h1>
<div class="inc-exp-container">
<div>
<h4>Income</h4>
<!-- 這邊設置的id money-plus會給JS抓取 -->
<!-- 後方的plus會修改文字顏色 -->
<p id="money-plus" class="money plus">$0.00</p>
</div>
<div>
<h4>Expense</h4>
<!-- 這邊設置的id money-minus會給JS抓取 -->
<!-- 後方的minus會修改文字顏色 -->
<p id="money-minus" class="money minus">-$0.00</p>
</div>
</div>
<h3>History</h3>
<!-- 這個部分是JS操作的不會顯示出來但是因為要在CSS設定所以先打出來調整樣式 -->
<ul id="list" class="list">
<!-- <li class="minus">
Cash<span>-$400</span><button class="delete-btn">x</button>
</li> -->
</ul>
<h3>Add new transaction</h3>
<!-- 設置id form抓取這邊input全區 -->
<form id="form">
<!-- 這區域有需要跟JS互動所以有設置id text amount分別是兩個input區域-->
<div class="form-control">
<label for="text">Text</label>
<input type="text" id="text" placeholder="Enter text..." />
</div>
<div class="form-control">
<label for="amount">
Amount <br />(negative - expense, positive - income)
</label>
<input type="number" id="amount" placeholder="Enter amount..." />
</div>
<button class="btn">Add transaction</button>
</form>
</div>
<script src="script.js"></script>
</body>
```
## CSS:
### CSS設置好的樣式

### 程式碼
```css=
@import url('https://fonts.googleapis.com/css?family=Lato&display=swap')
/* 讓特定的物件在瀏覽器上變的比較明顯 */
:root {
--box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}
* {
box-sizing: border-box;
}
/* body這邊用的是老樣子一樣全置中並且設定滿版margin0 */
body {
background-color: #f7f7f7;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: 'Lato', sans-serif;
}
.container {
margin: 30px auto;
width: 350px;
}
h1 {
letter-spacing: 1px;
margin: 0;
}
h3 {
border-bottom: 1px solid #bbb;
padding-bottom: 10px;
margin: 40px 0 10px;
}
h4 {
margin: 0;
text-transform: uppercase;
}
.inc-exp-container {
background-color: #fff;
box-shadow: var(--box--shadow);
padding: 20px;
display: flex;
/* 讓income跟expnse各占一邊 */
justify-content: space-between;
margin: 20px 0;
}
.inc-exp-container>div {
flex: 1;
text-align: center;
}
/* 中間分隔線區域只設定第一個div的右方底線 */
.inc-exp-container>div:first-of-type {
border-right: 1px solid #dedede;
}
/* 單純是money的部分處理文字大小間隔以及margin */
.money {
font-size: 20px;
letter-spacing: 1px;
margin: 5px;
}
/* 這部分以動態的JS會修改文字顏色 */
.money.plus {
color: #2ecc71;
}
/* 這部分以動態的JS會修改文字顏色 */
.money.minus {
color: #c0392b;
}
/* 這邊設置inline-block讓label可以吃到margin */
label {
display: inline-block;
margin: 10px 0;
}
input[type='text'],
input[type='number'] {
border: 1px solid #dedede;
border-radius: 2px;
display: block;
font-size: 16px;
padding: 10px;
width: 100%;
}
.btn {
cursor: pointer;
background-color: #9c88ff;
box-shadow: var(--box--shadow);
color: #fff;
border: 0;
display: block;
font-size: 16px;
margin: 10px 0 30px;
padding: 10px;
width: 100%;
}
/* 去除外框 */
.btn:focus,
.delete-btn {
outline: 0;
}
.list {
/* 去除圓點點 */
list-style-type: none;
padding: 0;
margin-bottom: 40px;
}
.list li {
background-color: #fff;
box-shadow: var(--box--shadow);
color: #333;
display: flex;
justify-content: space-between;
/* 這邊設置定位relative是為了delete btn的設置 */
position: relative;
padding: 10px;
margin: 10px 0;
}
/* 這邊是加入物件的顏色用classname區別 */
.list li.plus {
border-right: 5px solid #2ecc71;
}
.list li.minus {
border-right: 5px solid #c0392b;
}
.delete-btn {
cursor: pointer;
background-color: #e74c3c;
border: 0;
color: #fff;
font-size: 20px;
line-height: 20px;
padding: 2px 5px;
position: absolute;
top: 50%;
left: 0%;
/* 調整位置往左邊一個自己往上半個自己ㄉ */
transform: translate(-100%, -50%);
/* 這邊設置隱藏 */
opacity: 0;
/* 透明度的特效設置 */
transition: opacity 0.3s ease;
}
/* 當滑鼠滑過History的物件後delete btn 才會顯示 */
.list li:hover .delete-btn {
opacity: 1;
}
```
小補充:
無
## JS:
### 變數設置
要使用JS處理的地方基本上都要抓取:
* balance 區域
處理數字呈現

* money-plus,money-minus區域
處理數字呈現

* list區域
處理加入的新的物件

* form區域
主要處理submit事件

* text,amount區域
處理輸入的內容

```javascript=
const balance = document.getElementById("balance");
const money_plus = document.getElementById("money-plus");
const money_minus = document.getElementById("money-minus");
const list = document.getElementById("list");
const form = document.getElementById("form");
const text = document.getElementById("text");
const amount = document.getElementById("amount");
```
### functions:
* addTransactionDOM-把收入或是消費的新的li呈現到History上面(也就是DOM上面)
他會在addTransaction function被呼叫,因為只有當新的交易產生這個功能才會被需要
* 注意的點:
作者使用onclick在 delete-btn上面並且使用template string`${}`放入動態的變數進去符合id,這個用法只能用在字串中一般的function無法使用
```javascript=
//add transactions to DOM 把收入或是消費的新的li呈現到History上面(也就是DOM)
function addTransactionDOM(transaction) {
// get sign 取得正負號(+-)
const sign = transaction.amount < 0 ? "-" : "+";
//創造新的li要放進去收入或是花費
const item = document.createElement("li");
//add class base on value 把剛創造的li加上class但是做要判斷
// 當transactions的值amount<0時放入minus,>0時放plus
item.classList.add(transaction.amount < 0 ? "minus" : "plus");
//放入的字串用template string串接放入變數transacitons.text當作物品名稱
//sign 表示正負
//Math.abs(transactions.amount) 這段用數學方法取絕對值因為前面有sing判斷正負了
item.innerHTML = `
${transaction.text} <span>${sign}${Math.abs(
transaction.amount
// 使用onlick事件並且使用動態參數`${}`包裹住transaction的id這樣一來參數就會是動態的
//內容會隨著刪除的id不同而產生不同的array
)}</span> <button class="delete-btn" onclick="removeTransaction(${
transaction.id
})">x</button>
`;
//appenChild item到list上面
list.appendChild(item);
}
```
* updateValues
讓balance, income and expense的數值可以即時隨著新的交易更新
* 注意的點:
map reduce filter的用法
尤其是reduce的寫法比較特別
```javascript=
reduce((acc, item) => (acc += item), 0)
```
```javascript=
//update the balance, income and expense
function updateValues() {
// 這邊會創造出一個新的array裡面只有包含amount的值
const amounts = transactions.map((transaction) => transaction.amount);
// 加總所有的amount使用reduce也就是收入以及花費的的總數
//acc代表總數 item代表每筆交易的內容金額
//後方的toFixed代表取到小數點第幾位
const total = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2);
//處理income部分的數字呈現
const income = amounts
.filter((item) => item > 0)
.reduce((acc, item) => (acc += item), 0)
.toFixed(2);
//處理expense部分的數字呈現
const expense = (
amounts.filter((item) => item < 0).reduce((acc, item) => (acc += item), 0) *
-1
).toFixed(2);
//把剛剛處理好的income,total,expense放到DOM裡面更新文字
balance.innerText = `$${total}`;
money_plus.innerText = `$${income}`;
money_minus.innerText = `$${expense}`;
}
```
* addTransaction
submit後觸發的方法:
1. 加入新的交易資料
1. 把加入的資料更新到DOM(History)
1. 更新balance, income, expense數字
1. 加入資料進去localstorage
2.3.4必須呼叫其他函式來處理
最後把空格清空
```javascript=
//add transation submit後觸發的方法:加入新的交易資料
function addTransaction(e) {
e.preventDefault();
// 如果text,amount其中一個為空則跳出警告,有正確輸入的話創造一個新的transaction
if (text.value.trim() === "" || amount.value.trim() === "") {
alert("Please add a text and amount");
} else {
const transaction = {
id: generateID(),
text: text.value,
//注意這邊的amount出來必須是數字所以加上個+號
amount: +amount.value,
};
//把新加入的transaction推進去原本的trasactions裡面
transactions.push(transaction);
//把收入或是消費的新的li呈現到History上面(也就是DOM)
addTransactionDOM(transaction);
//使balance, income, expense 數字可以即時更新
updateValues();
//當加入新的交易進去transactions裡面就要呼叫這裡就會更新localstorage
updateLocalStorage();
//當輸入完成之後恢復空白
text.value = "";
amount.value = "";
}
}
```
* generateID
主要使用數學的方法來產生id給上面的transaction
```javascript=
// Generate random ID
function generateID() {
return Math.floor(Math.random() * 100000000);
}
```
* updateLocalStorage
設置鍵值對並且轉換成JSON格式推上去localstorage儲存
```javascript=
//update localstorage transactions 更新localstorage裡面的transactions資料
function updateLocalStorage() {
// 把transactions的資料用JSON字串化的格式丟上去localstorage
localStorage.setItem("transactions", JSON.stringify(transactions));
}
```
* removeTransaction
使用filter去做篩選出"沒有參數id的array"並且重新指派給transactions(原本的資料庫)
就可以刪掉放入removeTransaction裡面的物件
```javascript=
//remove transaction by id
function removeTransaction(id) {
// 使用filter去做篩選出"沒有參數的輸入id的array"並且重新指派給transactions(原本的資料庫)
// 就可以刪掉放入removeTransaction裡面的物件
transactions = transactions.filter((transaction) => transaction.id !== id);
// 刪除資料的時候也要更新localstorage
updateLocalStorage();
init();
}
```
* init
會更新所有檯面上數值以及History裡面的表格使用:
1. updateValues 更新balance, income and expense的數值
1. addTransactionDOM 更新History裡面的表格
```javascript=
function init() {
list.innerHTML = "";
//把transactions裡面的資料每一筆都執行addTransactionDOM這個function
transactions.forEach(addTransactionDOM);
updateValues();
}
```
* 事件監聽 submit
```javascript=
form.addEventListener("submit", addTransaction);
```
小補充:
`Math.floor`
函式會回傳小於等於所給數字的最大整數。
`Math.random`
回傳一個偽隨機小數 (pseudo-random),小數也稱浮點數; 介於0到1之間(包含 0,不包含1) 。
`toFixed()`
選擇性輸入數值。顯示數值至多少個小數點,範圍由0到20之間,執行時或可支援非常大範圍的數值。如果無輸入會默認做0。
### 完整程式碼:
```javascript=
const balance = document.getElementById("balance");
const money_plus = document.getElementById("money-plus");
const money_minus = document.getElementById("money-minus");
const list = document.getElementById("list");
const form = document.getElementById("form");
const text = document.getElementById("text");
const amount = document.getElementById("amount");
//這邊先設置這個dummy用作展示使用,之後會使用的是localstorage裡面存放的資料
// const dummyTransactions = [
// {
// id: 1,
// text: "Flower",
// amount: -20,
// },
// {
// id: 2,
// text: "Salary",
// amount: 300,
// },
// {
// id: 3,
// text: "Book",
// amount: -10,
// },
// {
// id: 4,
// text: "Camera",
// amount: 150,
// },
// ];
//從localstorage抓取資料使用getItem並且需要轉換格式使用JSON.parse轉回原本的物件不然原本是JSON格式不能使用
const localStorageTransactions = JSON.parse(
localStorage.getItem("transactions")
);
//判斷getItem得到的資料內容是否為空不是的話localStorageTransactions(我們存上去的內容),是空的話則放入空的[]console.log的話可以看到
let transactions =
localStorage.getItem("transactions") !== null ? localStorageTransactions : [];
console.log(transactions);
//add transation submit後觸發的方法:加入新的交易資料
function addTransaction(e) {
e.preventDefault();
if (text.value.trim() === "" || amount.value.trim() === "") {
alert("Please add a text and amount");
} else {
const transaction = {
id: generateID(),
text: text.value,
//注意這邊的amount出來必須是數字所以加上個+號
amount: +amount.value,
};
//把新加入的一筆交易推進去原本的trasactions裡面
transactions.push(transaction);
//把收入或是消費的新的li呈現到History上面(也就是DOM)
addTransactionDOM(transaction);
//使balance, income, expense 數字可以即時更新
updateValues();
//當加入新的交易進去transactions裡面就要呼叫這裡就會更新localstorage
updateLocalStorage();
//當輸入完成之後恢復空白
text.value = "";
amount.value = "";
}
}
//generate ID 取得隨機的ID
function generateID() {
return Math.floor(Math.random() * 100000000);
}
//add transactions to DOM 把收入或是消費的新的li呈現到History上面(也就是DOM)
function addTransactionDOM(transaction) {
// get sign 取得正負號(+-)
const sign = transaction.amount < 0 ? "-" : "+";
//創造新的li要放進去收入或是花費
const item = document.createElement("li");
//add class base on value 把剛創造的li加上class但是做要判斷
// 當transactions的值amount<0時放入minus,>0時放plus
item.classList.add(transaction.amount < 0 ? "minus" : "plus");
//放入的字串用template string串接放入變數transacitons.text當作物品名稱
//sign 表示正負
//Math.abs(transactions.amount) 這段用數學方法取絕對值因為前面有sing判斷正負了
item.innerHTML = `
${transaction.text} <span>${sign}${Math.abs(
transaction.amount
// 使用onlick事件並且使用動態參數``包裹住transaction的id
)}</span> <button class="delete-btn" onclick="removeTransaction(${
transaction.id
})">x</button>
`;
//appenChild item到list上面
list.appendChild(item);
}
//update the balance, income and expense 讓balance, income and expense可以即時更新隨著新的交易產生
function updateValues() {
// 這邊會創造出一個新的array裡面只有包含amount的值
const amounts = transactions.map((transaction) => transaction.amount);
// 加總所有的amount使用reduce也就是收入以及花費的的總數
//acc代表總數 item代表每筆交易的內容金額
//後方的toFixed代表取到小數點第幾位
const total = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2);
//處理income部分的數字呈現
const income = amounts
.filter((item) => item > 0)
.reduce((acc, item) => (acc += item), 0)
.toFixed(2);
//處理expense部分的數字呈現
const expense = (
amounts.filter((item) => item < 0).reduce((acc, item) => (acc += item), 0) *
-1
).toFixed(2);
//把剛剛處理好的income,total,expense放到DOM裡面更新文字
balance.innerText = `$${total}`;
money_plus.innerText = `$${income}`;
money_minus.innerText = `$${expense}`;
}
//remove transaction by id
function removeTransaction(id) {
// 使用filter去做篩選出"沒有參數的輸入id的array"並且重新指派給transactions(原本的資料庫)
// 就可以刪掉放入removeTransaction裡面的物件藉著他的id
transactions = transactions.filter((transaction) => transaction.id !== id);
// 刪除資料的時候也要更新localstorage
updateLocalStorage();
init();
}
//update localstorage transactions 更新localstorage裡面的transactions資料
function updateLocalStorage() {
// 把transactions的資料用JSON字串化的格式丟上去localstorage
localStorage.setItem("transactions", JSON.stringify(transactions));
}
//Init app 啟動app
function init() {
list.innerHTML = "";
//把transactions裡面的資料每一筆都執行addTransactionDOM這個function
transactions.forEach(addTransactionDOM);
updateValues();
}
init();
//事件監聽
form.addEventListener("submit", addTransaction);
```