React 開發實戰(一)- Repeat 組件

前言

最近在寫一個面向 React 初學者的系列教程玩轉 React,內容對有 React 開發經驗的同窗來講可能太過於基礎和囉嗦,不太感興趣。因此我打算同時開始另一個系列文章《React 開發實戰》。該系列主要面向有 React 開發經驗的同窗,更側重 React 實戰,每一篇文章會跟你們一塊兒開發一個 React 組件或者一個簡單有趣的 React 應用,這些組件或者應用每每知足以下特色:javascript

  • 在個人實際項目中用到過的。
  • 在常見的開源組件庫中沒有的。
  • 有點小衆,可是在特定的業務場景下能很大地提升項目的開發效率。
  • 可能還比較有趣。

若是這些組件能直接應用到你們的實際開發中去,那再好不過了;若是不能,能給你們一點啓發,我以爲這件事情也是頗有價值的。css

另外,每一篇文章後面都會附有本篇文章的完整示例和代碼。java

問題描述

你們應該都見過這種應用場景,頁面上的某一部分,須要可以讓用戶添加任意多項。web

多是表單中的一個字段,以下所示。segmentfault

圖片描述

也多是表單的一部分,以下所示,用戶能夠在一個表單內增長多個用戶信息,而後將用戶信息批量進行保存。api

圖片描述

還有更變態的,以下所示,一個表單內用戶信息部分能夠添加多份,每個用戶信息中地址也能夠添加多份。(Oh, My God. PM,你殺了我吧。)數組

圖片描述

還好,React 應付這種需求,仍是小菜一碟。可是在一個 web 應用中有這麼多的類似場景的話,若是咱們挨個實現一遍,那真是太枯燥了,與搬磚無異。遇到這種狀況,就須要咱們把相同的功能抽象出來,作成組件,這將極大地提高你的開發效率。數據結構

基於這個場景,咱們今天就開發一個能讓其 children 重複任意多份的組件,咱們就稱之爲 Repeat 吧。函數

你指望 Repeat 組件該怎麼用

在開發一個組件的時候,不要着急寫代碼,先想一想你要把這個組件作成什麼樣子,例如這個 Repeat 組件,我但願有以下特性:this

  • Repeat 組件提供默認的,添加、移除按鈕。
  • 點擊添加,將 React 的 children 複製一份,點擊移除將某一項移除。
  • 當只有一項時不能移除。
  • Repeat 支持 onChange 回調函數,當 Repeat 內的表單輸入發生變化時能夠即時通知其父組件。

而後在代碼中我指望能夠這樣來用 Repeat 這個組件:

class App extends React.Component {
    handleChange(items) {
        console.info(items);
    }
    render() {
        <Repeat onChange={items => this.handleChange(items)}>
            <input  type="text" />
        </Repeat>
    }
}

OK,就是這麼簡單,這樣 Input 組件就能夠重複加添多份了。基於這個構想,咱們來實現 Repeat 這個組件。

開始實現 Repeat 組件

class Repeat extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            items: [''],
        };
    }
    handleChange(e, index) {
        const items = [...this.state.items];
        items[index] = e.target.value;
        this.setState({ items });
        this.props.onChange(items);
    }
    handleAddItem(e, index) {
        e.preventDefault();
        const items = [...this.state.items];
        items.splice(index, 0, '');
        this.setState({ items });
    }
    handleRemoveItem(e, index) {
        e.preventDefault();
        if (this.state.items.length === 1) return;
        const items = [...this.state.items];
        items.splice(index, 1);
        this.setState({ items });
    }
    render() {
        const children = React.Children.only(this.props.children);
        const elementItems = this.state.items.map((item, index) => (
            <div key={index}>
                {
                    React.cloneElement(children, {
                        onChange: e => this.handleChange(e, index),
                        value: item,
                    })
                }
                <div>
                    <a href="#" onClick={e => this.handleAddItem(e, index)}>添加</a>
                    <a href="#" onClick={e => this.handleRemoveItem(e, index)}>移除</a>
                </div>
            </div>
        ));
        return <div>{elementItems}</div>;
    }
}

代碼很簡單,簡單解釋一下:

  • 組件的 state 中持有 items 字段來保存每個項的數據。
  • render 時先獲取到惟一的 children,而後 map 組件 state 中的 items,將每一項映射爲 children 的一個副本。併爲這個副本傳入兩個屬性,onChange 接收每一項的數據變化,value 傳遞每一項當前應展現的值。
  • 另外 Repeat 爲每一項準備了一個「添加」按鈕和一個「移除」按鈕,用來在當前項位置新增一項或者移除當前項。原理就是將 this.state.items 中對應下標處的數組元素刪掉就行了。

到此,Repeat 是否是大體有模有樣了呢。須要提醒你們的是,React.cloneElementReact.Children.xxx 這些 api 一般只會在這種公共組件中使用,在大部分場景,儘可能少用。

跟 children 有個約定

有些同窗可能已經發現了,上面例子中, Repeatchildren 是個 input,那若是是一個其餘的組件不就完蛋了嘛。

這是第一個問題,爲了解決這個問題呢,Repeat 須要對它的 children 提兩個條件:

  1. 屬性上必需要接收一個 onChange 回調函數,函數接收一個對象參數,參數結構以下:

    {
        target: {
            value: 'xxxx'
        }
    }

    value 的值爲當前項產出的數據,多是個對象也多是字符串或者數值。沒錯,我就是爲了兼容 input event 的數據結構。你固然能夠用任何你喜歡的且方便處理的數據結構。

  2. children 組件須要接收一個 value 屬性,以展現其擁有的值。也就是說 children 組件應當是一個受控的(controlled)組件。

這就是一個協議,你但願某個組件內經過 Repeat 組件方便地添加多份並能獲取到一組數據,那就必需要遵照這個協議。有同窗可能會說爲何不搞的智能一點呢?嗯,這裏我想分享一點我的經驗:有些時候,尤爲是在業務開發過程當中,把公共部分抽取出來複用便可,點到爲止,沒有必要搞得那麼「強大」,剩下的事情讓一個很容易遵照的協議來完成,其實效率會更高,更容易讓人理解。

其實在計算機的世界中到處充滿了協議,例如你想讓 HTTP Server 返回正確的響應,你必需要遵循 http 協議來和它通訊;你生產的顯卡能買的出去,必需要遵照相應的協議,要能插到別人家生產的主板上。

扯遠了!收!

對,有了上面這個約定之後,Repeat 一行代碼未加,是否是感受功能完善了許多?嗯,就是這個目的。如今咱們來實現一下文章開始時候說的第二個場景。

聰明的你必定已經知道該怎麼作了,沒錯,只要咱們實現一個 UserForm 組件,並讓他知足上面的約定便可。請看代碼:

class UserForm extends React.Component {
    handleFieldChange(e) {
        const { name, value } = e.target;
        const formData = {
            ...this.props.value, 
            [name]: value,
        }
        this.props.onChange({
            target: {
                value: formData,
            }
        });
    }
    render() {
        const formData = this.props.value || {};
        return (
            <div>
                <div>
                    <label for="">姓名</label>
                    <input
                        type="text"
                        name="name"
                        value={formData.name}
                        onChange={e => this.handleFieldChange(e)}
                    />
                </div>
                <div>
                    <label for="">地址</label>
                    <input
                        type="text"
                        name="addr"
                        value={formData.addr}
                        onChange={e => this.handleFieldChange(e)}
                    />
                </div>
            </div>
        )
    }
}

爲了讓代碼更簡潔,我把 UserForm 這個組件實現爲了一個支持受控的組件,可是在目前的業務場景下已經足夠了,在實際狀況下,你能夠按需調整。

經過這個例子,還但願你們能體會到組件拆分的一個好處。就是,UserFormRepeat 拆分紅兩個組件之後,UserForm 的複用性會更強。能夠想象一下,當用戶被批量添加之後,是否是有可能在編輯單個用戶的時候,能夠繼續使用這個組件。

好啦,關於第三個場景我想就沒有必要再實現一遍了,Repeat 嵌套多少層其實都是能夠的。

更進一步

實際上在實際應用中,Repeat 這個組件還須要作進一步完善,其中一個就是樣式,還有可能在不一樣的場景下,雖然交互都是這樣,但樣式會有所差別。另外默認是「添加」、「移除」兩個文字按鈕,說不定實際業務場景中是兩個 +,- 的圖標按鈕;還有可能「添加」、「移除」的位置爲有所變化。

這些問題怎麼處理呢?下面給你們描述下思路,具體代碼就不寫了,若是有什麼疑問能夠給我留言。

  1. 關於樣式,你能夠給 Repeat 添加 itemClassNamebuttonsClassName 兩個屬性分別爲每一項和按鈕區域的 css class。這樣你就能夠在不一樣的場景下指定不一樣的樣式了。
  2. 關於如何將文字按鈕改成圖標按鈕,你能夠給 Repeat 添加 renderButtons 這樣一個函數屬性,若是未指定則用默認的方式渲染按鈕,若是有則勇氣返回值渲染屬性。

最後

這是本篇文章的代碼:https://codepen.io/Sarike/pen...

好啦,文章就到這吧,若是有什麼疑問能夠給我留言。謝謝你們,祝你們國慶、中秋節快樂。

相關文章
相關標籤/搜索