iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 7
6
Modern Web

從 Hooks 開始,讓你的網頁 React 起來系列 第 7

[Day 07 - 計數器] 幫計數器設個最大最小值吧 - JSX 中條件渲染的使用

假設現在我們想要幫計數器設定最大值和最小值,其中計數器最大只能到 10,最小只能到 0 ,而且預設起始值是 5 的話,可以怎麼做呢?

其中一種做法是當數字 >= 10 的時候,就把向上的箭頭隱藏起來,當數字 <= 0 的時候,就把向下的箭頭隱藏起來,像下圖這樣:

Imgur

要把元素隱藏起來有幾種常見的做法,一種是添加 CSS 屬性把它隱藏起來,一種是讓整個 DOM 中的該元素都不要渲染出來,先讓我們來看第一種做法。

透過 CSS 隱藏元素

第一種作法蠻直覺的,就是透過改變 CSS 樣式來把該元素隱藏起來,讓使用者看不到,而可以把元素隱藏起來的 CSS 樣式主要包含 display: nonevisibility: hidden

display: none 或 visibility: hidden

這兩種 CSS 屬性都可以讓使用者看不到該元素,但差別在於 display: none 在把該元素隱藏的情況下,同時會移除該元素原本佔據在網頁上的空間 ,因此當某元素原本存在而用了 display: none 的話,會因為該元素不見而導致畫面排版「跳一下」。 visibility: hidden 同樣會把該元素給隱藏起來,但是原本該元素所佔據的空間還是會保留在那裡 ,因此不會有因為東西被移除後而畫面排版「跳一下」的情況。

關於 display: nonevisibility: hidden 的差異可以參考 [CSS] display:none和visibility:hidden的差別 @ 愛流浪的小風

這裡為了避免畫面排版會因為元素隱藏而有改變,因此使用 visibility: hidden。那麼要怎麼在 JSX 中根據條件來改變 CSS 樣式呢?

在 JSX 中使用 inline-style

這裡我們使用 inline-style 來修改 CSS 樣式,還記得在 [Day 05] 將計數器頁面改成用 JSX 來寫 中有提過使用行內樣式(inline-style)的方法,就是在 style={} 屬性後的 {} 中帶入帶有樣式的物件即可。

現在可以先開啟昨天完成的範例 Day 6 - Counter with useState @ CodePen,Fork 一份出來修改。可以先把行內樣式 visibility: hidden; 寫進去試試看:

// 向上箭頭的部分
<div
  className="chevron chevron-up"
  // 在 style={} 的 {} 中放入帶有 CSS 的樣式即可
  style={{
    visibility: 'hidden',
  }}
  onClick={() => {
    setCount(count + 1);
  }}
/>

修改的部份如下:

Imgur

同時可以把 const [count, setCount] = useState(<預設值>) 中的預設值改成 5

這時候你應該會看到畫面上顯示 5,而且向上和向下的箭頭都因為 visibility: hidden 的關係而隱藏了:

Imgur

根據條件顯示不同的行內樣式

接下來只需要判斷當 count >= 10 的時候把 hidden 套用到向上的箭頭;當 count <= 0 的時候把 hidden 套用到向下的箭頭即可。

既然 JSX 本質上就是 JavaScript,要做條件判斷就可以直接使用 JavaScript 中條件式完成,這裡會使用到 && 這種寫法,我們先來完成效果,再來說明 && 是什麼意思。

對於向上的箭頭來說,可以使用:

<div
  className="chevron chevron-up"
  // 使用 JavaScript 的 && 判斷式
  // 當 count >= 10 的時候回傳 'hidden' 否則回傳 false
  style={{
    visibility: count >= 10 && 'hidden',
  }}
  onClick={() => {
    setCount(count + 1);
  }}
/>

對向下的箭頭來說,則是:

<div
  className="chevron chevron-down"
  // 使用 JavaScript 的 && 判斷式
  // 當 count <= 0 的時候回傳 'hidden' 否則回傳 false
  style={{
    visibility: count <= 0 && 'hidden',
  }}
  onClick={() => {
    setCount(count - 1);
  }}
/>

完整的程式碼會像這樣:

Imgur

這時候的效果會像這樣。你可能會發現 只要你手速夠快,好像就可以讓數字超過 10 或低於 0

Imgur

之所以會這樣是因為當初在寫 CSS 的時候,針對 .chevron 這個元素偷懶的用了 transition: all 0.3s;, 讓所有的樣式都會有過渡(transition)的效果,因此這個按鈕在 0.3s 內並不會馬上消失,進而使得使用者還點擊的到。要解決這個問題就是去指定需要套用 transition 效果的樣式即可,因此你可以打開 CSS 的部分,在 .chevron 的地方,把 transition 改成 transition: background-image 0.3s; 這樣就可以了:

Imgur

這樣就不會多出 0.3 秒的延遲讓使用者的快手可以點擊到了。

完整的程式碼可以參考 Day 7 - Counter with useState - conditional inline-style @ CodePen

說進階不進階:&& 和 || 是什麼

在 JavaScript 中,&&|| 這種語法稱作「邏輯運算子(Expressions - Logical operator)」,而因為在 JSX 中的 {} 內只能放入表達式(expressions),而不能寫入像是 if...else... 這種陳述句(statement),因此在 React 中很常時候都會使用邏輯運算子這種語法。

在說明 && 之前,先來看一下大家比較常看到的 |||| 邏輯上是「或(or)」的意思,在 JavaScript 中常常被當做定義變數的預設值來使用,假設寫 a || b 的話,意思就是當 a 為 false(為假)時就用 b,當 a 為 true(為真)時就直接用 a

同時在 **JavaScript 中的真假值在判斷會自動作轉型,因此像是 nullNaN0、空字串(""'')、undefined 都會被轉型並判斷為「false(假)」。

以下面的例子來說:

// || 就是前面為假時去拿後面的那個

const a = 0 || 'iPhone';           // 因為 0 被轉型後為 false,所以 a 會是 'iPhone'

const b = 26900 || 24900;          // 因為 26900 會轉型為 true,所以 b 會是 26900

const c = true || '不會輪到我';     // 因為 true 為真,所以 c 是 true

const d = false || '會輪到我';     // 因為 false 為假,所以 d 是 '會輪到我'

|| 簡單來說,就是當 || 前面的值為 false(假)時,就取後面的那個當值。

至於 && 則反過來。當寫 a && b 時,當 a 為 true(為真)時,就拿後面的 b,否則拿 a。以下面的例子來說:

// && 就是前面為真時去拿後面的那個

const a = 0 && 'iPhone';          // 因為 0 被轉型後為 false,所以 a 會是 0

const b = 26900 && 24900;          // 因為 26900 轉型為 true,所以 b 會是 24900

const c = true && '不會輪到我';     // 因為 true 為真,所以 c 是'不會輪到我'

const d = false && '會輪到我';     // 因為 false 為假,所以 d 是 false

&& 簡單來說,就是當 && 前面的值為 true 時,就取後面的那個當值。

所以回到這裡寫的 visibility: count >= 10 && 'hidden',意思就會是當 count >= 10 成立時就取後面那個 ,於是就 hidden 了,否則就回傳 false,當 CSS 樣式的值是 false 時,在 React 中就不會把該樣式套到該元素上。

延伸閱讀:Logical Operators @ MDN

從瀏覽器的開發者工具中可以看到這樣的邏輯:

Imgur

補充:在 React 中,除了使用 &&|| 來進行條件渲染之外,當要做到「若 ... 則 ...,否則...」的這種功能時,則會使用三元判斷式(ternary operator),也就是 ... ? ... : ... 的這種寫法,這個部分會在後面其他實作使用到。

透過 className 隱藏元素

同樣的做法也可以套用到 className 上面,舉例來說,先在 CSS 的地方定義 .visibility-hidden 這個樣式:

.visibility-hidden {
  visibility: hidden;
}

那麼在 <Counter /> 組件中 className 的部分就可以這樣寫:

// 向上的箭頭
<div
  className={`chevron chevron-up ${count >= 10 && 'visibility-hidden'}`}
  onClick={() => {
    setCount(count + 1);
  }}
/>

// 向上的箭頭
<div
  className={`chevron chevron-down ${count <= 0 && 'visibility-hidden'}`}
  onClick={() => {
    setCount(count - 1);
  }}
/>

在不同的條件下,className 會帶入不同的值,因此可以達到一樣的效果,完整的程式碼可以參考 Day 7 - Counter with useState - conditional className @ CodePen。

把整個 DOM 都隱藏起來

前面學到的方式,是透過 CSS 樣式來把該元素隱藏起來,但使用者仍然可以透過瀏覽器的開發者工具看到該 element。如果你希望使用者透過開發者工具連 element 都看不到的話,一樣可以透過 JavaScript 的 && 來達到這個效果,只是現在條件判斷的內容要套用在 JSX 的元素上。

因此向上的箭頭會變成這樣子:

// 當 count < 10 時才出現向上的箭頭
{count < 10 && (
  <div
    className="chevron chevron-up"
    onClick={() => {
      setCount(count + 1);
    }}
  />
)}

向下的箭頭則會變這樣子:

// 當 count > 0 時才出現向下的箭頭
{count > 0 && (
  <div
    className="chevron chevron-down"
    onClick={() => {
      setCount(count - 1);
    }}
  />
)}

完整的程式碼會像這樣子,也可以參考連結 Day 7 - Counter with useState - conditional rendering @ CodePen:

Imgur

透過這種做法時,從瀏覽器的開發者工具你會看到當符合條件時,這個元素會整個從瀏覽器中被移掉:

Imgur

做法上的比較

至於實務上要使用 CSS 樣式讓使用者看不到元素就好,還是需要從 DOM 中整個把元素移除,主要端看這個功能的目的。如果使用的是 CSS 樣式,使用者就比較有機會自己去把樣式解開後,繼續往更高的值加上去,像是下圖這樣:

Imgur

因此一般來說,如果你需要比較嚴格的去控制使用者的行為,不想要使用者透過 CSS 就能簡單修改的話,那麼就把 DOM 整個移除;但如果這個功能被使用者手動打開也不會有太大影響的話,那就使用 CSS 樣式來控制畫面就好,如此會有比較好的效能和體驗。

重要補充:JSX 中標籤內如果沒有內容的話可以自己關閉

前幾天已經使用過了 JSX 和 React Hooks,但在當時為了文章的順暢度,有一些適用和不適用的情境並沒有進一步說明,但如果一整篇都打 JSX 或 React Hooks 的補充說明應該會相當無聊,因此決定利用每天的最後一點的篇幅來補充這些內容。

不知道你有沒有發現到,在原本計數器開始的樣版中,我們的 HTML 是寫這樣:

<div class="container">
  <div class="chevron chevron-up">
  </div>
  <div class="number">
    256
  </div>
  <div class="chevron chevron-down">
  </div>
</div>

可是當我們搬入到 JSX 中,向上箭頭的 <div>

<!-- 原本的 HTML -->
<div class="chevron chevron-up"></div>

<!-- JSX 中變成 -->
<div class="chevron chevron-up" />

向下箭頭的 <div> 也一樣:

<!-- 原本的 HTML -->
<div class="chevron chevron-down"></div>

<!-- JSX 中變成 -->
<div class="chevron chevron-down" />

所以最後變成像這樣:

const Counter = () => {
  return (
    <div className="container">
      <div className="chevron chevron-up" />
      <div className="number">{count}</div>
      <div className="chevron chevron-down" />
    </div>
  );
};

之所以會這樣,是因為在 JSX 中,當開始和結束的標籤(tag)之間沒有任何內容的時候,也就是該標籤內沒有子層元素時,可以把它自我關閉(self closing tag)起來,也就是在開頭的 HTML 標籤最後加上 / 即可,結尾的 HTML 標籤即可移除,舉例來說 <div></div>,因為開頭和結尾的 HTML 標籤之間沒有任何內容,因此在 JSX 中會變成 <div />

程式範例

參考資源


上一篇
[Day 06 - 計數器] 醒醒啊!為什麼一動也不動 - useState 的基本使用
下一篇
[Day 08 - 計數器] 一個不夠,給我一次來十個 - JSX 中迴圈的使用
系列文
從 Hooks 開始,讓你的網頁 React 起來30

尚未有邦友留言

立即登入留言