添加操作列
編輯與刪除功能都是針對已存在的某一個用戶執行的操作,所以在用戶列表中需要再加一個“操作”列來展現【編輯】與【刪除】這兩個按鈕。
修改/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。

項目目錄:

