ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت — از صفر تا صد
ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت یکی از بهترین مثالهای کاربردی برای آشنایی با این زبان برنامهنویسی محسوب میشود. ما هرگز یک زبان برنامهنویسی را به خوبی یاد نخواهیم گرفت، مگر این که شخصاً چیزی را با آن بسازیم. بنابراین با ما همراه باشید تا با بررسی یک مثال عملی با این زبان بیشتر آشنا شویم. در این مقاله فرض شده است که خواننده محترم دانشی ابتدایی از HTML ،CSS ،Bootstrap 4 و جاوا اسکریپت دارد.
مقدمهای بر ساخت اپلیکیشن مدیریت هزینه با جاوا اسکریپت
ما در این راهنما قصد داریم یک اپلیکیشن جاوا اسکریپت طراحی کنیم که با استفاده از آن بتوانیم میزان مصارف هزینهای یک فرد را در طی بازههای مشخصی اندازهگیری کرده و بفهمیم که چه مقدار از درآمد فرد صرف پسانداز، چه مقدار صرف سرمایهگذاری و چه مقدار هزینه شده است.
طراحی رابط کاربری اپلیکیشن مدیریت هزینه
نخستین مرحلهی کار، طراحی یک UI برای اپلیکیشن مدیریت هزینه است. شما میتوانید از ایدههای خود استفاده کنید و یا سری به وبسایتهای Dribbble یا Behance بزنید تا ایدهای از طراحیهای مختلف به دست آورید. برای ایجاد UI میتوانید از نرمافزار Figma استفاده کنید. دلیل این که کار را از طراحی UI آغاز میکنیم این است که کدنویسی سادهتر است. انتخاب یک طراحی موجب میشود که تفکر شما نظم و سازماندهی پیدا کند و در نتیجه سرعت توسعه اپلیکیشن افزایش مییابد.
کدهای HTML موردنیاز
در ادامه کار خود را با ایجاد یک فایل index.html در پوشه پروژه آغاز میکنیم. ابتدا باید بوتاسترپ را به پروژه خود اضافه کنید. به این منظور میتوانید از راهنماییهای مستندات رسمی آن (+) بهره بگیرید. همچنین ما از فونت Open Sans برای طراحی ظاهر بهتر استفاده میکنیم. این گزینه کاملاً اختیاری است.
اکنون که اسکریپتها آماده است، اقدام به تحلیل طراحی خود میکنیم. برای شروع باید دو کانتینر داشته باشیم که کانتینر آبی در سمت چپ حدود 40% فضای صفحه را اشغال و کانتینر سمت راست بقیه صفحه را پر میکند. پیادهسازی این حالت به کمک Bootstrap Grid کار آسانی است. در ادامه کار خود را با کانتینر آبی سمت راست آغاز میکنیم. کد HTML ما به شکل زیر است:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>Expense Manager</title>
6 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
7 integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
8 <link rel="stylesheet" href="style.css">
9 <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
10 <link href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css" rel="stylesheet">
11 <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
12 integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
13 crossorigin="anonymous"></script>
14 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
15 integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
16 crossorigin="anonymous"></script>
17 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
18 integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
19 crossorigin="anonymous"></script>
20 <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js" crossorigin="anonymous"></script>
21
22</head>
23<body>
24<div class="row">
25 <div class="col-4 left-container">
26 <div class="month-container">
27 <div class="header fs-white">Your Budget</div>
28 <div id="current-month" class="sub-text fs-white"></div>
29 <div class="budget-container p-2 mt-4">
30 <span id="month-budget" class="month-amount">₹ 0</span>
31 </div>
32 </div>
33
34 <div class="chart-container">
35 <canvas id="expense-chart"></canvas>
36 </div>
37 </div>
38 <div class="col-8 right-container">
39 <div class="calc-container">
40 <div class="header fs-dark-grey">Track Your Budget</div>
41 <div class="dropdown open">
42 <button class="btn btn-info dropdown-toggle"
43 type="button" id="dropdownMenu3" data-toggle="dropdown"
44 aria-haspopup="true" aria-expanded="false">
45 Expense Type
46 </button>
47 <div class="dropdown-menu">
48 <a class="dropdown-item" id="type-savings">Savings</a>
49 <a class="dropdown-item" id="type-expense">Expense</a>
50 <a class="dropdown-item" id="type-investment">Investment</a>
51 </div>
52 </div>
53 <div class="mt-3 tracking-text text-capitalize sub-text bottom-border">Tracking Savings ?</div>
54
55 <div class="row mt-4">
56 <div class="col-7">
57 <input class="form-control input-expense-description" type="text" placeholder="Description">
58 </div>
59 <div class="col-4">
60 <input class="form-control input-expense-value" type="number" placeholder="Value">
61 </div>
62 <div class="col-1">
63 <button type="button" class="btn btn-success btn-submit-expense">✓</button>
64 </div>
65 </div>
66 <div class="expense-list mt-4">
67
68 </div>
69
70 </div>
71 </div>
72</div>
73
74<script src="app.js"></script>
75</body>
76</html>
کد درون تگ body کارهای زیر را انجام میدهد:
- یک کانتینر به نام left-container ایجاد میکند که شامل month-container است.
- این کانتینر دارای title و current month و همچنین budget واقعی برای ماه مربوطه است.
همچنین از پول محلی استفاده خواهیم کرد.
سپس به سراغ بخش افزودن پسانداز یا مصارف میرسیم. این کانتینر شامل یک عنوان، یک منوی بازشدنی برای انتخاب نوع هزینه و چند فیلد ورودی برای ثبت هزینهها و همچنین یک لیست برای نمایش مداخل بر اساس تاریخ است. این کد زیر left-container قرار دارد.
بدین ترتیب فایل HTML ما آماده است. اینک یکی از سه فایل مورد نیاز اپلیکیشن خود را تکمیل کردهایم. در ادامه باید این فایل HTML را استایلبندی کنیم. آیا متوجه فایل style.css که در تگ <head> ایمپورت کردهایم شدید؟ در ادامه صفحه وب خود را با یک استایل ساده و زیبا استایلبندی میکنیم.
کدهای CSS مورد نیاز
از آنجا که در این پروژه از بوتاسترپ استفاده کردهایم، اغلب بخشهای CSS به صورت خودکار برای ما ایجاد شدهاند. بوتاسترپ بخش زیادی از موقعیتیابی و طراحی را انجام میدهد و از این رو کار زیادی برای انجام نمانده و صرفاً باید حاشیهها، اندازههای فونت و رنگ و ظاهر صفحه وب را تغییر دهیم.
فایل CSS ما به شکل زیر است:
1* {
2 margin: 0;
3 padding: 0;
4 box-sizing: border-box;
5}
6
7.clearfix::after {
8 content: "";
9 display: table;
10 clear: both;
11}
12
13body {
14 color: #555;
15 font-family: Open Sans;
16 font-size: 16px;
17 position: relative;
18 height: 100vh;
19 font-weight: 400;
20}
21
22.left-container {
23 height: 100vh;
24 background-image: linear-gradient(#0277BD, #03A9F4);
25 background-size: cover;
26 background-position: center;
27 position: relative;
28}
29
30.right-container {
31 height: 100vh;
32 width: 100%;
33 position: relative;
34}
35
36.header {
37 font-weight: 700;
38 font-size: 36px;
39}
40
41.sub-text {
42 font-size: 22px;
43 font-weight: 400;
44}
45
46.month-container {
47 padding-top: 25%;
48 padding-left: 5%;
49 padding-right: 5%;
50}
51
52.calc-container {
53 padding-top: 12%;
54 padding-left: 5%;
55 padding-right: 5%;
56}
57
58.fs-white {
59 color: #ffffff;
60}
61
62.fs-dark-grey {
63 color: #4e4e4e;
64}
65
66.budget-container {
67 display: inline-block;
68 background: #ffffff;
69 border-radius: 8px;
70 box-shadow: 0 6px 4px #000000;
71}
72
73.month-amount {
74 font-size: 36px;
75 font-weight: 700;
76}
77
78.bottom-border {
79 border-bottom: 1px solid #00446D;
80}
81
82.expense-row {
83 padding: 10px;
84}
85
86.expense-date {
87 color: #077CC1;
88}
89
90.expense-text {
91 color: #077CC1;
92}
93
94.expense-list {
95 overflow-y: scroll;
96}
97
98.fs-15 {
99 font-size: 15px;
100}
101
102.expense-value {
103 text-align: end;
104}
105
106.expense-saving {
107 color: #039300;
108}
109
110.expense-cost {
111 color: #E40000;
112}
113
114.expense-investment {
115 color: #f48803;
116}
117
118#expense-chart {
119 margin: 20% 0;
120}
121
122.btn-submit-expense {
123 border-radius: 50%;
124}
125
126.currency-select {
127 margin: 0 4%;
128}
129
130.selected-currency {
131 color: #ffffff;
132 font-size: 12px;
133 font-weight: 700;
134 margin-top: 1%;
135}
یکی از مشخصههای جالب CSS، قابلیت ایجاد پسزمینههای گرادیانی است. این قابلیت به طور خاص در مواردی به کار میآید که بخواهیم کمی رنگ به پروژه خود اضافه کنیم.
اگر در فایل فوق CSS مربوط به left-container را بررسی کنید، میبینید که مشخصه پسزمینه تابع liner-gradient را میگیرد که یک جهت و رنگهای مورد استفاده در گرادیان را میپذیرد.
1background: linear-gradient(direction, colour-1, colour-2, …);
جهت گرادیان دارای مقدار پیشفرض top-to-bottom است.
با این حال برخی اوقات یک گرادیان دقیقاً مطابق آن چه در ذهن شما است عمل نمیکند. در این موارد میتوانید از مشخصه radial-gradient در CSS بهره بگیرید.
به این ترتیب کار ما در بخشهای HTML و CSS پروژه پایان یافته است. در ادامه روی فایل app.js کار میکنیم و تعاملها و کارکردهای پروژه را به آن اضافه میکنیم.
توسعه اپلیکیشن مدیریت هزینه با جاوا اسکریپت
جاوا اسکریپت مهمترین بخش این پروژه است. بخشهای HTML و CSS شیوه نمایش ظاهر پروژه مدیریت هزینه را تعیین کردند، اما اینک باید روی منطق اجرایی آن کار کنیم. فایل app.js جایی است که همه اتفاقات مهم رخ میدهند.
پیش از آغاز، زمانی را صرف تأمل در این خصوص بکنید که چه کارکردهایی را باید به فایل app.js اضافه کنیم. در حال حاضر زمانی که یک نوع هزینه را انتخاب میکنیم یا یک توضیح و مقدار هزینه را اضافه کرده و روی دکمهها کلیک میکنیم، هیچ اتفاقی نمیافتد. همان طور که حدس میزنید باید eventListeners را اضافه کرده و ماه جاری را نمایش دهیم.
ابتدا باید تابعهای مختلفی بنویسیم. برخی از این تابعها صرفاً مسئول مدیریت UI و منطق هستند و بودجه ماهانه را محاسبه میکنند. این تابعها را کنترلر مینامیم. پروژه ما 3 کنترلر خواهد داشت:
- کنترلر اصلی: این کنترلر تعاملهای اولیه و کلی اپلیکیشن مدیریت هزینه را کنترل میکند.
- کنترلر UI: عناصر UI از قبیل تغییر دادن رنگ فونت و ایجاد لیست مداخل و غیره را کنترل میکند.
- کنترلر هزینه: بخش محاسبه را کنترل میکند و مقادیر کاربر را گرفته و بودجه ماه جاری را محاسبه میکند.
به این ترتیب باید یک فایل به نام app.js بسازیم. تگ اسکریپت مربوطه را به انتهای فایل index.html و درست پس از تگ پایانی body اضافه کردهایم.
1<script src="app.js"></script>
فایل app.js ما به صورت زیر خواهد بود:
1let ExpenseController = (() => {
2 let total = 0, savings = 0, expenses = 0, investments = 0;
3
4 return {
5 inputEntry(userInput) {
6 if (userInput['expenseType'] === 'savings') {
7 savings += userInput['value'];
8 total += userInput['value'];
9 }
10 if (userInput['expenseType'] === 'investment') {
11 investments += userInput['value'];
12 total -= userInput['value'];
13 }
14 if (userInput['expenseType'] === 'expense') {
15 expenses += userInput['value'];
16 total -= userInput['value'];
17 }
18 },
19
20 getSavingsData() {
21 return savings;
22 },
23
24 getExpensesData() {
25 return expenses;
26 },
27
28 getInvestmentData() {
29 return investments;
30 },
31
32 getTotalData() {
33 return total;
34 }
35 }
36
37})();
38
39let UIController = (() => {
40 let expenseType = 'savings';
41
42 let HTMLStrings = {
43 inExpenseDescription: '.input-expense-description',
44 inExpenseValue: '.input-expense-value',
45 btnSubmitExpense: '.btn-submit-expense',
46 expenseList: '.expense-list',
47 currentMonth: '#current-month',
48 typeExpense: '#type-expense',
49 typeSavings: '#type-savings',
50 typeInvestment: '#type-investment',
51 trackingText: '.tracking-text',
52 expenseChart: '#expense-chart',
53 monthBudget: '#month-budget'
54 };
55
56 return {
57 numberFormat(number) {
58 return Intl.NumberFormat('en-IN').format(number);
59 },
60 showCurrentMonth() {
61 let now, month, year, months;
62
63 now = new Date();
64 month = now.getMonth();
65 year = now.getFullYear();
66 months = [
67 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October',
68 'November', 'December'
69 ];
70 document.querySelector(HTMLStrings.currentMonth).textContent = months[month] + " " + year;
71 },
72
73 getHTMLStrings() {
74 return HTMLStrings;
75 },
76
77 setExpenseType(type) {
78 console.log('here', type);
79 this.expenseType = type;
80 let emoji ="?";
81 if (type === 'savings') {
82 emoji ="?";
83 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
84 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-warning');
85 }
86 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
87 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-danger');
88 }
89 if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
90 document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-success');
91 }
92 }
93
94 if (type === 'expense') {
95 emoji = "?";
96 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
97 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-warning');
98 }
99 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
100 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-success');
101 }
102 if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
103 document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-danger');
104 }
105 }
106 if (type === 'investment') {
107 emoji = "?";
108 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-danger')) {
109 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-danger');
110 }
111 if (document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-success')) {
112 document.querySelector(HTMLStrings.btnSubmitExpense).classList.remove('btn-success');
113 }
114 if (!document.querySelector(HTMLStrings.btnSubmitExpense).classList.contains('btn-warning')) {
115 document.querySelector(HTMLStrings.btnSubmitExpense).classList.add('btn-warning');
116 }
117 }
118
119 document.querySelector(HTMLStrings.trackingText).textContent = "Tracking " + type + " " + emoji;
120
121 },
122
123 getUserExpenseInput() {
124 return {
125 description: document.querySelector(HTMLStrings.inExpenseDescription).value,
126 value: parseInt(document.querySelector(HTMLStrings.inExpenseValue).value),
127 date: new Date().toLocaleDateString(),
128 expenseType: this.expenseType ? this.expenseType : 'savings'
129 }
130 },
131
132 addListItem (inputObj) {
133 let html, element;
134 element = HTMLStrings.expenseList;
135
136 if (inputObj['expenseType'] === 'savings') {
137 html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-saving fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
138 } else if (inputObj['expenseType'] === 'expense') {
139 html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-cost fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
140 } else if (inputObj['expenseType'] === 'investment') {
141 html = '<div class="bottom-border"> <div class="row expense-row"><div class="col-2 expense-date fs-15">' + inputObj['date'] + ' </div><div class="col-8 expense-text fs-15"> ' + inputObj['description'] + ' </div><div class="col-2 expense-value expense-investment fs-15"> ₹ ' + this.numberFormat(inputObj['value']) + ' </div></div>'
142 }
143
144 // Add the new element
145 document.querySelector(element).insertAdjacentHTML('beforeend', html);
146
147 // Clear the input fields after adding element
148 document.querySelector(HTMLStrings.inExpenseValue).value = "";
149 document.querySelector(HTMLStrings.inExpenseDescription).value = "";
150 },
151
152 updateOverallTotal(totalValue) {
153 document.querySelector(HTMLStrings.monthBudget).textContent = "₹ " + this.numberFormat(totalValue);
154
155 if (totalValue > 0) {
156 if (document.querySelector(HTMLStrings.monthBudget).classList.contains('expense-cost')) {
157 document.querySelector(HTMLStrings.monthBudget).classList.remove('expense-cost');
158 }
159 document.querySelector(HTMLStrings.monthBudget).classList.add('expense-saving');
160 } else {
161 if (document.querySelector(HTMLStrings.monthBudget).classList.contains('expense-saving')) {
162 document.querySelector(HTMLStrings.monthBudget).classList.remove('expense-saving');
163 }
164 document.querySelector(HTMLStrings.monthBudget).classList.add('expense-cost');
165 }
166 },
167
168 displayChart(savings = 0, expenses = 0, investments = 0) {
169 let ctx = document.querySelector(HTMLStrings.expenseChart);
170 let expenseChart = new Chart(ctx, {
171 type: 'doughnut',
172 data: {
173 labels: ['Savings', 'Expenses', 'Investments'],
174 datasets: [{
175 data: [savings, expenses, investments],
176 backgroundColor: [
177 'rgba(32, 137, 56, 1)',
178 'rgba(255, 84, 98, 1)',
179 'rgba(255, 206, 86, 1)'
180 ],
181 borderWidth: 0.5
182 }]
183 },
184 options: {
185 legend: {
186 labels: {
187 fontColor: 'white'
188 }
189 }
190 }
191 });
192 }
193 }
194})();
195
196((UIController, ExpenseController) => {
197
198 let HTMLStrings = UIController.getHTMLStrings();
199 let setupEventListeners = () => {
200 document.querySelector(HTMLStrings.btnSubmitExpense).addEventListener('click', addExpense);
201 document.querySelector(HTMLStrings.typeExpense).addEventListener('click', () => {
202 setExpenseType('expense')
203 });
204 document.querySelector(HTMLStrings.typeInvestment).addEventListener('click', () => {
205 setExpenseType('investment')
206 });
207 document.querySelector(HTMLStrings.typeSavings).addEventListener('click', () => {
208 setExpenseType('savings')
209 });
210 };
211
212 let setExpenseType = (type) => {
213 UIController.setExpenseType(type);
214 }
215
216 let addExpense = () => {
217 let input = UIController.getUserExpenseInput();
218 console.log(input);
219
220 if (input.description !== "" && !isNaN(input.value) && input.value > 0) {
221 console.log('Adding item');
222 UIController.addListItem(input);
223 ExpenseController.inputEntry(input);
224 UIController.updateOverallTotal(ExpenseController.getTotalData());
225 UIController.displayChart(ExpenseController.getSavingsData(), ExpenseController.getExpensesData(),
226 ExpenseController.getInvestmentData());
227 }
228 }
229
230 let init = () => {
231 console.log('Initializing...');
232 setupEventListeners();
233 UIController.showCurrentMonth();
234 }
235
236 init();
237
238})(UIController, ExpenseController);
در ادامه بخشهای مختلف این فایل را توضیح میدهیم.
عبارتهای تابع با اجرای بیدرنگ (IIFE)
IIFE-ها تابعهایی در جاوا اسکریپت هستند که به محض تعریف شدن، اجرا میشوند. این تابعها در زمان اجرای فایل اسکریپت بیدرنگ فراخوانی میشوند. از این رو نیازی به تابع دیگر برای فراخوانی شدن ندارند. تا پیش از ES6 شکل آنها چنین بود:
1(function(){
2 console.log('Welcome, this is an IIFE!');
3})();
در ES6 شکل آنها چنین است:
1(() => {
2 console.log('Welcome, this is an IIFE!')
3})();
در ادامه منطق تجاری اپلیکیشن خود را بررسی میکنیم. نخست کار را با بررسی کنترلرها آغاز میکنیم.
برای من پرش داره اصلا نمیاره