[React] state練習 可長按設定的漸變進度條

這次 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 兩支。
 
下面簡單說明

1. 點選 input 左右的按鈕可以對進度條進行減或加的操作。

因為兩邊 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>
  );
};

 

2.也可以直接輸入,只能輸入1~100的整數數字。

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>

 

onChange 事件

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

 

3. input 改變,狀態條填滿程度會改變,狀態條下方數字也會改變。

主要就是用兩個 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 的部分。
 

useEffect 與監聽事件

上面講到我是根據螢幕寬度給樣式,那麼監聽寬度改變的部分當然就是用 addEventListener 了。
那問題就是...

react 的 addEventListener 要擺在什麼地方?

 
先用 vue 的慣性思惟來思考,這種東西一般會放在 mounted。
那,當我不是使用 class component 而是 hook 要怎麼做?
 
中間思考過程略,參考 官網 Effect Hook文件 ,最後採用 useEffect 。
官網說 useEffect 相似於 componentDidMount 和 componentDidUpdate。
 

使用 useEffect 的話要記得引入

import React, { useEffect } from "react";

如果是在 cdn 單頁版本的話則是要改寫成

React.useEffect
let getProgressBarWidth = () => {
  if (window.innerWidth < 351) {
    setProgressWidth("20px");
  } else {
    setProgressWidth("200px");
  }
};

useEffect(() => {
  window.innerWidth > 350
    ? setProgressWidth("200px")
    : setProgressWidth("20px");
  window.addEventListener("resize", getProgressBarWidth);
}, []);

 

4. 按鈕長按則連續加/減

這裡是整個 demo code 摸索最久的,主要還是對 useState 的不夠瞭解所致。

卡很久的原因在於 useState 是一個 非同步的 react 鉤子。

本來是有嘗試使用 async/await 處理非同步,但沒一次成功的,沒成功也就不記錄了。
 

最後是用這種作法順利的完成了長按時的連續改值

setProgressValue((progressValue) => Number(progressValue) + operate);

 

看我的 code 可能不夠直觀,這邊貼一下參考文章的 code

() => setCount(prevCount => prevCount + 1)

 

(這段話抄人家文章的)

function 的寫法讓 setCount 變成「同步」的,保證了執行順序,當你執行 setCount ,一定會等 update 完 state 後再做下一件事。

 

接下來來講長按的功能如何完成。
 
我用 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實作動畫