react 项目实战(七)用户编辑与删除
添加操作列
编辑与删除功能都是针对已存在的某一个用户执行的操作,所以在用户列表中需要再加一个“操作”列来展现【编辑】与【删除】这两个按钮。
修改/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。
项目目录: