Expense Tracker
使用到的技能樹
HTML
- label
CSS
- first-of-type
- input [type=" "]
- outline
JS
- jQuery
- map(),filter(),reduce()
- math.random()
- math.floor()
- LocalStorage
簡介
Demo
可以紀錄支出與收入的記帳簿。
HTML
上半部架構
<h2>Expense Tracker</h2>
<div class="container">
<h4>Your Balance</h4>
<h1 id="balance">$0.00</h1>
<div class="inc-exp-container">
<div>
<h4>Income</h4>
<p id="money-plus" class="money plus">+$0.00</p>
</div>
<div>
<h4>Expense</h4>
<p id="money-minus" class="money minus">-$0.00</p>
</div>
下半部架構
js 動態增加 li
<h3>History</h3>
<ul id="list" class="list">
<li class="minus">
Cash <span>-$400</span><button class="delete-btn">x</button>
</li>
</ul>
表單輸入框與 submit 按鈕
<h3>Add new transaction</h3>
<form id="form">
<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>
CSS
設立常用顏色變數&起手式
: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 {
background-color: #f7f7f7;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: sans-serif;
}
快速置中
30px 為上下預留的距離,左右使用 auto,讓網頁自己計算就會達成置中的效果。
.container {
margin: 30px auto;
width: 350px;
}
first-of-type選擇器
這邊的效果為,選擇到 income 的 div,所以使用到 first-of-type,然後在使用 border-right。
.inc-exp-container > div:first-of-type {
border-right: 1px solid #dedede;
}
用 type 選擇 input
可以快速篩選元素,去加上樣式
input[type='text'], input[type='number'] {
border: 1px solid #dedede;
border-radius: 2px;
display: block;
font-size: 16px;
padding: 10px;
width: 100%;
}
opacity 來預設消失元素,使用偽元素來條件式顯示
.delete-btn {
/* 預設消失 */
opacity: 0;
/* 0.3 秒後顯示出,增加使用者體驗 */
transition: opacity 0.3s ease;
}
/* 滑鼠滑入後來顯示 */
.list li:hover .delete-btn {
opacity: 1;
}
去除點選的預設外框
瀏覽器都會有個預設外框, outline 設定為 0,就可以取消了
.btn:focus,
.delete-btn:focus {
outline: 0;
}
以下為此專案的完整 CSS code :
: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 {
background-color: #f7f7f7;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: sans-serif;
}
.container {
margin: 30px auto;
width: 350px;
}
h1 {
/* 字首 1px */
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;
justify-content: space-between;
margin: 20px 0;
}
.inc-exp-container>div {
flex: 1;
text-align: center;
}
.inc-exp-container>div:first-of-type {
border-right: 1px solid #dedede;
}
.money {
font-size: 20px;
letter-spacing: 1px;
margin: 5px 0;
}
.money.plus {
color: #2ecc71;
}
.money.minus {
color: #c0392b;
}
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:focus {
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;
position: relative;
padding: 10px;
margin: 10px 0;
}
.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;
}
.list li:hover .delete-btn {
opacity: 1;
}
JS
練習 jQuery ,以下將 JS 轉換為 jQuery
設定變數
原生 JS
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");
jQuery
$('#balance')
$('#money-plus')
$('#money-minus')
$('#list')
$('#form')
$('#text')
$('#amount')
新增資料到 list 上測試
const dummyTransactions = [
{ id :1 , text :'Flower',amount :12 },
{ id :2 , text :'box', amount : -500 },
]
函式的概念
- 判斷輸入的符號 (sign) 為正或負數,產生新的li
- 判斷輸入的值為正或負數(item)
- 判斷完後,將 item 放到 list 上
Math.abs()
會傳回絕對值
// 新增陣列,測試addTransactionDOM()是否可以運行
const dummyTransactions = [
{ id :1 , text :'Flower',amount :12 },
{ id :2 , text :'box', amount : -500 },
]
let transactions = dummyTransactions;
// Add transactions to DOM list
// 將動態資料轉換成 li 放到 list
function addTransactionDOM(transaction) {
// Get sign
// if 判斷式縮寫 `?(true 執行):(false 執行)`
// if(transaction.amount < 0) sign 就是 -
const sign = transaction.amount < 0 ? "-" : "+";
// 產生新的 li 元素
const item = document.createElement("li");
// Add class based on value
// 把產生新的 li 元素加上 class
// 判斷當 transactions的value,amount < 0 放minus,> 0 放plus
item.classList.add(transaction.amount < 0 ? "minus" : "plus");
// 將資料動態讀入,使用${}讀取變數。
item.innerHTML = `
${transaction.text} <span>${sign}${Math.abs(transaction.amount)}
</span> <button class = "delete-btn" >x</button>
`;
//使用appenChild,將item接在list
list.appendChild(item);
}
init()
使 transactions (預先做好的資料),每一筆都丟到 addTransactionDOM()。
預設 listinnerHTML = ""為空值,不設定的話會出現HTML 裡預設的 Cash -400。
原生Js
//Init app
function init() {
// 預設 list 為空,不設定的話會出現 HTML 裡設定 Cash 的 -400 元
list.innerHTML = "";
// 將 transactions 裡面的每一筆資料
// 都執行 addTransactionDOM()
transactions.forEach(addTransactionDOM);
}
init();
設定總金額,支出,收入的元素
map()
需要回傳一個值,他會透過函式內所回傳的值組合成一個陣列。reduce()
方法像是一個累加器,可以陣列中每項元素(由左至右)傳入回呼函式,將陣列化為單一值。filter()
會回傳一個陣列,其條件是 return 後方為 true 的物件,適合用在搜尋符合條件的資料(本範例都是用來搜尋符合的資料)。
語法:
.reduce((acc, item) => (acc += item), 0)
最後面的 0,是第一次呼叫 callback 時,要傳入的累加器的初始值。若沒有提供初始值, 則原陣列的第一個元素將會被當作初始的累加器。 但最好是都給一個初始值,必免有 bug,目前在這邊的程式不會影響到。
updateValues()
- 使用
map()
取得新函數,函數裡有每一個函數的 amout 數值(未加總)。 - 使用
reduce()
加總每一個 amout 數值,放到 total 變數, - 以 income 做範例,所以先使用
filter()
過濾資料,取得大於0的數。 - 再來,用
refuce()
加總。 toFixed()
取的小數第二位。- 把處理好的 income,total, expense 放到 DOM 裡面更新文字。
// Update the balance,income and expense
function updateValues() {
// 使用 map 方法回傳一個新 Array,他的內容是舊 Array 的 amount 值
const amounts = transactions.map((transaction) => transaction.amount);
// 用 reduce 方法,加總所有值的 amount,並 toFixed(2) 取到小數第二位
// acc += item -> acc = acc + item
const total = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2);
const income = amounts
// .filter取得大於零的值
.filter((item) => item > 0)
// .reduce將 >0 的值 (income) 加總
.reduce((acc, item) => (acc += item), 0)
.toFixed(2);
const expense = (
amounts
// .filter 取得大於零的值
.filter((item) => item < 0)
// .reduce 將 <0 的值 (expense) 加總
.reduce((acc, item) => (acc += item), 0) *
-1
).toFixed(2);
// 將動態更新好的 income, total, expense 放到DOM裡面更新文字
balance.innerText = `$${total}`;
money_plus.innerText = `$${income}`;
money_minus.innerText = `$${expense}`;
}
// Init app
function init() {
list.innerHTML = "";
transactions.forEach(addTransactionDOM);
// 要呼叫 updateValues 函式
updateValues();
}
jQuery
- innerText =改成.text()
function updateValues() {
const amounts = transactions.map((transaction) => transaction.amount);
const total = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2);
const income = amounts
.filter((item) => item > 0)
.reduce((acc, item) => (acc += item), 0)
.toFixed(2);
const expense = (
amounts.filter((item) => item < 0).reduce((acc, item) => (acc += item), 0) *
-1
).toFixed(2);
// balance.innerText = `$${total}`;
$("#balance").text(`$${total}`);
$("#money-plus").text(`$${income}`);
$("#money-minus").text(`$${expense}`);
}
generateID()
Math.floor()
會回傳小於等於所給數字的最大整數,
簡單來說就是,小數無條件捨去到比自身小的最大整數。
Math.random()
會隨機產生出 0~1 之間的小數,出來的值永遠不會大於 1。
挺方便的隨機產生器
Math.random(); //0.8961082300942438
Math.random(); //0.009676286758744546
如果將Math.random()
產生出來的數,
乘上 2 就會得到 0-2 之間的小數,乘上 3 就會得到 0-3 之間的小數,
再使用Math.floor()
就會達成無條件取整數。
Math.floor(Math.random()*2); //回傳0或1
Math.floor(Math.random()*3); //回傳0或1或2
Math.floor(Math.random()*5); //回傳0或1或2或3或4
Math.floor(Math.random()*50); //回傳0或1或2或3...或49
- 取0-100000000的亂數當作ID。
// Generate random ID
function generateID() {
return Math.floor(Math.random() * 100000000);
}
刪除選取的代辦事項
<button class="delete-btn" onclick="removeTransaction(${transaction.id})">
假如我們點擊A列表,那A列表的 transaction.id
會傳進去 removeTransaction()
函式裡。
再使用filter()
過濾,當原始的 transaction.id
們不等於A列表的 id ,就會被回傳。
所以A列表沒有被回傳,再使用init()
,就被刪除了。
如果沒有呼叫init()
,畫面是不會重新渲染的,
所以最後要記得加上init()
函數。
//Remove transaction by ID
function removeTransaction(id) {
transactions = transactions.filter((transaction) => transaction.id !== id);
init();
}
submit 監聽事件
原生 Js
form.addEventListener("submit", addTransaction);
jQuery
.addEventListener("submit",xxx)
轉成.submit
$("#form").submit(addTransaction);
設定 Local Storage 存取資料
從 Localstorage 抓取資料使用 getItem()
需要轉換格式使用JSON.parse
判斷getItem()
得到的資料內容是否為空。
const localStorageTransactions = JSON.parse(
localStorage.getItem("transaction")
);
let transactions =
localStorage.getItem("transaction") !== null ? localStorageTransactions : [];
更新 Localstorage 裡面的資料。
// Update local storage transactions
function updateLocalStorage() {
localStorage.setItem("transactions", JSON.stringify(transactions));
}
在addTransaction()
,removeTransaction()
函數裡需要呼叫updateLocalStorage()
,才能夠更新資料。