1. 程式人生 > >[ 一起學React系列 -- 5 ] 如何優雅得使用表單控制元件

[ 一起學React系列 -- 5 ] 如何優雅得使用表單控制元件

網頁中使用的form表單大家肯定都再熟悉不過了,它主要作用是用來收集和提交資訊。React中的表單元件與我們普通的Html中的表單及其表現形式沒有什麼不同,所以如何使用表單我覺得再拿出來說可能是畫蛇添足、毫無意義。不過再怎麼樣也不能辜負大家對標題的期待吧,本篇內容筆者將以控制元件為主體進行記錄。

那麼問題來了,何為控制元件?在React中,將form元件(<form></form>)下的所有子元件統稱為控制元件,比如常用的input:text, Select, input:radio等等。

對於一個表單來說,<form/>只是這個表單的載體,它具體能做哪些事還是由其內部的控制元件決定的。所以如何將這些控制元件有效得組織起來形成一個表單體系就顯得尤為重要。同時React也將控制元件分為受控元件

(Controlled Components)和非受控元件(uncontrolled components)。不過有一點需要明確下這個分類依據並不是以元件的類別來分類而是以使用方式來分類的,具體咱們下面接著說。

受控元件

官方對受控元件的定義是:

An input form element whose value is controlled by React in this way is called a “controlled component”

簡而言之就是使用者輸入型的表單控制元件是由React體系來管理。比如一個輸入框,我們可以通過其value對其賦值進而實現改變輸入框中的值的目的(當然不只限於輸入框,還有下拉框、文字域等)。而這也是受控元件可以被React控制的關鍵所在。先舉個例子:

import React, {Component} from 'react';

class ControlledInput extends Component {
    constructor(props) {
        super(props);
        this.state = {
            inputValue: 'React'
        }
    }

    render() {
        return (
            <input type="text" value={this.state.inputValue}/>
        )
    }
}
export default ControlledInput;

通過例子我們能看出在這個元件的State中我們定義了inputValue這個欄位並且將它以value的形式賦給了一個輸入框,此時這個元件的表現如下:clipboard.png

不過擁有雪亮眼睛的朋友一眼就能看出來兩個"問題":

  1. 此時的輸入框不管怎麼敲擊鍵盤,輸入框中的值都是不變的。熟悉原生輸入框的朋友都知道,一旦它的value屬性被賦了值那麼相當於輸入框的內容被寫死了,如果想改變輸入框的內容那就得改變value屬性的值
  2. 輸入框的value屬性的值是元件State中的inputValue欄位。

根據上面兩個"問題"我們可以預見到:如果通過setState方法去改變inputValue的值,這樣React會重新渲染元件節點中的inputValue值,不就相當於改變了輸入框的value屬性值了麼?那就動手實踐下,我們在這個元件中新增一個按鈕用它來改變inputValue值:

import React, {Component} from 'react';

class ControlledInput extends Component {
    constructor(props) {
        super(props);
        this.state = {
            inputValue: 'React'
        }
    }

    changeHandler = () => {
        this.setState({
            inputValue: 'React Form'
        })
    };

    render() {
        return (
            <div>
                <input type="text" value={this.state.inputValue}/>
                <input type="button" value="變" onClick={this.changeHandler}/>
            </div>

        )
    }
}
export default ControlledInput;

clipboard.png

效果與我們所遇見的一樣。不過實際開發中我們可不能通過點選按鈕去改變輸入框的值,因為輸入框需要使用者的實際輸入,所以我們需要在輸入框DOM節點上繫結onChange實踐,將使用者實際輸入的值set給元件的State。修改方式如下:

import React, {Component} from 'react';

class ControlledInput extends Component {
    constructor(props) {
        super(props);
        this.state = {
            inputValue: 'React'
        }
    }

    inputHandler = (e) => {
        const content = e.target.value;
        this.setState({
            inputValue: content
        })
    };

    render() {
        return (
            <div>
                <input type="text" value={this.state.inputValue} onChange={this.inputHandler}/>
            </div>

        )
    }
}
export default ControlledInput;

每當輸入框中的內容發生變動我們就將變動後的內容通過State重新渲染給輸入框的value屬性。說到這裡,可能就有人認為這不是脫褲子放屁嗎?為什麼要在使用者輸入完後再繞一圈?且不急,後面會有具體說明。我們繼續往下說!!!上面的程式碼就是將輸入框作為一個受控元件在使用。具體流程我們可以畫一個圖,這樣更為直觀也更容易理解。clipboard.png

這就是受控元件的實現方式流程圖。這裡僅僅是用輸入框作為演示主體,其實還有下拉框、文字域這類可以產生更多互動的Html標籤(或者說是可以通過value屬性改變其外在表現的Html標籤)。不過這裡筆者想特地排除下單選框和複選框,因為它們的狀態太過單一。

非受控元件

說到非受控元件我相信大家都能意會它的是什麼意思,就是它的狀態由自己維護,實際上這也是Html標籤原來的樣子。比如輸入框被輸入了什麼值,那這個值就被輸入框自己管理,不由外來因素管理。或許這麼做會顯得更為簡單,筆者一直也這麼認為(僅限一般情況下)。這裡我們繼續以輸入框為主要演示物件繼續往下說。對於一個輸入框如果我們想獲取它的輸入內容,一般有有兩個方法,一個是繫結事件在其輸入期間同步的獲取輸入值,還有一個辦法是通過原生DOM物件的value獲取輸入值。不過對於非受控元件我們應該最大限度得保證其純淨,如果給其繫結一個事件來獲取輸入值那麼相當於用受控元件的實現方式去處理它會顯得很冗餘,所以通過原生DOM去獲取輸入值是最Nice的辦法當然React也提供了訪問原生DOM的方法: React.createRef()。這是React提供的新的訪問原生DOM的方法,牆裂建議這樣使用;同樣我們也使用它改變一下原來的程式碼:

import React, {Component} from 'react';

class ControlledInput extends Component {
    constructor(props) {
        super(props);
        this.input = React.createRef();
    }

    inputHandler = () => {
        const originalDom = this.input.current;//current代表原生DOM物件
        const content = originalDom.value;
        alert(content);
    };

    render() {
        return (
            <div>
                <input type="text" ref={this.input}/>
                <input type="button" value="取值" onClick={this.inputHandler}/>
            </div>

        )
    }
}
export default ControlledInput;

clipboard.png

執行結果很完美!!!非受控元件的實現及使用方法就是這麼簡單,我們只管取值就行了,其它由它們自己管理就好了。

受控元件與非受控元件的不同及啟示

通過上面的介紹我們瞭解了受控元件與非受控元件及其實現、使用方式。它們的不同之處筆者這裡用一句話總結下:

受控元件的value值受React管理;非受控元件的value值由自身管理;

那這句話能給我們什麼樣的啟示?首先看非受控元件的value值由自身管理,對於非受控元件我們好像除了拿值其它的什麼都幹不了,實際情況的卻如此,pass!!再看受控元件的value值受React管理,前面我們知道受控元件對於使用者輸入的值需要從State中繞一下再到元件本身。那麼我們可不可以在這個“繞”的過程中做點什麼?比如在setState之前對使用者輸入的內容做點手腳什麼的...帶著這個想法,我們往下看!

如何選擇受控元件與非受控元件

既然React將form控制元件分為受控元件與非受控元件自然有它自己的道理,那麼我們實際開發中應該如何優雅得選擇它們?假如我們只是單純得獲取使用者的輸入,那我們會本能的選擇非受控元件,因為它使用很方便不需要寫那麼多複雜的邏輯;假如我們想對使用者的輸入做點手腳,比如字母轉大小寫等等...因為原生input標籤不具備此功能,或許我們就得使用受控元件。當然一位外國友人對如何選擇受控元件與非受控元件發表了自己的看法,筆者這裡做個搬運。不想看或者沒法看的朋友可以看下面的搬運內容,跳過廢話直接上結論:

clipboard.png

  1. 一次性資料獲取可以任意使用其中一種。不過筆者覺得選擇非受控元件會更為便捷,不需要寫一大堆邏輯
  2. 提交時進行驗證可以任意使用其中一種。這個仁者見仁智者見智,不過筆者還是覺得非受控元件會更好
  3. 實時驗證使用受控元件。因為我們在使用者輸入過程中對輸入值進行實時驗證,而非受控元件不支援這麼做,初非你用第三方庫;
  4. 有條件地禁用提交按鈕使用受控元件。禁用提交按鈕的前提是輸入值不合法,參考第三條。
  5. 對輸入值進行格式化使用受控元件。非受控元件輸入什麼就是什麼,我們無法去改變;而受控元件我們可以在獲取到輸入值的時候對輸入值進行轉換,比如大小寫獲取貨幣轉換等等,最後render回控制元件中。
  6. 對一條資料盡心多次輸入使用受控元件。同樣的非受控元件的狀態由自身管理,而對於受控元件我們可以對輸入值做更多的處理
  7. 動態輸入使用受控元件。假如我們從後臺拉取一個數據要填入輸入框,那麼必須得使用受控元件,因為非受控元件只能被使用者輸入。

心得:在表單中使用單選框和複選框

為什麼單獨將單選框和複選框拉出來軍訓?是因為它們的狀態單一不需要使用者輸入且它們的value屬性並不和自身表現有直接關係。例如我們無法通過對單選框的value設定為true從而讓它變為選中的狀態。而且單選框和複選框基本都是成群結隊出現的,所以從受控元件與非受控元件角度來處理它們顯然不適合。這裡筆者推薦兩種處理方式:

事件代理

比如這樣寫:

import React, {Component} from 'react';

class ControlledInput extends Component {

    constructor(props) {
        super(props);
        this.parent = React.createRef();
    }

    componentDidMount() {
        this.parent.current.addEventListener('change', (event) => {
            //do something
            console.log(event);
        })
    }

    componentWillUnmount() {
        this.parent.current.removeEventListener('change');

    }

    render() {
        return (
            <div ref={this.parent}>
                <input type="radio" name='sex'/>
                <input type="radio" name='sex'/>
            </div>

        )
    }
}
export default ControlledInput;

在一群單選框或者複選框外層包一個div並且繫結事件監聽。當我們點選它們的時候就會獲取到對於的事件物件,然後就可以做你想做!

動態生產

所謂動態生產就是用一個function生成所需要的一群單選框或者複選框,同時給它們繫結相關事件,比如:

import React, {Component} from 'react';

class ControlledInput extends Component {

    constructor(props) {
        super(props);
        this.checkBoxs = ['A', 'B', 'C'];
    }

    //Process checkbox group
    createCheckBoxes = () => this.checkBoxs.map(checkbox => (
        <label key={checkbox}>
            {checkbox}: <input type="checkbox" name="hobby" value={checkbox} onChange={this.checkBoxChange}/>
        </label>
    ));

    checkBoxChange = (e) => {
        //do something
        console.log(e);
    };

    render() {
        return (
            <div>
                Checkboxes: {this.createCheckBoxes()}
            </div>

        )
    }
}
export default ControlledInput;

對於這兩種方法,大家仁者見仁智者見智。不過如果有其他的方法歡迎留言 :)

寫在最後

其實也沒啥寫的,就想再提一下輸入框的三個屬性幫助不熟悉的朋友多瞭解下。對於Html輸入框很熟悉的朋友此處可以跳過了。

  1. placeholder: 主要用來提示使用者這個輸入框需要輸入什麼樣的內容。不影響正常輸入!
  2. defaultValue: 填充該輸入框的預設值,此時不顯示placeholder內容。不影響正常輸入!
  3. value: 起到填充輸入框內容的作用;與defaultValue不能共存;且一旦設定了value屬性(即使為空或者無值)那麼該輸入框將無法進行輸入。