目標
用JavaScript做一個BMI計算機,而且可以顯示過往的測驗結果。
這次一邊整理筆記一邊改寫,將當初很多段重複又累贅的地方重寫,心中很感慨,原來我也是有進步的。
預覽
難點:
1.顯示結果的圓圈會根據結果數字的狀態變色
2.列表的顏色也要跟著變
思考流程:
1.先製作一個介面,使用者可以在相應的欄位輸入身高體重,然後按下結果按鈕
2.使用js計算使用者輸入的資料,並且將這些資料以物件的形式,儲存成一個陣列來管理。
3.這個陣列的資料同時也會儲存進local storage中,不過會是以字串的形式
4.處理完的BMI結果會呈現在欄位右方的圓圈,還有BMI狀態。
5.底下會生成一個相應的列表,記載BMI指數, BMI狀態、身高、體重、檢測日期。
圓圈以及列表都會根據BMI狀態改變顏色
6.頁尾
7.結果圓圈的右下方有一個重新施測的紐,我想讓它活潑一點,所以想加一個旋轉的小動畫
8.我想要限制列表的數量,讓它不要過長。超過十五筆的話,就從最早的那一條開始刪掉。
9.我想讓列表從最新一筆紀錄開始呈現,而不是最後一筆
10.我想讓中間的列表在使用者第一次打開網頁時隱藏起來,當有紀錄時才開啟
開始動手作
1.先製作一個介面,使用者可以在相應的欄位輸入身高體重,然後按下結果按鈕
我將網頁分成三個部分,第一個是欄位區,叫header,中間要放檢測紀錄,就叫history,最後是頁尾,就叫footer。
先分好之後,就開始製作欄位。考慮到這個區塊的各個元素要相互對齊,我新增一個div區塊叫做cal,將這個區塊的display設定成flex,然後使用以下語法:
1 | align-items: center; |
我之前常常搞不清楚Items和content的區別,為了以防忘記.在這裡寫起來
items
align - items和justify-items只能作用於單行
content
若有多行元素時,就應使用align-content / justify-content
然後,要記得,flex這個屬性必須掛在外容器上!
flexbox參考網頁
接下來,因為需要用到js抓取輸入的資料,各個輸入欄位我都下了不同的class,之後按鈕也要做變化,所以也先下了一個class。
1 | var height = document.querySelector(".height"); //使用者身高 |
2.使用js計算使用者輸入的資料,並且將這些資料以物件的形式,儲存成一個陣列來管理。
為了要儲存並且更好管理將來的資料,我先宣告一個陣列:
1 | //若是資料庫中有資料,就轉成物件存進Data, 若沒有就創造一個空陣列 |
這個陣列會從資料庫獲得資料,然後把資料轉化成物件儲存在自己裡面。
為什麼不能直接宣告空陣列呢?因為我希望使用者在第二次第三次打開這個網頁時,都能看到自己以前的紀錄,所以要讀取資料庫的資料。
這個陣列中的物件,我計劃要有以下的屬性:
BMIstatus(BMI狀態), BMIindex(BMI指數), height, weight, time, color
接下來是計算使用者資料。我寫了一個function。
1 | function calculate(e){ |
一點小提醒:
1.我發現如果不寫parseInt的話,height.value傳進來的會是字串而不是數字(我想是因為,.value本來就是抓取文字欄位元素的關係),為了保險起見,兩個需要傳入的變數我都加上parseInt。順帶一提,parseInt是將字串轉換為數字的一個function。
2.為了讓數字容易運算,結果看起來乾淨漂亮,我在result的數字做了toFixed(2)的處理-讓落落長的數字取到剩下小數點後兩位。
3.switch case的使用:原本我以為只能在有特定值時才可以使用,但其實換個角度想
1 | if(result <= 18.5){ |
這個意思不就代表 當result <=18.5這個表達式 === true
嗎?
所以可以改寫為
1 | switch(true){ |
如此一來,物件中的BMIstatus, BMIindex, height, weight都有了,差一個time
在js中要取得當前時間,必須這樣寫:
1 | //獲得當前日期 |
要先呼叫一個新的Date物件,然後就可以使用下面幾個函示來取得你想要的內容。對了,getMonth()中,月份的計算是從0開始,一月(0)到十二月(11),所以要是你想要正確的數字的話,你應該要記得加一。(那為什麼日期不也從0開始……?)
1 | var BMIdata = { |
把物件中的各屬性配上之前寫的變數。
順帶一提,為了避免使用者沒有填入數字或是只寫零,我又加了一個function。
1 | resultBtn.addEventListener("click", check); |
3.這個陣列的資料同時也會儲存進local storage中,不過會是以字串的形式
處理好陣列後,我們要來把它新增進資料庫中
1 | calStorage.setItem("BMIlist", JSON.stringify(data)); |
在資料庫中新增一個叫做BMIlist的欄位,把data陣列中的物件字串化後傳進去。
這樣一來,當function calculate把資料都計算完,並且把資料儲存進陣列時,資料庫也會一起被更新。
4.底下會生成一個相應的列表,記載BMI指數, BMI狀態、身高、體重、檢測日期。
圓圈以及列表都會根據BMI狀態改變顏色
我在做的時候,因為感覺旁邊的結果圓圈比較難,所以先從列表開始做。
我寫了一個Function來處理這件事,這個function叫做updateList(data),括號中間的data實際上跟上面提到的data陣列是不同的東西,之後我們還要再呼叫一次這個Function並且在括號中傳入data陣列。我只是想提醒我自己接下來要傳進去的是什麼東西。
1 | function updateList(data){ |
在這邊最重要的做法就是,使用innerHTML來把新增的列表元素加進ul列表中。所以我先設定一個空字串(str)的變數,在跑接下來的迴圈時,會依序加進相應的內容,最後再將這個字串變成html元素塞進ul列表中。
這樣一來,列表就完成了!這個function被放在程式碼最上面(因為我希望使用者一打開網頁時就能出現列表),還有Calculate函式中(這樣有新的資料傳進資料庫時,列表就能馬上更新)。
5. 我想要限制列表的數量,讓它不要過長。超過十五筆的話,就從最早的那一條開始刪掉。
列表完成之後,在測試時發現,要是一直增加資料而沒有一個限制的話,列表就會變的落落長。要避免這件事,我決定限制陣列的長度,寫了一個function
1 | if(data.length > 15){ |
clear的功能是,把陣列的元素都跑一遍後,從最後那一筆刪掉一筆,然後用修剪過的陣列再更新一次資料庫。
之後當陣列大於15時,就呼叫這個函式,把陣列修剪掉。
看到這裡你可能覺得很奇怪,把最後那一筆刪掉的話,不就等於把最新的那筆資料刪掉嗎?
這一步驟跟下一步驟環環相扣,請看:
6.我想讓列表從最新一筆紀錄開始呈現,而不是最後一筆
網頁在這一步已經完成了有七成了吧。看著列表都能一一出現我已經很滿意,但是因為列表是由陣列的0~14開始跑,我想一想覺得不太對勁:最新的資料卻要滑到最下面才能看到,不是很不方便嗎?
為了解決這個問題,我一開始的解法是使用data.reverse()
反轉陣列。讓最後一筆能夠成為陣列第一筆。
結果我發現,因為有許多function都在執行這個陣列,一邊反轉的話,另一邊就會出錯。
最後,我發現事情可以不用這麼複雜。
1 | data.splice(0, 0, BMIdata); |
x是你想插入的位置,y是你想從那位置之後刪掉的資料數目,item是你想插進去的資料。
使用splice,讓新的資料從第零筆插入,成為第零筆就好,最後一筆成為第一筆,原本的倒數第二成為第二筆,如此往後推進。
也因此在上一步驟中的迴圈,我把最後一筆刪掉,其實也等於是把最早的刪掉。
其實我在這裡犯過不少愚蠢的錯……
7.處理完的BMI結果會呈現在欄位右方的圓圈,還有BMI狀態。
不知道為什麼,我一直很害怕這個部分,所以我拖到很後面才做。
這一步中,我們要把原本的結果按鈕刪掉,然後新增一個會變色的圓圈圈,圈圈中間有BMI指數,還有一個重新開始的小按鈕,圓圈旁邊跟著一段顯示結果的文字。
先寫一個function,叫做switchBTN(){}
接著來刪掉原本的按鈕。
1 | var getParent = resultBtn.parentNode; |
我先取得按鈕的父元素(ParentNode),然後移除按鈕(remoceChild),接著新增一個div元素,給他一個Class名稱(我已經預先在css中設定好外觀了)。
接下來,因為這個圓圈有一些必須隨著資料變動的要素,所以就再跑一次迴圈,抓取各元素中的特定值(我要找的是bmi指數以及狀態)
接下來主要分兩部分,先寫個函式把接下來會重複的動作統一處理
1 | //這個函式其實一直在做一樣的事, 就是根據傳進來的資料, 還有這筆資料的各種屬性來渲染不同的html |
然後是條件判斷
1 | switch (data[0].BMIstatus){ |
這個功能基本上就完成了。
在這裡的做法跟做列表有點像,都使用到一個把字串塞進空字串中,然後創造html元素的做法。不過在這裡我沒有跑for迴圈,而是用switch來判斷當下的狀態,因為前面我把最新的資料都變成第零筆,所以我只要判斷第零筆的BMI狀態就好。
舉上面的例子,假如第零筆(也就是最新這一筆)的BMI狀態是過輕的話,我就在str中放進兩個p段落(一個是指數,一個是小小的bmi字樣)和一個a連結(重新開始的按鈕),這幾個元素的外觀我都在css中設定好了。
把str的HTML元素用innerHTML的方法設定好之後,接下來是根據該狀態來變更顏色。想要用js變更css樣式的話,就先把要變更的元素抓取過來,然後加上.style.color/ .border/….等等css語法。
a連結也設定好之後,使用appendChild把這些新增好的元素加進剛剛的Parent node之下,就可以了。
別忘了,還有旁邊的狀態要寫!
8.頁尾
頁尾只有一個Logo要放,但是原本給的logo是透明的,不是這個顏色。
我使用了css中的filter來調整,可以上codepen找css filter。
9.結果圓圈的右下方有一個重新施測的紐,我想讓它活潑一點,所以想加一個旋轉的小動畫
1 | .show a:hover{ |
我想做的是:圖案旋轉一圈後,停止一下,再開始新的loop。
除了要把iteration設定成infinite(無限)之外,因為要有間隔,所以50%和100%時做的是一樣的事,讓它停在原地。
10.我想讓中間的列表在使用者第一次打開網頁時隱藏起來,當有紀錄時才開啟
到這裡網頁都完成了,但是當使用者第一次打開這個網頁時,中間的history區域因為沒有內容,會一片空白,我覺得這樣很奇怪,所以想要先把它隱藏起來,等到有資料進入時再出現。
沒想到我在這裡也花了很久!
我一開始的做法是:把history區塊整個刪掉,先做條件判斷(當Data的長度>=1時),然後再一一新增回來。但是因為data有兩筆時也會大於一,有三筆時也會大於一,也就是說,只要有新的資料傳進來,這個條件判斷就會永遠成立,那就等於沒有用了!
最後才靈光一閃
his.style.display = "none";
我在程式馬最上面加了這一行,代表:「在網頁打開時,history列表是關起來的」,然後在updateList函式中,我才把它打開,這樣在生成列表的時候,history區塊也會跟著出現了。
心得
其實距我做完這份作業已經過去幾個月了,那時候剛碰js,做得小心翼翼又戰戰兢兢的,還老是很悲觀地覺得我一定不行。能夠完成真的很開心!而且老實說,我那時的程式碼超冗長,哈哈,這次一邊整理筆記一邊改寫,將當初很多段重複又累贅的地方重寫,心中很感慨,原來我也是有進步的。算是在最近有些停滯又迷惘的日子提供一點新的動力。