動手擼元件系列 —— 1. 使用React實現一個Collapse元件

不羈的風發表於2022-07-16

寫元件的能力是衡量前端工程師水平的重要指標,不管是基礎元件還是業務元件。
筆者在空閒時間也喜歡寫元件,為了幫助初學者上手寫React元件,同時為了分享我在寫元件中的經驗和想法,決定開設一個系列,即:動手擼元件系列,和大家分享一些公共元件和業務元件的實現方式和實現技巧。

作為這個系列的第一篇文章,分享下如何從零到一實現一個摺疊皮膚(Collapse)元件

Collapse基礎UI繪製

摺疊皮膚作為一個基礎元件,由兩部分構成:第一部分是標題區域,第二部分是可摺疊區域,點選標題區域可以摺疊和展開內容區。為了元件的美觀性可以在標題右側新增一個箭頭圖示,在展開和摺疊的時候使其旋轉。

為了降低環境搭建成本,實踐採用create-react-app環境,建立create-react-app開發環境異常簡單,只需要在安裝node的系統中執行如下命令

npx create-react-app 專案名稱

需要注意的是專案名必須為英文,create-react-app會自動為我們建立一個目錄。

專案建立完成後,在src目錄下建立名為Collapse.jsx的檔案,輸入如下程式碼:
(初學者可以選擇複製)

import React, { useState } from "react";
import "./style.css";

const CollapsablePanel = () => {
    const [isCollapsed, setIsCollapsed] = useState(true);

    const togglePanel = () => {
        setIsCollapsed((prevState) => !prevState);
    };
    return (
        <div className="wrapper">
            <div className="pannel" onClick={togglePanel}>
                <div className="heading">
                    <span>Flower Collapse</span>
                    <svg width="20px"
                        height="25px" viewBox="0 0 1024 1024"
                        style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
                         45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7 
                         776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
                        </path></svg>
                </div>
                <div
                    className="content"
                >
                    <div className="contentInner" >
                        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
                        nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
                        erat, sed diam voluptua.
                    </div>
                </div>
            </div>
        </div>
    );
};

export default CollapsablePanel;

接著建立名稱為style.css的樣式檔案

.wrapper {
    display: flex;
    justify-content: center;
    width: 100%;
    height: 100vh;
    background-color: rgb(228, 239, 239);
    padding-top: 40vh;
}

.pannel {
    width: 400px;
    text-align: left;
}

.heading {
    background-color: #bfa;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    color: #000;
    font-size: 20px;
    line-height: 20px;
    border: 1px solid rgb(212, 240, 205);
    padding: 10px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
}

.content {
    font-size: 20px;
    background: #fff;
    border: 1px solid #fff;
    border-top: none;
    padding: 0 20px;
    color: #000000;
    overflow: hidden;
}

.contentInner {
    padding: 20px 0;
}

建立完以上兩個檔案後,在index.js中掛載建立好的Collapse元件:
(原有程式碼無關緊要,直接刪除即可)

import React from 'react';
import ReactDOM from 'react-dom/client';
import Collapse from './components/Collapse';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Collapse />
);

建立完成後,使用yarn start命令啟動應用,就能看到繪製好的Collapse元件外觀:

Collapse元件UI圖

完成了Collapse基礎UI繪製,回顧一下做了哪些操作:
首先定義了名為isCollapsed的state,儲存元件展開關閉狀態,並宣告瞭名為togglePanel方法,在使用者點選標題的時候呼叫此方法即可實現皮膚的展開關閉。
接著分別定義了樣式名為pannel、heading、content的div容器及與其相關的子容器,並在style.css中設定了容器的樣式。元件中的ICON使用svg標籤直接繪製,避免因引入svg包增大元件體積。

內容區展開動畫

實現動畫的方式有很多,可以使用css的transition屬性實現,也可使用React生態種類繁多的動畫庫。在React生態中,有個非常流行的動畫庫叫react-spring,不僅功能強大,而且支援hook方式呼叫,本文就用這個動畫庫來實現內容區域展開動畫和按鈕旋轉動畫。

安裝react-spring動畫庫

yarn add react-spring

安裝完react-spring動畫庫以後,就可以定義方法讓spring幫我們生成動畫樣式了

const panelContentAnimatedStyle = useSpring({
    height: isCollapsed ? 0 : 200,
  });

接著把內容區域的標籤名從<div>改為<animated.div>即可(和useSpring相同,animated也是react-spring具備的一個物件),並在標籤中加上剛剛建立的panelContentAnimatedStyle:

import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import "./style.css";

const CollapsablePanel = () => {
    const [isCollapsed, setIsCollapsed] = useState(true);

    const togglePanel = () => {
        setIsCollapsed((prevState) => !prevState);
    };
    const panelContentAnimatedStyle = useSpring({
        height: isCollapsed ? 0 : 180,
    });
    return (
        <div className="wrapper">
            <div className="pannel" onClick={togglePanel}>
                <div className="heading">
                    <span>Flower Collapse</span>
                    <svg width="20px"
                        height="25px" viewBox="0 0 1024 1024"
                        style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
                         45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7 
                         776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
                        </path></svg>
                </div>
                <animated.div
                    style={panelContentAnimatedStyle}
                    className="content"
                >
                    <div className="contentInner" >
                        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
                        nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
                        erat, sed diam voluptua.
                    </div>
                </animated.div>
            </div>
        </div>
    );
};

export default CollapsablePanel;

點選標題欄可以看到如下效果:

collapse.gif

等等~~ 高度是固定的嗎?

顯然不是!使用者在使用Collapse元件的時候,傳遞的內容不單是文字還有可能是圖片或者是任意型別的ReactNode,所以在展開的時候是需要獲取content物件的實際高度,獲取DOM物件高度的操作有一個庫可以幫助我們,react-use-measure,這個庫不僅可以測量DOM物件的長度和寬度,還可以測量DOM物件距離瀏覽器上下左右的位置。
react-use-measure提供了名為useMeasure的hook,使用方式如下:

const [ref, bounds] = useMeasure();

第一個引數是ref物件,將其繫結到需要測量的DOM物件的ref屬性上即可,第二個bounds就是位置物件,包含上面提到的所有屬性。

繼續改造元件程式碼Collapse.jsx

import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";

const CollapsablePanel = () => {
    const [isCollapsed, setIsCollapsed] = useState(true);
    const [ref, bounds] = useMeasure();

    const togglePanel = () => {
        setIsCollapsed((prevState) => !prevState);
    };
    const panelContentAnimatedStyle = useSpring({
        height: isCollapsed ? 0 : bounds.height,
    });
    return (
        <div className="wrapper">
            <div className="pannel" onClick={togglePanel}>
                <div className="heading">
                    <span>Flower Collapse</span>
                    <svg width="20px"
                        height="25px" viewBox="0 0 1024 1024"
                        style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
                         45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7 
                         776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
                        </path></svg>
                </div>
                <animated.div

                    style={panelContentAnimatedStyle}
                    className="content"
                >
                    <div ref={ref} className="contentInner" >
                        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
                        nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
                        erat, sed diam voluptua.
                        Lorem ipsum, dolor sit amet consectetur adipisicing elit.
                         Quasi aperiam dignissimos eaque deserunt expedita sit 
                         accusamus sunt laudantium repellendus nisi! Sit, 
                         consequuntur. Tempora, officiis molestiae 
                         fuga sit quae aliquid maxime.
                    </div>
                </animated.div>
            </div>
        </div>
    );
};

export default CollapsablePanel;

現在的效果:

collapse2.gif
使用react-use-measure可以非常方便的獲取DOM物件的真實高度和在瀏覽器中的位置,在專案中靈活運用可以提高開發效率。

實現箭頭圖示旋轉動畫

箭頭圖示的旋轉和內容區域的實現類似,只需要將其標籤改成animated.div,並將useSpring生成的樣式物件繫結即可。
生成箭頭ICON旋轉動畫style物件:

const toggleWrapperAnimatedStyle = useSpring({
    transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
  });

svg物件外套一個div,並繫結動畫樣式:

import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";

const CollapsablePanel = () => {
    const [isCollapsed, setIsCollapsed] = useState(true);
    const [ref, bounds] = useMeasure();

    const togglePanel = () => {
        setIsCollapsed((prevState) => !prevState);
    };
    const panelContentAnimatedStyle = useSpring({
        height: isCollapsed ? 0 : bounds.height,
    });
    const toggleWrapperAnimatedStyle = useSpring({
        transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
    });
    return (
        <div className="wrapper">
            <div className="pannel" onClick={togglePanel}>
                <div className="heading">
                    <span>Flower Collapse</span>
                    <animated.div style={toggleWrapperAnimatedStyle}>
                        <svg width="20px"
                            height="25px" viewBox="0 0 1024 1024"
                            style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
                         45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7 
                         776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
                            </path></svg>
                    </animated.div>
                </div>
                <animated.div

                    style={panelContentAnimatedStyle}
                    className="content"
                >
                    <div ref={ref} className="contentInner" >
                        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
                        nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
                        erat, sed diam voluptua.
                    </div>
                </animated.div>
            </div>
        </div>
    );
};

export default CollapsablePanel;

可以看到如下效果:

collapse3.gif

總結

動手擼元件系列第一篇文章選擇講Collapse元件的實現,是因為這個元件的實現簡單而且富有趣味,讀著可以體會到寫元件的樂趣。一個簡單的元件在實現的時候也有可能遇到問題,像如何獲取content區域中的高度,就很有代表性。當前React及Vue的生態都異常繁榮,開發者在實現具體需求的時候要能夠靈活運用這些開源庫,以開發出簡潔且功能強大的元件。

Coollapse.jsx原始碼
style.css原始碼

相關文章