iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 25
3
Modern Web

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

[筆記][React]來做個作品吧!待辦事項「todolist」篇(5)-寫下新增資料的里程碑

嗨囉!大家好啊!就像上一期(還是上上一期?)最後說的,今天就要來處理新增資料啦!話說剛剛在準備進度時,都沒經過任何測試,一次執行就過,讓我非常驚訝,一直在試是不是有哪裡忘了做XD,那接下來就來度過這順利的一天吧!


設定State

首先要在新增待辦事項的表單組件中設定state,把待辦事項的資料都寫到State中,最後再將State送給Redux處理就好!而新增的表單組件是InputTasksForm,但是待辦事項的名稱是在InputTask中,秉持著把State提升到共同組件的精神,應該要選擇在InputTask中設定State資料,不過先別說得太複雜,我們一樣一步一步把它完成:
InputTask.jsx

  1. 在組件內的constructor設定state,欄位就像我們昨天提出的那樣,另外考慮到有一個檔案上傳的input沒辦法設定value,所以出動ref處理:
    constructor(props){
         super(props)
         this.state = {id:'',name:'',date:'',time:'',file:'',commit:'',important:'',complete:false}
         this.filebox = React.createRef()
     }
    
  2. 寫下事件,當組件的值改變時要改變state,下方的做法是去判斷發生改變的是哪個欄位,就取他的name屬性,然後寫進對應的state物件的key中,所以我們等等記得要設定name值:
    changeState(event){
       //取目前發生改變的值
       let value = event.target.value
       //如果是檔案的話因為會有路徑,所以這裡只抓檔名
       if (event.target.name === "file"){
           //去取最後一個出現的「/反斜線」到最後一個字就是檔名了
           value = value.substring(value.lastIndexOf('\\')+1)
       }
       //checkbox也另外處理,因為沒有值
       else if (event.target.name === "complete"){
           value = event.target.checked
       }
       //設定值給對應的name欄位,所以我們組件的name都要設的和state中的名稱一樣
       this.setState({[event.target.name]:value})
    }
    
    變動state的番外篇!其實也不算番外啦!只是todolist的有些資料不是透過input填寫的對吧!像是要不要標記重要或是否完成,這兩個資料,他們分別會透過點擊checkbox和星星圖案的Icon觸發,我們就在這裡寫下他吧!
    1. 先寫下complete的樣式內容,到index.css中增加!

      /*加上刪除線*/
      .complete{
          text-decoration: line-through;
          color: #9B9B9B;
      }
      

      標記已完成,要在checkbox上綁定state中的資料,還有增加onChange事件,讓這邊的異動可以連動更新state

      <input name="complete" type="checkbox" class="taskChk" 
              checked={this.state.complete} 
              onChange={this.changeState} />
      

      最後在待辦事項的input中的class寫判斷式,讓他可以根據state中的值改變樣式:

      <input name="name" type="text" placeholder="Type Something Here…"
                        class={'taskTitle' + 
                            (this.state.complete ? ' complete ' : '')}
                        value={this.state.name}
                        onChange={this.changeState} />
      

      掰惹位,既然上方都改到他了,就順便綁定state的資料和設定onchange事件。

    2. 點擊星星標記重要,這個要再星星的Icon上增加onClick事件,這邊因為也要更改他的選取後的樣式,所以一樣先寫下class

      /*更換背景顏色*/
      .important{
          background: #FFF2DC;
      }
      /*更換icon顏色*/
      .iconImportant{
          color: #F5A623;
      }
      

      為星星的Icon設定點擊事件,標記重要後,不只星星本身,連div和待辦事件名稱的input都要加入判斷式,確認是否要增加上方的class

      <div class={this.state.important == 'Y' ?
                    'important inputTaskTitle' : 'inputTaskTitle'}>
      {/*其餘省略*/}
          <input name="name" type="text" placeholder="Type Something Here…"
                        class={ ' taskTitle ' +
                        (this.state.important == 'Y' ? ' important ' : '') +
                        (this.state.complete ? 'complete' : '')}
                        value={this.state.name}
                        onChange={this.changeState} />
      
          <i class={this.state.important == 'Y' ?
                   'fas fa-star fa-lg icon iconImportant' : 'far fa-star fa-lg icon'}
                        onClick={this.tagImportant} ></i>
      {/*其餘省略*/}
      </div>
      

      事件內容如下,因為星星本身沒有value可以控制,所以特別寫一個用來異動statefunction

      tagImportant() {
          //如果現在不是重要的就把它變重要的
          if (this.state.important == '') {
              this.setState({ important: 'Y' })
          }
          else {
              this.setState({ important: '' })
          }
      }
      
  3. 第二個事件是送給Redux新增的事件,首先要匯入要使用的事件function
    import { addTodoList } from "../../actions"
    
    然後在class中使用,新增一個送出新增的事件,並在裡面把整個State丟給剛剛匯入的事件新增:
    submitTodo(){
        //先檢查資料,至少要有名稱
        if(this.state.name === ''){
            alert('待辦事項名稱未輸入!')
        }
        else{
            //因為state就存著資料,所以直接把state送給他新增
            this.props.addTodoList(this.state)
            alert('成功新增!')
            //關閉新增畫面
            this.props.closeAdd()
            //初始化資料資料
            this.setState({id:'',name:'',date:'',time:'',file:'',commit:''
            ,important:'',complete:false})
            //不受控組件另外處理
            this.filebox.current.value = ''
        }
    }
    

然後貼心小提醒是上面幾個事件都有用到this,所以記得要在constructor中用bind指定this

constructor(props){
    super(props)
    this.state = {id:'',name:'',date:'',time:'',file:'',commit:'',important:'',complete:false}
    this.changeState = this.changeState.bind(this)
    this.submitTodo = this.submitTodo.bind(this)
    this.tagImportant = this.tagImportant.bind(this)
    this.filebox = React.createRef()
}
  1. 資料和事件準備好了,要把他傳入表單中,除了statechangeState要將資料寫回state外,記得+ Save也在InputTasksForm中,也不要忘了剛剛為filebox建立的ref要指定給他,所以要一起在render中透過props傳進去!
    render() {
        return (
            <div>
                <div class={this.state.important == 'Y' ?
                    'important inputTaskTitle' : 'inputTaskTitle'}>
                    <input name="complete" type="checkbox" class="taskChk"
                        checked={this.state.complete}
                        onChange={this.changeState} />
                    {/*替該name設定對應的state名稱,
                    然後指定value為state中的值,
                    和增加onChange事件,
                    讓值改變時可以同時寫回`state`*/}
                    <input name="name" type="text" placeholder="Type Something Here…"
                        class={(this.state.important == 'Y' ?
                            'important taskTitle ' : 'taskTitle ') +
                            (this.state.complete ? 'complete' : '')}
                        value={this.state.name}
                        onChange={this.changeState} />                    
                        <i class={this.state.important == 'Y' ?
                        'fas fa-star fa-lg icon iconImportant' : 
                        'far fa-star fa-lg icon'}
                        onClick={this.tagImportant} ></i>
                    <i class="fas fa-pen fa-lg icon icon_edit"></i>
                </div>
                <InputTasksForm closeAdd={this.props.closeAdd}
                    stateData={this.state}
                    changeState={this.changeState}
                    submitTodo={this.submitTodo}
                    filebox={this.filebox} />
            </div>)
    }
    
  2. 現在要透過Redux來幫忙處理資料,因此要替InputTask組件和addTodoListconnect!而connect後又會是一個新組件,所以我先改變一下InputTask的名稱,在他前面加個Connect
    //幫InputTask改名字
    class ConnectInputTask extends React.Component {
        //內容就像上方改的,文章的最後也會有今天的GitHub進度
    }
    
  3. 記得connect需要哪些參數嗎?有分資料的和事件的!現在在表單中我們只需要事件而已,所以先寫好需求,等等把需求和ConnectInputTask一起透過connectRedux,這樣他才知道我們需要什麼:
    const mapDispatchToProps = dispatch => {
        return {
            //使用dispatch呼叫事件addTodoList操作store
            addTodoList: todoList => dispatch(addTodoList(todoList))
          }    
    }
    
  4. 萬事俱備,只欠connect,把connect後的組件再指定回InputTask,這樣就不用在改其他地方了:
    const InputTask = connect(null,mapDispatchToProps)(ConnectInputTask)
    

現在把InputTask處理好後,還有他裡面的表單InputTasksForm組件要處理,這個部分只需要做四件事情:

  1. 為每個輸入框都設定對應statename
  2. 將受控組件的value都設定為state的值,雖然在這邊還不需給值,不過還是得讓他受控才能改變state,阿不受控的filebox要指定ref給他,讓我們可以在外面控制,至於值就寫進之前為他留的span中。
  3. 為每個組件包括filebox都增加onChange事件,讓他值改變時可以寫到state中。
  4. 把送出新增的事件submitTodo指定給+ Save按鈕的onClick觸發。

做完以上四件事情後,InputTasksForm應該會長這樣:

import React from "react"
import { InputName } from "../InputName"

class InputTasksForm extends React.Component {
    render() {
        return (
            <div class="InputTasksForm">
                <div class="InputTask">
                    <InputName className="fas fa-calendar-alt" inputName="Deadline" />
                    <div class="inputForm">
                        <input name="date" type="date" class="inputStyle inputDateTime" 
                                value={this.props.stateData.date}
                                onChange = {this.props.changeState} />
                          
                        <input name="time" type="time" class="inputStyle inputDateTime" 
                                value={this.props.stateData.time}
                                onChange = {this.props.changeState} />
                    </div>
                    <InputName className="fas fa-file" inputName="File" />
                    <div class="inputForm">
                        <input name="file" type="file" class="inputStyle" 
                                ref = {this.props.filebox}
                                onChange = {this.props.changeState} /><br/>
                        <span class="inputStyle">{this.props.stateData.file}</span>
                    </div>
                    <InputName className="far fa-comment-dots" inputName="Comment" />
                    <div class="inputForm">
                        <textarea name="commit" rows="7" cols="55" class="inputStyle"
                                    value = {this.props.stateData.commit}
                                    onChange = {this.props.changeState} >
                        </textarea>
                    </div>
                </div>
                <div>
                    <button type="button" class="addButton cancelButton" onClick={this.props.closeAdd}> X Cancel</button>
                    <button type="button" class="addButton saveButton" onClick={this.props.submitTodo}> + Save</button>
                </div>
            </div>
        )
    }
}

export { InputTasksForm }

看起來有點長,不過別嚇到,都是我們之前做過的!

好的,那裡面和外面都做完了,接下來還有外外面XD,哈哈哈!我可沒有開玩笑,記得那些被connect後的組件應該要放在哪裡嗎?

就是<Provider>中!應該沒有忘記他吧?他還有個可愛的store屬性是要指定我們createstore啊!好吧!沒關係,只要看到了一定會想起來的!讓我們打開負責最後輸出的Main

  1. 匯入todoListStoreProciderstore昨天如果匯過就不要再重複匯,不然會出錯哦!
  2. 使用Provider組件並指定store屬性為todoListStore,因為其他組件也會需要用到connect,所以選擇在Main中讓他包住整個頁面的組件。
import React from "react"
import { Provider } from "react-redux"
import { HashRouter, Route } from "react-router-dom"
import { TopBlock } from "../TopBlock"
import { MyTasks } from "../MyTasks"
import { todoListStore } from "../../store"

class Main extends React.Component {
    render() {
        return (
            <Provider store={todoListStore}>
                <HashRouter>
                    <div>
                        <TopBlock />
                        <Route exact path="/" component={MyTasks} />
                    </div>
                </HashRouter>
            </Provider>
        )
    }
}

window.store = todoListStore

export { Main }

上方留著window.store = todoListStore,讓我們確認資料用,因為現在還沒有把顯示資料的列表做出來,那進行到這裡可以對自己說聲辛苦了,打開網頁新增事項看看吧XD

當沒填寫名稱時:
https://ithelp.ithome.com.tw/upload/images/20181022/20106935Y8OlQakI6E.png

將所有資料輸入後新增:
https://ithelp.ithome.com.tw/upload/images/20181023/20106935lb7OtL8Kyy.png

確定後會關閉新增畫面,且再打開時欄位都清空了:
https://ithelp.ithome.com.tw/upload/images/20181022/201069356voyUV248W.png

然後用剛剛留下來的store驗證資料有沒有新增進去:
https://ithelp.ithome.com.tw/upload/images/20181023/201069352a2SRczay4.png

成功啦!雖然感覺這一次調整了很多地方,但是只要跟著框架的規則,一步一步錯基本上就不會出錯了!那下方附上今天的進度GitHub:
GitHub程式目錄連結
GitPage頁面連結


明天終於可以來把資料輸出出來了!等看得到資料後,就會覺得作品像個樣子了XD,不過這樣子用文章記錄著作品慢慢完整的感覺還滿有趣的!至少是從來沒有過的體驗,如果有任何建議還麻煩各位大大留言告訴我,小弟我會非常感激的,哈哈哈。

最後也感謝各位大大的觀看,如果有文章中有任何錯誤或是講解不清楚的地方,還麻煩大家留言告訴我,小弟會盡快修正或補充文章內容的!謝謝大家/images/emoticon/emoticon41.gif


上一篇
[筆記][React]來做個作品吧!待辦事項「todolist」篇(4)-Redux登板,建立store
下一篇
[筆記][React]來做個作品吧!待辦事項「todolist」篇(6)-上吧!迴圈!
系列文
一步一腳印的React旅程30

尚未有邦友留言

立即登入留言