添加操作列
編輯與刪除功能都是針對已存在的某一個用戶執行的操作,所以在用戶列表中需要再加一個“操作”列來展現【編輯】與【刪除】這兩個按鈕。
修改/src/pages/UserList.js
文件,添加方法handleEdit與handleDel,並在table中添加一列:
... class UserList extends React.Component { constructor (props) { ... } componentWillMount () { ... } // 編輯 handleEdit (user) { } // 刪除 handleDel (user) { } render () { const {userList} = this.state; return ( <HomeLayout title="用戶列表"> <table> <thead> <tr> <th>用戶ID</th> <th>用戶名</th> <th>性別</th> <th>年齡</th> <th>操作</th> </tr> </thead> <tbody> { userList.map((user) => { return ( <tr key={user.id}> <td>{user.id}</td> <td>{user.name}</td> <td>{user.gender}</td> <td>{user.age}</td> <td> <a href="javascript:void(0)" onClick={() => this.handleEdit(user)}>編輯</a> <a href="javascript:void(0)" onClick={() => this.handleDel(user)}>刪除</a> </td> </tr> ); }) } </tbody> </table> </HomeLayout> ); } } ...
點擊編輯(刪除)時,會把該行的user對象作為參數傳給handleEdit(handleDel)方法,在handleEdit(handleDel)方法中我們就可以根據傳入的user對象進行相應的操作了。
用戶刪除
用戶刪除比較簡單,先解決它。
在執行刪除數據的操作時,通常需要對操作進行進一步的確認以避免誤刪數據釀成慘劇。
所以在handleDel方法中我們應該先確認用戶是否想要執行刪除操作,在用戶確認后調用刪除用戶的接口來刪除用戶:
... // 刪除 handleDel (user) { const confirmed = window.confirm(`確定要刪除用戶 ${user.name} 嗎?`); // confirm 無法識別,需要加 window. if (confirmed) { fetch('http://localhost:8000/user/' + user.id, { method: 'delete' }) .then(res => res.json()) .then(res => { this.setState({ userList: this.state.userList.filter(item => item.id !== user.id) }); alert('刪除用戶成功'); }) .catch(err => { console.error(err); alert('刪除用戶失敗'); }); } } ...
用戶編輯
用戶編輯和用戶添加基本上是一樣的,不同的地方有:
- 用戶編輯需要將用戶的數據先填充到表單
- 用戶編輯在提交表單的時候調用的接口和方法不同
- 頁面標題不同
- 頁面路由不同
那么我們可以復制UserAdd.js文件的代碼到一個新的UserEdit.js文件中,再對上述四點進行修改…嗎?
當然不行!在前文中我們費盡心思對重復代碼進行優化,更不能為了偷懶直接復制代碼完事啦。
想辦法讓原來的代碼既能夠支持添加操作又能夠支持編輯操作!
為了達到這一個目標,我們需要:
- 升級formProvider使其返回的表單組件支持傳入表單的值(用於主動填充表單)
- 將UserAdd.js中的大部分代碼抽離到一個通用組件UserEditor,通過傳入不同的props來控制組件的行為是添加還是編輯
升級formProvider
修改/src/utils/formProvider.js
文件:
function formProvider (fields) { return function (Comp) { ... class FormComponent extends React.Component { constructor (props) { ... this.setFormValues = this.setFormValues.bind(this); } setFormValues (values) { if (!values) { return; } const {form} = this.state; let newForm = {...form}; for (const field in form) { if (form.hasOwnProperty(field)) { if (typeof values[field] !== 'undefined') { newForm[field] = {...newForm[field], value: values[field]}; } // 正常情況下主動設置的每個字段一定是有效的 newForm[field].valid = true; } } this.setState({form: newForm}); } handleValueChange (fieldName, value) { ... } render () { const {form, formValid} = this.state; return ( <Comp {...this.props} form={form} formValid={formValid} onFormChange={this.handleValueChange} setFormValues={this.setFormValues} /> ); } } return FormComponent; } } ...
給表單組件傳入了一個setFormValues的方法,用於在組件中主動設置表單的值。
完整代碼(高階組件):
src / utils / formProvider.js
/** * 高階組件 formProvider * 返回組件的組件(函數) * 使用高階組件可以在不修改原組件代碼的情況下,修改原組件的行為或增強功能 */ import React from 'react'; function formProvider (fields) { // fields 對象 return function(Comp) { // Comp /** * 定義常量 * 初始表單狀態 */ const initialFormState = {}; // 循環 for(const key in fields){ initialFormState[key] = { value: fields[key].defaultValue, error: '' }; } // 創建組件 class FormComponent extends React.Component { // 構造器 constructor(props) { super(props); // 定義初始狀態 this.state = { form: initialFormState, formValid: false // 加了一個formValid用來保存整個表單的校驗狀態 }; // 輸入框改變事件 綁定this this.handleValueChange = this.handleValueChange.bind(this); // 設置表單的值 this.setFormValues = this.setFormValues.bind(this); } // 輸入框改變事件 handleValueChange(fieldName, value){ // 定義常量 const { form } = this.state; const newFieldState = {value, valid: true, error: ''}; const fieldRules = fields[fieldName].rules; // 循環 for(let i=0; i<fieldRules.length; i++){ const {pattern, error} = fieldRules[i]; let valid = false; if(typeof pattern === 'function'){ valid = pattern(value); }else{ valid = pattern.test(value); } if(!valid){ newFieldState.valid = false; newFieldState.error = error; break; } } /** * ... 擴展運算符 * 將一個數組轉為用逗號分隔的參數序列 */ const newForm = {...form, [fieldName]: newFieldState}; /** * every * 對數組中的每個元素都執行一次指定的函數,直到此函數返回 false * 如果發現這個元素,every 將返回 false * 如果回調函數對每個元素執行后都返回 true,every 將返回 true */ const formValid = Object.values(newForm).every(f => f.valid); // 設置狀態 this.setState({ form: newForm, formValid }); } /** * 設置表單的值 */ setFormValues(values){ if(!values){ return; } const { form } = this.state; /** * form 表單對象 * ...擴展運算符 */ let newForm = {...form}; for(const field in form){ if(form.hasOwnProperty(field)){ if(typeof values[field] !== 'undefined'){ newForm[field] = {...newForm[field], value: values[field]}; } // 正常情況下主動設置的每個字段一定是有效的 newForm[field].valid = true; } } // 設置狀態 this.setState({form: newForm}); } render(){ const { form, formValid } = this.state; return ( <Comp {...this.props} form={form} formValid={formValid} onFormChange={this.handleValueChange} setFormValues={this.setFormValues} /> ); } } // 返回組件 return FormComponent; } } export default formProvider;
抽離UserEditor
接下來新建/src/components/UserEditor.js
文件,將表單處理代碼從UserAdd.js里搬過去(省略號部分與原來的代碼相同):
import React from 'react'; import FormItem from '../components/FormItem'; // 或者寫成 ./FormItem import formProvider from '../utils/formProvider'; // 引入 prop-types import PropTypes from 'prop-types'; class UserEditor extends React.Component { handleSubmit (e) { ... } render () { const {form: {name, age, gender}, onFormChange} = this.props; return ( <form onSubmit={(e) => this.handleSubmit(e)}> ... </form> ); } } UserEditor.contextTypes = { router: PropTypes.object.isRequired }; UserEditor = formProvider({ ... })(UserEditor); export default UserEditor;
然后再handleSubmit方法中,通過檢查是否收到一個editTarget的props來判斷這次的操作是添加操作還是編輯操作,並根據當前的操作切換調用接口的url和method:
... handleSubmit (e) { e.preventDefault(); const {form: {name, age, gender}, formValid, editTarget} = this.props; if (!formValid) { alert('請填寫正確的信息后重試'); return; } let editType = '添加'; let apiUrl = 'http://localhost:8000/user'; let method = 'post'; if (editTarget) { editType = '編輯'; apiUrl += '/' + editTarget.id; method = 'put'; } fetch(apiUrl, { method, body: JSON.stringify({ name: name.value, age: age.value, gender: gender.value }), headers: { 'Content-Type': 'application/json' } }) .then((res) => res.json()) .then((res) => { if (res.id) { alert(editType + '用戶成功'); this.context.router.push('/user/list'); return; } else { alert(editType + '失敗'); } }) .catch((err) => console.error(err)); } ...
同時,我們也需要在UserEditor加載的時候檢查是否存在props.editTarget
,如果存在,使用props.setFormValues
方法將editTarget的值設置到表單:
... componentWillMount () { const {editTarget, setFormValues} = this.props; if (editTarget) { setFormValues(editTarget); } } ...
這樣我們的UserEditor就基本完成了,當我們要作為一個用戶添加器使用時,只需要:
... <UserEditor/> ...
而作為一個用戶編輯器使用時,則需要將編輯的目標用戶對象傳給editTarget這個屬性:
... <UserEditor editTarget={user}/> ...
完成代碼(編輯器組件):
src / components / UserEditor.js
/** * 編輯器組件 */ import React from 'react'; import FormItem from '../components/FormItem'; // 或寫成 ./FormItem // 高階組件 formProvider表單驗證 import formProvider from '../utils/formProvider'; // 引入 prop-types import PropTypes from 'prop-types'; class UserEditor extends React.Component { // 按鈕提交事件 handleSubmit(e){ // 阻止表單submit事件自動跳轉頁面的動作 e.preventDefault(); // 定義常量 const { form: { name, age, gender }, formValid, editTarget} = this.props; // 組件傳值 // 驗證 if(!formValid){ alert('請填寫正確的信息后重試'); return; } // 默認值 let editType = '添加'; let apiUrl = 'http://localhost:8000/user'; let method = 'post'; // 判斷類型 if(editTarget){ editType = '編輯'; apiUrl += '/' + editTarget.id; method = 'put'; } // 發送請求 fetch(apiUrl, { method, // method: method 的簡寫 // 使用fetch提交的json數據需要使用JSON.stringify轉換為字符串 body: JSON.stringify({ name: name.value, age: age.value, gender: gender.value }), headers: { 'Content-Type': 'application/json' } }) // 強制回調的數據格式為json .then((res) => res.json()) // 成功的回調 .then((res) => { // 當添加成功時,返回的json對象中應包含一個有效的id字段 // 所以可以使用res.id來判斷添加是否成功 if(res.id){ alert(editType + '添加用戶成功!'); this.context.router.push('/user/list'); // 跳轉到用戶列表頁面 return; }else{ alert(editType + '添加用戶失敗!'); } }) // 失敗的回調 .catch((err) => console.error(err)); } // 生命周期--組件加載中 componentWillMount(){ const {editTarget, setFormValues} = this.props; if(editTarget){ setFormValues(editTarget); } } render() { // 定義常量 const {form: {name, age, gender}, onFormChange} = this.props; return ( <form onSubmit={(e) => this.handleSubmit(e)}> <FormItem label="用戶名:" valid={name.valid} error={name.error}> <input type="text" value={name.value} onChange={(e) => onFormChange('name', e.target.value)}/> </FormItem> <FormItem label="年齡:" valid={age.valid} error={age.error}> <input type="number" value={age.value || ''} onChange={(e) => onFormChange('age', e.target.value)}/> </FormItem> <FormItem label="性別:" valid={gender.valid} error={gender.error}> <select value={gender.value} onChange={(e) => onFormChange('gender', e.target.value)}> <option value="">請選擇</option> <option value="male">男</option> <option value="female">女</option> </select> </FormItem> <br /> <input type="submit" value="提交" /> </form> ); } } // 必須給UserEditor定義一個包含router屬性的contextTypes // 使得組件中可以通過this.context.router來使用React Router提供的方法 UserEditor.contextTypes = { router: PropTypes.object.isRequired }; // 實例化 UserEditor = formProvider({ // field 對象 // 姓名 name: { defaultValue: '', rules: [ { pattern: function (value) { return value.length > 0; }, error: '請輸入用戶名' }, { pattern: /^.{1,4}$/, error: '用戶名最多4個字符' } ] }, // 年齡 age: { defaultValue: 0, rules: [ { pattern: function(value){ return value >= 1 && value <= 100; }, error: '請輸入1~100的年齡' } ] }, // 性別 gender: { defaultValue: '', rules: [ { pattern: function(value) { return !!value; }, error: '請選擇性別' } ] } })(UserEditor); export default UserEditor;
所以現在就可以將UserAdd.js文件改成這樣了:
/** * 用戶添加頁面 */ import React from 'react'; // 布局組件 import HomeLayout from '../layouts/HomeLayout'; // 編輯組件 import UserEditor from '../components/UserEditor'; class UserAdd extends React.Component { render() { return ( <HomeLayout title="添加用戶"> <UserEditor /> </HomeLayout> ); } } export default UserAdd;
添加UserEditPage
現在需要添加一個/src/pages/UserEdit.js
文件作為編輯用戶的頁面:
/** * 編輯用戶頁面 */ import React from 'react'; // 布局組件 import HomeLayout from '../layouts/HomeLayout'; // 引入 prop-types import PropTypes from 'prop-types'; // 編輯組件 import UserEditor from '../components/UserEditor'; class UserEdit extends React.Component { // 構造器 constructor(props) { super(props); // 定義初始化狀態 this.state = { user: null }; } // 生命周期--組件加載中 componentWillMount(){ // 定義常量 const userId = this.context.router.params.id; /** * 發送請求 * 獲取用戶數據 */ fetch('http://localhost:8000/user/' + userId) .then(res => res.json()) .then(res => { this.setState({ user: res }); }) } render() { const {user} = this.state; return ( <HomeLayout title="編輯用戶"> { user ? <UserEditor editTarget={user} /> : '加載中...' } </HomeLayout> ); } } UserEdit.contextTypes = { router: PropTypes.object.isRequired }; export default UserEdit;
在這個頁面組件里,我們根據路由中名為id的參數(this.context.router.params.id
)來調用接口獲取用戶數據(保存在this.state.user
中)。
當user數據未就緒時,我們不應該展示出編輯器以避免用戶混亂或者誤操作:使用三元運算符,當this.state.user
有值時渲染UserEditor組件,否則顯示文本“加載中…”。
注意:任何使用this.context.xxx的地方,必須在組件的contextTypes里定義對應的PropTypes。
別忘了在/src/index.js
中給頁面添加路由,路由的path中使用:id來定義路由的參數(參數名與頁面組件中獲取參數時的參數名相對應):
import UserEditPage from './pages/UserEdit'; // 用戶編輯頁面 ReactDOM.render(( <Router history={hashHistory}> ... <Route path="/user/edit/:id" component={UserEditPage}/> </Router> ), document.getElementById('root'));
完成handleEdit方法
最后,來補上UserList頁面組件的handleEdit方法:
import PropTypes from 'prop-types'; class UserList extends React.Component { constructor (props) { ... } componentWillMount () { ... } /** * 編輯 */ handleEdit (user) { // 跳轉編輯頁面 this.context.router.push('/user/edit/' + user.id); } handleDel (user) { ... } /** * 任何使用this.context.xxx的地方,必須在組件的contextTypes里定義對應的PropTypes */ UserList.contextTypes = { router: PropTypes.object.isRequired };
在handleEdit方法中只需要使用router.push方法跳轉到該用戶的編輯頁面,別忘了加上contextTypes。
項目目錄: