[ 一起學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屬性(即使爲空或者無值)那麼該輸入框將無法進行輸入。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章