react antd-mobile ActionSheet+tag实现多选方式
作者:YING-FINE
实现效果如下
在手机下方弹出弹窗
选择月份点确定后
再次点击申请月份,之前已选的月份会自动选上
全部功能描述
1、点击输入框,自动弹出选择弹窗(使用actionsheet,这部分不过多阐释)
2、弹窗可多选的内容由后台接口传入(或自己写数据,看需求)
3、灰色部分为后台返回的不可选择的标签,不允许用户点击选择(disabled)
4、选择一个或多个标签后,点击确定,保存选择的标签,并在输入框中显示选择的内容。倘若没有点击确定,直接点取消或弹框外的区域关闭弹窗,则不会保存所选标签
4、再次点击输入框,弹窗会自动勾选上之前选过的弹窗。之后操作可如上循环。
功能实现
一、从后台获取所有月份信息
这里就是普通的通过redux的网络请求获取数据,按实际情况获取数据
getMouth = () => { const {dispatch} = this.props; dispatch({ type: `${NAMESPACE}/getMouth`, }); };
不允许点击的月份
const disabledMouth = [2,5,8];
按理说,不允许点击的月份也应该通过后台获取,此处因为还没拿到后台接口,暂时用全局变量充当不允许点击的月份数组
二、添加组件的state数据
this.state = { checkMouth: [],//保存已选择的月份 allMouth: [],//保存月份标签的属性数据 temCheckMouth: [],//临时保存已选择的鱼粉 temAllMouth: [],//临时保存月份标签的属性数据 sign: false//判断用户是否点击确定 }
三、定义含有标签属性的数组allMouth
getAllMouth = () => { //mouth即为第一步通过后台拿到的model里state的月份值 const mouth = this.props.mouth.data || ''; const newMonth = []; if (mouth && mouth.length > 0) { mouth.map(item => { newMonth.push( { value: item.code,//可理解为每个月份的id label: item.detail,//月份标签需展示的内容,如一月、二月等的字样 selected: false,//月份标签是否被选择 disabled: false//该月份是否不可点击 } ) }); } // 修改无法选择的月份信息,把无法选择的月份disabled置灰掉,不允许用户点击选择 disabledMouth.map(item => { newMonth[item].disabled = true; }); // console.log(newMonth); // 把每个月份的属性数据数组存入state this.setState({ allMouth: newMonth, temAllMouth: newMonth }) };
注:
一般后台传的数据只会有code和detail或其他内容,不会有selected和disabled,所以此处我们要在数组手动添加,给每个月份自己独有的selected值和disabled值。
在最初默认都是未选中和允许点击状态。
四、保存月份属性数组allMouth
componentDidMount() { let timer; new Promise(resolve => { this.getMouth(); timer = setInterval(() => { if (this.props.mouth.data) { resolve(); } }, 100); }).then(() => { this.getAllMouth(); clearInterval(timer); }); }
这部分代码其实我考虑了很久,因为这里有一个前提条件
必须通过getMouth()后台请求到数据之后,getAllMouth()才能访问数据,初始化月份数组数据并放到state
但是在这一步我报错了很久,因为倘若普通写法(如下)
this.getMouth();× this.getAllMouth();×
这一步虽然显示网络请求成功了,但是props.mouth并没有拿到数据!
这一步该如何写思考了很久,直接简单实用promise也不行
componentDidMount() { × new Promise(resolve => { × this.getMouth(); × resolve(); × }).then(() => { × this.getAllMouth(); × }); × } ×
此处getMouth()方法的确是执行完了,只是还没拿到数据,所以这样子写是不行的
而且componentDidMount正常来说只会执行一次,所以在componentDidMount执行完,而model的state还没拿到数据的时候,尽管过了一段时间拿到数据了this.getAllMouth(); 也不会再次执行了,所以初始化无法进行。
思前想后,能解决这一步的只能多次执行,确保拿到数据了,再进行this.getAllMouth();初始化数据。便有了上方代码的操作。
当然,开了定时器记得用完就关!这一步操作可能性能上不太友好,如果大家有什么更好的方法,请多多指教呀~
五、遍历显示月份
上面步骤成功后,可以看到我们初始化的月份数组allMouth是这样的,接下来就可以让他们展示出来了
(注:0号全选是后台一同传过来的,大家可忽略)
input框:
<div onClick={this.handleMouth}> <InputItem placeholder="请选择申请月份" readonly="readonly" clear > 申请月份 </InputItem> </div>
点击后弹出弹窗
主要是使用ActionSheet弹出弹窗显示内容,不多阐述。详情可见antd-mobile文档
handleMouth = () => { const {allMouth} = this.state; //这一步是为了删去数组第一条全选数据,全选不需要显示。可忽略 const newMouthData = allMouth && allMouth.slice(1); const modalData = ( <div className={styles.modalStyle}> <div className={styles['tag-body']}> <div style={{textAlign: 'left'}}>可选择月份(可多选)</div> <div className={styles['tag-box']}> { newMouthData && newMouthData.map((item) => { return ( <Tag data-seed="logId" onChange={(selected) => this.handleOnTagChange(selected, item.value)} // 用户点击不同月份时触发的方法(详情见下方标题六) selected={item.selected}// 控制不同月份的选中状态 disabled={item.disabled}// 控制不同月份是否可点击 > {item.label} </Tag> ) }) } </div> </div> </div> ) const button1 = ( <div className={styles.buttonStyle}> <span>取消</span> <span onClick={this.handleConfirm}>确定</span>//详情请见标题七的内容 </div> ) const BUTTONS = [modalData]; ActionSheet.showActionSheetWithOptions({ options: [button1], message: BUTTONS, }, // 关闭页面执行的方法 (buttonIndex) => { this.closePage();//详情请见标题八的内容 }); };
六、选择不同月份触发事件
handleOnTagChange = (selected, value) => { /** * 用户在弹窗选择月份时,虽然当前处于选中状态。 * 但是倘若用户选择完毕时不点击确定,而且直接关闭页面 * 则之前选中的月份都不应该保存!所以此处用临时数组进行保存 * 只有用户点击确定,才把选择的所有内容保存到正式的数组中 */ const {temAllMouth, temCheckMouth} = this.state; // 修改月份对应的selected属性 const newAllMouth = JSON.parse(JSON.stringify(temAllMouth)); newAllMouth[value].selected = selected; // console.log(newAllMouth); // 记录选中的月份 const newCheckMouth = JSON.parse(JSON.stringify(temCheckMouth)); if (selected) {//如果选中,把选中的月份添加进数组 newCheckMouth.push(parseInt(value)); } else {//如果是取消选中,需要把之前的选中从数组中删除 const deleteMouth = newCheckMouth.findIndex(item => item == value); newCheckMouth.splice(deleteMouth, 1); } // console.log(newCheckMouth); this.setState({ temCheckMouth: newCheckMouth, temAllMouth: newAllMouth }); };
关于const newAllMouth = JSON.parse(JSON.stringify(temAllMouth))的解释:
这一步其实我绕了很多弯路,最后一步步排除错误才发现我犯了个超级低级的错误!
因为这个方法中,我们很明显需要修改数组,但是我们不能直接修改state的数据
所以必须在方法中深拷贝state的数组,对新数组进行修改,最后再通过setstate把新数组赋值给state的数组。
而在写代码最初,我是直接
const {temAllMouth, temCheckMouth} = this.state; const newAllMouth = temAllMouth;×××××错误范例!!!
很明显这样写是错误的!直接赋值是把temAllMouth的地址赋值给了newAllMouth ,两者其实没有区别,修改newAllMouth 也会修改到state的数据temAllMouth,故出现问题。
所以最后直接把temAllMouth通过JSON深拷贝一份,保证newAllMouth与temAllMouth不相同,互不干扰。
七、点击确定触发实现
handleConfirm = () => { const {temCheckMouth, temAllMouth} = this.state; /** * 对选中月份进行排序 * 因为关闭弹窗后需要在input框中显示用户选择的月份 * 而用户选择的顺序是不定,所以temCheckMouth保存的选中月份也是乱的 * 故此处为了美观,对选中月份进行了排序 * 使用的是最简单的冒泡排序,可以忽略 */ const newCheckMouth = JSON.parse(JSON.stringify(temCheckMouth)); if (newCheckMouth && newCheckMouth.length > 1){ newCheckMouth.map((item,index) => { for (let j = 0; j<newCheckMouth.length-index;j+=1){ if (newCheckMouth[j] > newCheckMouth[j + 1]){ const temp = newCheckMouth[j]; newCheckMouth[j] = newCheckMouth[j + 1]; newCheckMouth[j + 1] = temp; } } }); } console.log('选中的月份--' + newCheckMouth); // 此时用户点击确定按钮,需要把之前保存的临时数据赋值给正式的数组 this.setState({ allMouth: temAllMouth, checkMouth: newCheckMouth, temCheckMouth: newCheckMouth, sign: true //确定页面 }); // 把选中的月份展示在input框,只有用户有选择月份的情况才需要展示 if (temCheckMouth){ this.props.form.setFieldsValue({ sqzzjtyf: newCheckMouth.toString() }); } }
八、关闭弹窗触发事件
注:这里关闭弹窗并不是点击取消或者点击弹窗外的手机部分关闭弹窗时才会触发!!
点击确定之后关闭弹窗同样会触发!!!!没注意这部分就会出现错误!
closePage = () => { setTimeout(()=>{ const {checkMouth, allMouth, sure} = this.state; // sure=false代表用户没有点击确定,则需要把之前保存的临时数据重置,保证用户下次打开弹窗时数据的正确性 if(!sure){ this.setState({ temCheckMouth: checkMouth, temAllMouth: allMouth }); }else{ // 把点击确定时修改的sure=true修改回false,保证下次再次打开弹窗的正确性 this.setState({ sure: false }); } },50) }
此处使用settimeout的原因:
上面也说了,这里调用的方法是只要关闭了弹窗就会执行,而该方法的执行顺序与用户点击确定的执行顺序应该是不相上下的(此处未做深入研究,不确定两者的执行顺序)
而这里我们有一个必须前提,关闭弹窗的这个方法必须在点击确定的方法后面执行,原因如下:
在点击确定方法后,我们会设置sure=true,并且把checkMouth和allMouth设置为新的值
而倘若关闭弹窗方法没有在确定方法后执行,则关闭弹窗方法执行的时候,sure值仍为false!并且checkMouth和allMouth也扔为旧值
这样,该方法就会执行
temCheckMouth: checkMouth(旧值), temAllMouth: allMouth(旧值)
如此,用户之前点击确定应该要保存的数据就没有保存成功了!错误就出现了!
故此处使用settimeout保证该方法执行的延后!
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。