iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
8
Modern Web

一步一腳印的React旅程系列 第 9

[筆記][React]受不受控的Component與Form表單

Hello!大家好!剛剛差一點點點就睡著了,真是完全都不能懈怠下來,不然我的隊友會殺了我XD,今天要說的東西主要是受控組件(Controlled Component)與不受控組件(Uncontrolled Component),這種組件到底是什麼,讓我們往下看!Check it out!


Form表單

對,既然標題提到了,我們就不能忽略他對吧?其實受控組件和不受控組件要從表單開始說起,以下先用react做一個非常非常簡單的Form表單組件:

class EasyForm extends React.Component {
    render() {
        return (
            <form>
                <label>姓名:</label>
                <input id="name" name="name" />
                <br/>
                    <input type="submit" value="送出表單"  />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

點擊提交按鈕submit後就會送出表單,submit事件會把表單內的inputname和值帶到網址上:
https://ithelp.ithome.com.tw/upload/images/20180926/201069351gtaPX9mRi.png

inputvalue綁定state

而大家應該都還記得組件有內部狀態state這個屬性,所以我們可以將state的屬性值設定在inputvalue上面,讓state的值會隨著input內的值變動!例子如下:

class EasyForm extends React.Component{
    constructor(props){
        super(props)
        //在組件內的state設定一個nameVal屬性,並設值為“預設姓名”
        this.state = {name : '預設姓名'}
    }
    render() {
        return (
            <form>
                <label>姓名:</label>
                /*在這裡將state的值設定給name的value*/
                <input id="name" name="name" value={this.state.name} />
                <br/>
                    <input type="submit" value="送出表單"  />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

結果應該不會有太大的問題,就是input出現我們設定的預設值this.state.name了:
https://ithelp.ithome.com.tw/upload/images/20180926/20106935elOtyOqaZI.png

同時更改state

但是!沒看到的問題才是問題!當我們試著要在input中輸入資料時,卻發現輸入不了啊!讓我們仔細想想原因,現在的input中的valuethis.state.name,所以當我要改inputvalue就會動到this.state.name,有發現了嗎?記得之前曾經說過,在組件中要改變state只有一個方法!就是setState(),所以我們趕緊把onChange加進input中,讓value發生改變的時候,可以同時更改state

class EasyForm extends React.Component{
    constructor(props){
        super(props)
        this.state = {name : ''}
        //設定該function的this為class本身
        this.changeState = this.changeState.bind(this)
    }

    //傳入event要取觸發事件的元件
    changeState(event){
        //使用setState將值寫到nameVal中
        this.setState({name:event.target.value})
        console.log('改了改了')
    }
    render() {
        return (
            <form>
                <label>姓名:</label>
                <input type="text" id="name" name="name" 
                        value={this.state.name} 
                        {/*設定onchange事件*/}
                        onChange={this.changeState} />
                <br/>
                    <input type="submit" value="送出表單"  />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

如我們所願,加入onChange讓他同步用setState更改state,就可以自由輸入了!
https://ithelp.ithome.com.tw/upload/images/20180927/20106935URH6RdUbVt.png

用綁定的state取得輸入資料

那既然我們可以用state.name賦予預設值,那能不能從state.name中把目前使用者輸入的值給取出來?可以的!讓我們為form添加onsubmitfunction,看看是否能取到正確的值吧!以下例子:

class EasyForm extends React.Component{
    constructor(props){
        super(props)
        this.state = {name : ''}
        this.changeState = this.changeState.bind(this)
        this.submitForm = this.submitForm.bind(this)
    }

    changeState(event){
        this.setState({name:event.target.value})
        console.log('改了改了')
    }
    
    //新增一個submit的function
    submitForm(event){
        console.log(`現在輸入的名字是:${this.state.name}`)
        event.preventDefault()
    }

    render() {
        return (
            /*幫form表單新增一個onSubmit事件,讓submit的時候執行*/
            <form onSubmit={this.submitForm}>
                <label>姓名:</label>
                <input type="text" id="name" name="name" 
                        value={this.state.name} 
                        onChange={this.changeState} />
                <br/>
                    <input type="submit" value="送出表單"  />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

在送出submit的時候,執行了submitForm在他裡面有一個event.preventDefault()是為了防止submit送出整個網頁,所以用event.preventDefault()來取消submit的預設功能(詳細用法可以看這裡),結果如下,果然能夠透過state取得input的值:
https://ithelp.ithome.com.tw/upload/images/20180927/20106935nNQOxrX9x4.png

Github程式碼連結
GitPage頁面連結

那什麼是受控組件?上面提到的就是受控組件!

對吧!就是這麼簡單,因為我們的資料傳輸是雙向的,可以從組件內部的state綁定value值,當value改變的時候也可以同時更改組件的state,當然啊!這個是input,像其他的還有textareaselectcheckbox都可以被受控!以下來簡單說明幾種受控組件吧!

textareaselect的用法

class EasyForm extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: '',
            introduction: '',
            gender: 'M'
        }
        this.changeState = this.changeState.bind(this)
        this.submitForm = this.submitForm.bind(this)
    }

    changeState(event) {
        /*因為所有的組件改變時都會呼叫這個function
        所以這裡就不能像一開始一樣寫死的*/

        //首先要去抓目前發生改變的組件的name
        let changeName = event.target.name
        //再把他目前的value拿去更改state
        this.setState({ [changeName]: event.target.value })
    }

    submitForm(event) {
        console.log(`現在輸入的名字是:${this.state.name}`)
        console.log(`現在選擇的性別是:${(this.state.gender == 'M')?'男':'女'}`)
        console.log(`現在輸入的介紹內容是:${this.state.introduction}`)
        event.preventDefault()
    }

    render() {
        return (
            <form onSubmit={this.submitForm}>
                <div>
                    <label>姓名:</label>
                    <input type="text" id="name" name="name"
                        value={this.state.name}
                        onChange={this.changeState} />
                </div>
                {/*需注意的是,textarea和select也是使用value屬性來綁定state*/}
                <div>
                    <label>性別:</label>
                    <select id="gender" name="gender"
                        value={this.state.gender}
                        onChange={this.changeState} >
                        <option value="M">男</option>
                        <option value="W">女</option>
                    </select>
                </div>
                <div>
                    <label>自我介紹:</label><br />
                    <textarea id="introduction" name="introduction"
                        value={this.state.introduction}
                        onChange={this.changeState}></textarea>
                    <br />
                </div>
                <input type="submit" value="送出表單" />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

上面的例子會比較不理解的地方應該是:

changeState(event) {
    let changeName = event.target.name
    this.setState({ [changeName]: event.target.value })
}

可以寫像上方那樣是因為我們綁定的state值剛好對應到每個組件的name屬性,所以才可以直接抓組件name的屬性來更改state中的資料,至於加上中括號是為了讓JavaScript知道那是變數,而不會把它當成changeName來處理。

結果就會有個擁有雙向綁定的簡易表單:
https://ithelp.ithome.com.tw/upload/images/20180927/20106935nWmFbZfKaG.png

Github程式碼連結
GitPage頁面連結

checkbox的用法

checkbox的用法比較特別,因為他是多選的,所以就無法只靠設定一個value來達成目的,先假設我們有幾個選項,並知道該選項是否被勾選,再用迴圈將所有的checkbox列出來:

class EasyForm extends React.Component {
    constructor(props) {
        super(props)
        this.submitForm = this.submitForm.bind(this)
        this.changeState = this.changeState.bind(this)
        //這是待辦事項的清單,id是唯一值、listName為事項、check為是否完成
        this.state = {lists :
                        [{id:'01',listName:'寫文章',check:false},
                        {id:'02',listName:'打程式',check:false},
                        {id:'03',listName:'耍廢',check:true}]}
    }
    
    //傳進index是為了知道目前點選的事項是在陣列中的哪個位置
    changeState(index){
        //修改時先將this.state.lists指定給一個變數
        let arrLists = this.state.lists
        
        //確認清單中的該事項目前狀態是不是已完成
        if(arrLists[index].check)
            //原本是true的話這時候會變false
            arrLists[index].check = false
        else
            //原本是false的話這時候會變true
            arrLists[index].check = true
            
        //改完後用setState將lists重新設定為arrLists
        this.setState({lists:arrLists})

    }

    submitForm(event) {
        let status = "目前做了:"
        
        //將陣列中check為true的事項都列出來,代表完成
        this.state.lists.map((list)=>{(list.check) ? status += `${list.listName} `:''})

        console.log(status)
        event.preventDefault()
    }

    render() {
        //使用map跑迴圈,將結果給lists,map的第二個參數index為目前是第幾個索引
        let lists = this.state.lists.map((list,index)=>(
            /*既然使用迴圈,就要設key對吧!*/
            <div key={list.id}>
                {/*這裡將checked屬性設定成清單中的check,true就打勾、false就沒勾
                onChange中代入第二個參數index,是為了辨別變動的是第幾個索引的事項
                最後因為是迴圈,所以要記得設key對吧!*/}
                <input type="checkbox" 
                        checked={list.check} 
                        onChange={this.changeState.bind(this,index)}
                        key={list.id} />
                <label>{list.listName}</label>
            </div>
        ));

        return (
            <form onSubmit={this.submitForm}>
                <div>
                    <label>每日待辦清單:</label>
                    {/*直接將剛剛跑完迴圈的變數放進來*/}
                    {lists}
                </div>
                <input type="submit" value="送出表單" />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm  />, document.getElementById('root'))

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180930/20106935x6aoQLnNZh.png

上面的程式碼看起來會有點複雜,但是其實只是多了迴圈的用法,還有控制checkBox的屬性值變成truefalse,並設定在checked而不是value上,其他都還是一樣,只要嘗試做一次就很容易理解,那以下附上Github連結讓各位參考/images/emoticon/emoticon41.gif

Github程式碼連結
GitPage頁面連結

那不受控組件是?

對!到最後總是要提一下這個,既然受控組件是可以用state去設定組件的value,那沒辦法這樣做的就例如file,因為必須由使用者選擇檔案,再去取值上傳,這種情況下我們沒辦法去設定他的值是什麼,所以被稱為不受控組件。

當我們要去取得不受控組件的值時,當然沒辦法用state取得,針對這種狀況可以利用React.createRef()來處理,這是初次見面的函式,他能夠讓你在不使用選擇器的狀態下直接操作組件內的DOM,以下來看看他的厲害吧!

class EasyForm extends React.Component {
    constructor(props) {
        super(props)
        this.submitForm = this.submitForm.bind(this)
        
        //使用React.createRef()建立一個物件給filebox
        this.filebox = React.createRef()
    }
    submitForm(event) {
        /*在function內就可以直接取用
        React.createRef()建立的this.filebox來取得對應設定ref的組件*/
        console.log(`選擇檔案為:${this.filebox.current.files[0].name}`)
        event.preventDefault()
    }

    render() {
        return (
            <form onSubmit={this.submitForm}>
                <div>
                    <label>上傳檔案:</label>
                    <input type="file"
                        /*這裡將用React.createRef的filebox指定給該組件的ref屬性
                        讓class內的function可以依照ref取得組件*/
                        ref={this.filebox} />
                </div>
                <input type="submit" value="送出表單" />
            </form>
        )
    }
}

ReactDOM.render(<EasyForm />, document.getElementById('root'))

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180928/20106935Xei5j1UWNW.png

Github程式碼連結
GitPage頁面連結


本篇稍微有點長,不過幾乎都是在講同一個概念,只是換成各種表單的用法XD,而且最後出現的React.createRef()表現也非常驚艷(對我來說啦XD),雖然大部分時候都直接用state啦!但是多知道一種用法就多一項武器嘛!

最後感謝各位大大的觀看,如果文章中有任何問題,還麻煩留言告訴我,小弟會在盡快修正或補充文章的!謝謝大家/images/emoticon/emoticon41.gif

參考文章:

  1. https://reactjs.org/docs/forms.html

上一篇
[筆記][React]用 for 迴圈處理Component
下一篇
[筆記][React]使用React開發的思考模式(1)-記得把共同狀態提升
系列文
一步一腳印的React旅程30

尚未有邦友留言

立即登入留言