這次 state 的部分想做一個進度條,概念是參考這篇文章: 【React.js入門 - 12】 state 與 詳解setState語法 ,然後搭配樣式與自己想要的小功能做一些細微的調整。
雖然是小作品但用到的東西還蠻多的XDDD
主要是在長按跟 rwd 的部分有遇到一些問題。真的寫到懷疑人生XD
react 大致上是基於 JS 的功能,所以我寫著寫著就常常懷疑到底是我不懂 JS 還是只是因為我不夠懂 react。
應該是兩者都有啦(慘
功能說明
1. 點選 input 左右的按鈕可以對進度條進行減或加的操作。
2. 也可以直接輸入,只能輸入1~100的整數數字。
3. input 改變,狀態條填滿程度會改變,狀態條下方數字也會改變。
4. 按鈕長按則連續加/減
8/6 更新:
更新了上下限(其實是之前沒修到的 bug)。
進度條數字改變時用動畫效果顯示。
原是參考 iT邦的系列文 【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫,想要學習 useRef 的使用,但發現好像不用也做得到動畫的效果...就沒有用...(?)
成品參考: DEMO 單頁板 / app版(code)
app 版的 code 主要都在 ProgressBar.css / ProgressBar.html 兩支。
下面簡單說明
因為兩邊 button 功能只差在加/減,所以寫成 component
加減的部分送 type=1/-1 來判斷
operation的 +/- 是 button 外觀要顯示的字
className 那邊是配合樣式需求
<OperateButton type={-1} operatetion="-" className="btnStyle sub"/>
<OperateButton type={1} operatetion="+" className="btnStyle add"/>
let OperateButton = (props) => {
return (
<button
className={props.className}
onMouseDown={() => {
holdDown(props.type);
}}
onMouseUp={() => holdUp()}
>
{props.operatetion}
</button>
);
};
input 的部分使用自製的 state 搭配正則表示,限制輸入
<input
type="text"
value={progressValue}
placeholder="輸入1~100以內的整數數字"
onChange={(event) => {
let value = event.target.value;
if (/^[1-9]{1}\d?$/.test(value) || value === "100") {
Number(value) < Number(progressValue)
? animateCalc(-1, Number(value))
: animateCalc(1, Number(value));
} else if (value === "") {
animateCalc(-1, "");
}
}}
></input>
input 的 onChange 事件按照 React 慣例,駝峰式命名。
onChange 接一個 event 物件, input 的 value 使用 event.target.value 取得( event 名稱端看你用什麼名字來接這個物件),跟一般習慣上不太一樣,註記起來。
正則部份我感覺是寫得蠻爛的,但我...
正則就真的很爛(不解釋不辯駁)
這邊意思就首位數只能是1-9然後第二位數隨便,空白跟100另外處理。
(我怕我過一陣子看不懂我寫什麼所以假裝說明一下)
這邊判斷了使用者輸入的值是否大於當下的進度條值或者是 100/空白(0),接著用 animateCalc() 傳參做處理。
animateCalc(operate, newValue) 裡面我覺得也是寫得很爛啦XD
operate 是加/減,newValue 是記錄使用者的設定值,
function 裡面 oriValue 是記錄目前的進度條的值。
接著透過 clearInterval() 與 clearInterval() 搭配 oriValue 做運算,當oriValue 與使用者設定的 newValue 相等,就結束 function。
這邊卡住主要都是上下限跟正則的問題...
是 javascript 能力不足的問題QQQQQQQQ
let animateCalc = (operate, newValue) => {
let oriValue = progressValue === "" ? 0 : progressValue;
let animate = setInterval(() => {
if (
(progressValue > 0 && progressValue < 100) ||
(progressValue === "" && operate === 1) ||
(progressValue === 100 && operate === -1)
) {
setProgressValue(oriValue + operate);
oriValue += operate;
}
if (oriValue === newValue) {
clearInterval(animate);
}
if ((newValue === "" || newValue === 0) && oriValue === 0) {
setProgressValue("");
clearInterval(animate);
}
}, 10);
};
主要就是用兩個 div 做出外框(.barContainerStyle)跟進度條(.barStyle)的效果。
裡面的 div 的寬度用行內樣式的方式隨 input 的 value 改變。
react 的行內樣式中可以包含陳述式甚至是用函式傳回,在需要動態取值的樣式屬性上相當方便。
這邊 code 都是樣式設定,看起來很亂很雜主要是因為配合 RWD (還有我太廢的緣故)。
下面 barContainerStyle() 和 barStyle() 都是樣式,主要是寬度小於某個數字時將進度條改為直的顯示,這裡只貼 barContainerStyle() 示意。
let barContainerStyle = () => {
let sameStyle = {
width: progressWidth,
border: "solid #ccc 1px",
borderRadius: "10px",
position: "relative",
};
if (progressWidth === "200px") {
return {
...sameStyle,
height: "15px",
padding: "1px",
margin: "20px auto",
};
}
return {
...sameStyle,
height: "195px",
padding: "0",
margin: "0 20px",
};
};
<div style={barContainerStyle()}>
<div style={barStyle()}></div>
</div>
主要就是依照寬度給予兩個不同的樣式,
稍微比較特別就是寬度不同但樣式相同的部分用擴充運算符(...)置入,也就是 sameStyle 的部分。
上面講到我是根據螢幕寬度給樣式,那麼監聽寬度改變的部分當然就是用 addEventListener 了。
那問題就是...
react 的 addEventListener 要擺在什麼地方?
先用 vue 的慣性思惟來思考,這種東西一般會放在 mounted。
那,當我不是使用 class component 而是 hook 要怎麼做?
中間思考過程略,參考 官網 Effect Hook文件 ,最後採用 useEffect 。
官網說 useEffect 相似於 componentDidMount 和 componentDidUpdate。
使用 useEffect 的話要記得引入
如果是在 cdn 單頁版本的話則是要改寫成
let getProgressBarWidth = () => {
if (window.innerWidth < 351) {
setProgressWidth("20px");
} else {
setProgressWidth("200px");
}
};
useEffect(() => {
window.innerWidth > 350
? setProgressWidth("200px")
: setProgressWidth("20px");
window.addEventListener("resize", getProgressBarWidth);
}, []);
這裡是整個 demo code 摸索最久的,主要還是對 useState 的不夠瞭解所致。
卡很久的原因在於 useState 是一個 非同步的 react 鉤子。
本來是有嘗試使用 async/await 處理非同步,但沒一次成功的,沒成功也就不記錄了。
最後是用這種作法順利的完成了長按時的連續改值
看我的 code 可能不夠直觀,這邊貼一下參考文章的 code
(這段話抄人家文章的)
接下來來講長按的功能如何完成。
我用 holdDown / holdUp 兩個 function 記錄點擊滑鼠與放開滑鼠的事件。
holdDown會在第一次點擊滑鼠的時候 用變數 time 記錄時間,此後用一個 setInterval() function 每30ms檢查一次「當下時間距離首次點擊時間」是否已經超過500ms。
若是,那就判定為使用者長按,改變狀態條的值。
裡面判斷式的部分是設定上下限,這邊如果沒有判斷的話憑藉按鈕可以無限往上或往下XDDDD這樣就不好啦~
let holdDown = (operate) => {
setProgressValue((progressValue) =>
(progressValue > 0 && operate === -1) ||
(progressValue < 100 && operate === 1)
? Number(progressValue) + operate
: progressValue
);
let time = new Date();
setInterval(() => {
let nowTime = new Date();
if (nowTime.getTime() - time.getTime() > 500) {
setProgressValue((progressValue) =>
progressValue > 0 && progressValue < 100
? Number(progressValue) + operate
: progressValue
);
}
}, 30);
};
holdUp負責的就只有清除 setInterval 。
不過這邊為了傳值方便,用了偷吃步的方法。
let holdUp = () => {
for (let i = 1; i < 99999; i++) {
window.clearInterval(i);
}
};
這次為了測試動畫效果,新增了三個按鈕:
<div className="fastSetField">
<FastSetBtn value={""}></FastSetBtn>
<FastSetBtn value={50}></FastSetBtn>
<FastSetBtn value={100}></FastSetBtn>
</div>
let FastSetBtn = (props) => {
return (
<button
className={
progressValue === props.value ? "fastBtn selected" : "fastBtn"
}
onClick={() => {
props.value > progressValue
? animateCalc(1, props.value)
: animateCalc(-1, props.value);
}}
>
{props.value === "" ? 0 : props.value}
</button>
);
};
這裡有用到動態 class 的部分,由於目前為止接觸的都是 function component,所以其實基本上我算是不會用 bind this 那種方式綁定。
後來用以上方式搭配三元,也算是可以做到動態綁定 className 的效果。
差不多就是以上這些了,雖然直觀上看起來功能超簡單,不過加上 RWD 的一些設計之後也是收穫蠻多的。
樣式參考
百分比进度条PSD素材
其他參考文章
【React.js入門 - 12】 state 與 詳解setState語法
React onChange Events (With Examples)
關於 useState,你需要知道的事
JavaScript-長按及滑鼠事件
How can I clearInterval() for all setInterval()?
useEffect 的完整指南
【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫