---
# System prepended metadata

title: Expense Tracker- 20 Web Projects With Vanilla Javascript
tags: [Javascript]

---

#  Expense Tracker- 20 Web Projects With Vanilla Javascript 

---
tags: Javascript relate
---

###### tags: `Javascript`

# 製作一個記帳簿

## 成品:

![](https://i.imgur.com/vVXIAQM.png)


[成品網址](https://vanillawebprojects.com/projects/expense-tracker/)

## 成品功能:

1.在最上方yout balance處顯示所剩餘額
2.在income、expense處顯示所以收入以及花費金額並呈現綠色以及紅色
3.History的地方會記錄並顯示出花費狀態以及輸入的內容ex 書本支出 100元並且會有刪除品項的按鈕
ps.支出會顯示紅色 收入會顯示綠色
![](https://i.imgur.com/t8bWefX.png)
4.下方有新增各種行為的欄位並且有加入的submit按鈕

## HTML


### 上CSS之前的HTML:

![](https://i.imgur.com/ukAEWT4.png)



### 程式碼:

```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設置好的樣式

![](https://i.imgur.com/vVXIAQM.png)


### 程式碼

```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 區域
處理數字呈現
![](https://i.imgur.com/Swd19mn.png)


* money-plus,money-minus區域
處理數字呈現
![](https://i.imgur.com/LnCbwGH.png)

* list區域
處理加入的新的物件
![](https://i.imgur.com/MLTc6PF.png)

* form區域
主要處理submit事件
![](https://i.imgur.com/q1v3tWp.png)

* text,amount區域
處理輸入的內容
![](https://i.imgur.com/eVuVJqe.png)


```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);
```



