以v-model与promise两种方式实现vue弹窗组件
最近公司有一个后台业务虽然也是写在了现有的后台系统中,但是之后要为这个业务单独拉出来新建一个后台系统,所以现有的后台系统中的vue组件库,就不能用了(因为不知道将来的系统要基于什么组件库,以防给未来移植项目带来麻烦),这次业务中又遇到了弹窗的功能,所以只能手动写一个了(虽然说弹窗组件很简单,也是想自己总结一下,有不对的地方也请指出),一开始用传统的props,$emit但是觉得要接两个取消与确认的回调这块的逻辑分散了所以就用了promise两个回调的方式把两个回调写在了一起,并不一定好,算是提供一种思路吧。
一.概览
先看最后的调用方式
props $emit方式
<chat-modal ref="chat-modal" v-model="showModal" cancelText="取消" sureText="确认" title="弹窗标题" small @on-ok="onOK" @on-cancel="onCancel"> <div>slot的东西,想向弹窗中添加自定义的内容</div> </chat-modal> methods: { display() { this.showModal = true;//交互点击手动触发显示弹窗 }, onOK() {},//点击确认的回调 onCancel() {}//点击取消的回调 }
promise的回调方式
<chat-modal ref="chat-modal"></chat-modal> methods: { display() { this.$refs["chat-modal"].openModal({ title: "弹窗标题", sureText: "确认", cancelText: "取消" }).then(res => { //点击确认的回调 }, res => { //点击取消的回调 }) } }
第二种方式的好处就是把所有的逻辑都集中到了一个方法里。
二.看下组件的源码
tip: 样式有些烂...
<template> <div> <div class="shadow" v-show="showModal"></div> <div class="modal" :class="{'smSize': otherText.small || small}" v-show="showModal"> <div class="header">{{ otherText.title || title}}</div> <div class="body"> <slot></slot> </div> <div class="footer"> <div class="item success" id="sure" ref="sure" @click="makeSure" v-show="otherText.sureText || sureText">{{ otherText.sureText || sureText }}</div> <div class="item red" id="cancel" ref="cancel" @click="makeCancel" v-show="otherText.cancelText || cancelText">{{ otherText.cancelText || cancelText }}</div> </div> </div> </div> </template> <script> //此组件提供两种调用方法,可以在组件上v-model一个表示是否显示弹窗的对话框,然后需要的一些值通过props传入,然后$emit在组件上@监听做回调 //第二中方法所有的传值回调都只需要在组件内部的一个方法调用然后在组件外部this.$refs[xxx].open调用然后.then触发回调,比上一种方便些 var initOtherText = { sureText: "", cancelText: "", title: "", small: false }; export default { props: { title: { type: String }, sureText: { type: String }, cancelText: { type: String }, value: { type: Boolean }, small: { type: Boolean } }, watch: { value(newVal) { this.showModal = newVal; } }, data() { return { otherText: JSON.parse(JSON.stringify(initOtherText)), showModal: this.value }; }, methods: { makeSure() { this.$emit("on-ok"); this.$emit("input", false); }, makeCancel() { this.$emit("on-cancel"); this.$emit("input", false); }, openModal(otherText) { this.otherText = { ...otherText }; this.showModal = true; var pms = new Promise((resolve, reject) => { this.$refs["sure"].addEventListener("click", () => { this.showModal = false; resolve("点击了确定"); }); this.$refs["cancel"].addEventListener("click", () => { this.showModal = false; reject("点击了取消"); }); }); return pms; } } }; </script> <style lang="scss" scoped> .shadow { background-color: rgba(0, 0, 0, 0.5); display: table; height: 100%; left: 0; position: fixed; top: 0; transition: opacity 0.3s ease; width: 100%; z-index: 50; } .modal { display: table-cell; vertical-align: middle; overflow-x: hidden; position: fixed; background-color: white; box-shadow: rgba(0, 0, 0, 0.33) 0px 2px 8px; border-radius: 5px; outline: 0px; overflow: hidden; transition: all 0.3s ease; width: 600px; height: 400px; top: 50%; left: 50%; margin-top: -200px; margin-left: -300px; } .header { align-items: center; background-color: #62a39e; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.16); color: #fff; font-weight: bold; display: -ms-flexbox; display: flex; height: 3.5rem; padding: 0 1.5rem; position: relative; z-index: 1; } .body { align-items: center; padding: 1.5rem; } .footer { justify-content: flex-end; padding: 1.5rem; position: absolute; bottom: 0; width: 100%; float: right; } .item { color: white; text-align: center; border-radius: 5px; padding: 10px; cursor: pointer; display: inline-block; } .info { background-color: #2196f3; } .success { background-color: #62a39e; } .red { background-color: #e95358; } .smSize { height: 200px; } </style>
首先分析一下第一种方式: 调用者需要在组件外部v-model上绑定一个变量(例中为showModal)来指示弹窗是否显示,显示的时候需要在组件外部手动设置 this.showModal = true
,组件内部props定义一个属性来接这个值为 value: {type: Boolean} ,同时在组件内部在用声明一个变量用来同步外部传进来的props值 默认值为 showModal: this.value (内部声明的值也叫了showModal),在watch中监听进行同步 watch: { value(newVal) { this.showModal = newVal } }
;然后把组件内部的这个showModal值绑定在需要显示或者隐藏的DOM元素上。向外抛出事件的时候是在点击组件内部的确定与关闭按钮时候
makeSure() { this.$emit("on-ok"); this.$emit("input", false); }, makeCancel() { this.$emit("on-cancel"); this.$emit("input", false); }
this.$emit("on-ok");this.$emit("on-cancel");
这两句的是向外抛出事件在组件外部@接一下然后写自己需要的回调函数。这时就可以实现弹窗的显示与隐藏了,你可能发现并没有一句代码去设置this.showModal = false;弹窗就隐藏了。主要是因为这几句代码 v-model = 'showModal' 和 组件内部的 props: {value: {type: Boolean}} this.$emit("input", false)
。v-model其实是vue的语法糖, <chat-modal v-model="showModal">
其实可以写为 <chat-modal :value="showModal" @input="showModal = arguments[0]">
所以要求我们在组件内部必须规定props的名字必须为value, 然后在组件内部触发确定或者取消的时候在组件内部触发 this.$emit("input", false)
这样实现了直接隐藏弹窗而不必打扰用户让用户在组件外部在手动将showModal置为false.
然后来看promise的方式: 第一种方式传进来的值都通过props来接的,这种方式通过在组件内部定义了另一个对象来接传进来的值,
var initOtherText = { sureText: "", cancelText: "", title: "", small: false }; otherText: JSON.parse(JSON.stringify(initOtherText)),
然后在menthods里定义了一个名为openModal的方法,然后把传进来的一系列参数赋值给组件内部的对象 this.otherText = { ...otherText }; this.showModal = true
; 并且将showModal置为true,然后每次触发的时候新建一个promise对象,里面的异步事件为点击确定和取消的两个点击事件,这里要操作DOM了
this.$refs["sure"].addEventListener("click", () => { this.showModal = false; resolve("点击了确定"); });
获取确定按钮的DOM元素绑定点击事件,回调里将showModal置为false并且resolve,
this.$refs["cancel"].addEventListener("click", () => { this.showModal = false; reject("点击了取消"); });
获取取消按钮的DOM绑定点击事件,回调里reject.
遇到的坑
这之前遇到了一个坑,因为第一次已经绑定了点击事件,第二次resolve和reject就会失败,本想取消一下绑定事件,但是因为将整个弹窗v-show="showModal"
的原因整个DOM被display:none;了就不需要手动解绑了。 第二个是关于用v-if还是v-show来隐藏弹窗,一开始用的是v-if但是发现在这步时
this.showModal = true; var pms = new Promise((resolve, reject) => { this.$refs["sure"].addEventListener.xxx//省略 }); return pms;
将showModal置为true时然后就去绑定事件这时候还没有DOM还没有解析玩DOM树上还没有,要不就得用this.$nextTick增加了复杂度,最后采用了v-show;
关于优先级问题
如果既在组件上用prop传了值(title,sureText之类的)如 <chat-modal" title="xx" sureText="xxx"></chat-modal>
也在方法里传了
this.$refs["chat-modal"].openModal({ title: "服务小结", sureText: "提交并结束", cancelText: "取消" }).then();
是以方法的优先级为高,在组件内部DOM元素上通过||设置了优先级,比如 <div class="header popModal">{{ otherText.title || title}}</div>
有方法的值取方法的值,没有取props得值。
总结