이 글에서는 부트스트랩의 팝업 창 구성 요소인 Modal을 소개하겠습니다. 이 구성 요소는 간단하고 사용하기 쉽고 아름다운 효과가 있으며 매우 실용적입니다!
브라우저에서 제공하는 경고 및 확인 상자의 경험이 좋지 않고 브라우저가 사용자 정의 HTML을 대화 상자 형태로 표시하는 표준 팝업 기능을 제공하지 않기 때문에 많은 프로젝트에서 대화 상자 구성 요소를 사용자 정의합니다. 이 기사에서는 프로젝트의 부트스트랩 모달 구성 요소를 기반으로 경고, 확인 및 모달 대화 상자를 사용자 정의한 경험을 소개합니다. 이는 비교적 간단하고 실용적인 참고 자료가 되기를 바랍니다.
1. 예시 디스플레이
위에 제공된 다운로드 링크에서 소스 코드를 다운로드하면 자세한 코드를 이해할 수 있습니다. 세 가지 구성 요소를 합치면 200줄이 넘습니다.
JavaScript 컴포넌트 개발 경험이 있다면 이 수준에서는 제 코드를 금방 이해할 수 있을 거라 믿습니다. 또한 실제 요구 사항에 더 가까운 시나리오를 시뮬레이션하는 소스 코드 데모도 제공합니다.
1) 사용자는 인터페이스의 버튼을 클릭하여 이전에 정의된 모달 상자를 엽니다.
2) 사용자가 열린 모달 상자에서 일부 양식을 작성하고 확인을 클릭하면 일부 확인이 시작됩니다.
이메일을 기재하지 않은 경우:
이메일을 작성한 후:
이 두 프롬프트는 실제로 경고 및 확인의 효과를 보여주기 위해 삽입된 것입니다. 실제로는 그렇게 어색한 기능이 없을 수도 있습니다.
3) 비밀번호가 비어 있다는 메시지가 표시되면 조심스러운 사람들은 확인 버튼이 비활성화된 상태라는 것을 알게 될 것입니다. 이는 비동기 작업이 성공적으로 완료되기 전에 확인 버튼이 궁극적으로 일부 비동기 작업을 완료하기 때문입니다. 모달 구성 요소는 닫히지 않으며 클릭한 버튼은 반복적으로 클릭되지 않도록 제어할 수 있습니다.
4) setTimeout을 사용하여 비동기 작업을 시뮬레이션했습니다. 이 비동기 작업은 확인 버튼을 클릭한 후 3s를 콜백하고 다음을 수행합니다.
이메일을 admin@admin으로 입력하면 제출 성공 메시지가 표시되고 확인 후 모든 팝업 상자가 닫힙니다.
이메일에 다른 값을 입력하면 제출 실패 메시지가 표시되고 해당 위치에 모달 상자가 계속 표시됩니다.
구성요소 정의, 특히 등록 버튼에서 일부 AOP 프로그래밍 처리를 추가하고 jquery의 지연 개체를 사용하여 필요한 비동기 프로그래밍을 구현했습니다. 자세한 내용은 소스 코드를 읽어보세요. 궁금한 점이 있으면 댓글을 남겨주세요. 지구교류 및 교육.
2. 구성요소 요구사항
때로는 유용한 컴포넌트를 작성하기 위해 대략적인 프로토타입과 외부에 제공할 인터페이스만 결정하면 되지만, 아직 코딩은 시작되지 않았지만 컴포넌트 작성이라는 가장 중요한 작업은 완료됩니다. . 이번 글에서 작성할 컴포넌트를 예로 들면, 제가 원하는 컴포넌트의 프로토타입과 호출 형태는 다음과 같습니다.
1) 맞춤 알림 상자
프로토타입은 다음과 같습니다.
호출 시 최대 두 개의 매개변수가 필요합니다. 하나의 msg는 표시할 프롬프트 내용을 전달하는 데 사용되고, 하나의 onOk는 확인 버튼을 클릭할 때 콜백을 처리하는 데 사용됩니다.
//1 Alert('您选择的订单状态不符合当前操作的条件,请刷新列表显示最新数据后再继续操作!'); //2 Alert({ msg: '您选择的订单状态不符合当前操作的条件,请刷新列表显示最新数据后再继续操作!', onOk: function(){ } });
第一种是没有回调的情况,那么直接传递msg即可,第二种是有回调的情况,用options对象的方式来传递msg和onOks回调这两个参数。不管onOk回调有没有,点击按钮的时候都要关闭弹框。
2)自定义confirm框
这个框的原型跟alert框只差一个按钮:
调用形式只有一种:
Confirm({ msg: '您选择的订单状态不符合当前操作的条件,请确认是否要继续操作!', onOk: function(){ }, onCancel: function(){ } });
onCancel是在点击取消按钮时候的回调。不管onOk和onCancel回调有没有,点击按钮的时候都要关闭弹框。onCancel回调可以没有。
3)自定义modal框
原型:
调用形式:
var modal = new Modal({ title: '', content: '', width: 600, buttons: [ { html: '<button type="button" class="btn btn-sm btn-primary btn-ok">确定</button>', selector: '.btn-ok', callback: function(){ //点击确定按钮的回调 } }, { html: '<button type="button" class="btn btn-sm btn-default btn-cancel">取消</button>', selector: '.btn-cancel', callback: function(){ //点击取消按钮的回调 } } ], onContentReady: function(){ //当modal添加到DOM并且初始化完毕时的事件回调,每个modal实例这个回调只会被触发一次 }, onContentChange: function(){ //当调用modal.setContent类似的方法改变了modal内容时的事件回调 }, onModalShow: function(){ //当调用modal.open类似方法显示modal时都会触发的事件回调 }, onModalHide: function(){ //当调用modal.hide类似方法隐藏modal时都会触发的事件回调 } }); $('#btn-audit').click(function(){ modal.open(); });
跟Alert和Confirm不同的是,一个页面里面只需要一个Alert和Confirm的实例,但是可能需要多个Modal的实例,所以每个Modal对象都需要单独new一下。由于每个Modal要完成的事情都不相同,所以:
需要一个title参数来设置名称,表达这个Modal正在处理的事情;
content参数表示Modal的html内容;
width参数设置Modal的宽度,Modal的高度保持auto;
buttons参数用来配置这个Modal上面的按钮,一般情况下Modal组件只需要两个按钮(确定和取消)就够了,但也有少数情况需要多个按钮,所以把按钮做成配置的方式相对灵活一点,每个按钮用三个参数来配置,html表示按钮的html结构,selector方便注册回调的时候通过事件委托的方式来处理,callback配置按钮点击时的回调;
onContentReady这个事件回调,可以在Modal初始化完毕的时候,主动去初始化Modal内部html的一些组件;由于组件初始化一般只进行一次,所以放在这个回调里面最合适;
onContentChange回调,在一个Modal需要被用作不同的场景,显示不同的HTML的内容时会派上用场,但是不是非常的好用,处理起来逻辑会稍微偏复杂,如果一个Modal实例只做一件事情的时候,onContentChange这个回调就用不到了;
onModalShow这个回调在每次显示Modal的时候都会显示,使用的场景有很多,比如某个Modal用来填写一些表单内容,下次填写的时候需要reset一下表单才能给用户使用,这种处理在这个回调里面处理就比较合适;
onModalHide这个回调有用,不过能够用到的场景不多,算是预留的一个接口。
4)其它需求
所有类型的弹框都做成虚拟模态的形式,显示框的同时加一个遮罩;
所有框都不需要支持拖动和大小调整;
alert和dialog框的标题,按钮数量、按钮位置、按钮文字都固定。
实际上:
遮罩这个效果,bootstrap的modal组件本身就已经支持了;
拖动和大小调整,这个功能属于锦上添花,但是对软件本身来说,并一定有多少额外的好处,所以我选择不做这种多余的处理;
alert和dialog不需要做太过个性化,能够统一风格,改变浏览器原生的弹框体验即可。
5)DEMO中调用实例
接下来演示下我在完成这三个组件开发之后,实际使用过程中调用这些组件的方式:
var modal = new Modal({ title: '测试modal', content: $('#modal-tpl').html(), width: 500, onOk: function(){ var $form = this.$modal.find('form'); var data = $form.serializeArray(); var postData = {}; data.forEach(function(obj){ postData[obj.name] = obj.value; }); if(!postData.email) { Alert('请输入EMAIL!'); return false; } var deferred = $.Deferred(); if(!postData.password) { Confirm({ msg: 'Password为空,是否要继续?', onOk: function(){ _post(); }, onCancel: function(){ deferred.reject(); } }) } else { _post(); } return $.when(deferred); function _post(){ //模拟异步任务 setTimeout(function(){ if(postData.email === 'admin@admin') { Alert({ msg: '提交成功!', onOk: function(){ deferred.resolve(); } }); } else { Alert({ msg: '提交失败!', onOk: function(){ deferred.reject(); } }); } },3000); } }, onModalShow: function () { var $form = this.$modal.find('form'); $form[0].reset(); } }); $('#btn-modal').click(function () { modal.open(); });
3. 实现要点
1)最基础的一点,要对bootstrap的modal组件源码有所了解:
初始化方式:$modal.modal()
打开:$modal.modal('show')
关闭:$modal.modal(hide)
事件:bootstrap大部分带过渡效果的组件的事件都是成对的,并且一个是现在时,一个是完成时,modal组件定义了2对:
show.bs.modal和shown.bs.modal,hide.bs.modal和hidden.bs.modal。
这两对事件分别在打开和关闭的过渡效果执行前后触发。从我要定义的组件需求来说,定义组件的时候需要show.bs.modal和hidden.bs.modal这两个事件,在侦听到bootstrap的modal组件派发这两个事件的时候,派发自己定义的组件的事件:
modalShow和modalHide。
选项:
backdrop: 是否显示遮罩;
keyboard: 是否支持键盘回调;
show:是否在初始化完毕就立即显示。
这三个选项默认都是true,但是在我定义组件的时候,我都配置成了false,键盘回调这种特性暂时不考虑,所以配置为true;当
调用bootstrap的modal初始化的时候当然不能立即显示弹框,所以也不能配置为true;backdrop配置为false的原因在下一点介绍。
2)遮罩处理
如果启用bootstrap的遮罩,会发现在点击遮罩部分的时候,弹框就会自动关掉了,这不是我期望的虚拟模态效果,所以必须把backdrop配置为false。但是把这个选项配置为false之后,又会引发一个新问题,就是组件没有了遮罩效果,所以为了兼顾这两个问题,只能自己写一个简单的遮罩处理:
var $body = $(document.body), BackDrop = (function () { var $backDrop, count = 0, create = function () { $backDrop = $('<div class="modal-backdrop fade in"></div>').appendTo($body); }; return { show: function () { !$backDrop && create(); $backDrop[0].style.display = 'block'; count++; }, hide: function () { count--; if (!count) { $backDrop.remove(); $backDrop = undefined; } } } })();
这段代码中引入count变量的原因是因为BackDrop是一个全局的单例对象,当调用多个modal实例的open方法的时候,都会调用BackDrop的show方法,为了保证在调用BackDrop的hide方法时,能够确保在所有的modal实例都关闭之后再隐藏Backdrop,所以就加了一个count变量来记录BackDrop的show方法被调用了多少次,只有当count为0的时候,调用BackDrop的hide方法才会真正隐藏BackDrop。
3)组件的选项的默认值定义
ModalDialog.defaults = { title: '', content: '', width: 600, buttons: [ { html: '<button type="button" class="btn btn-sm btn-primary btn-ok">确定</button>', selector: '.btn-ok', callback: getDefaultBtnCallbackProxy('onOk') }, { html: '<button type="button" class="btn btn-sm btn-default btn-cancel">取消</button>', selector: '.btn-cancel', callback: getDefaultBtnCallbackProxy('onCancel') } ], onOk: $.noop, onCancel: $.noop, onContentReady: $.noop, onContentChange: $.noop,//content替换之后的回调 onModalShow: $.noop, onModalHide: $.noop//modal关闭之后的回调 };
通过buttons配置两个默认的按钮,确定和取消,然后为了简化这两个默认按钮的回调配置,把这两个按钮的接口进一步扩展到了上一级别,onOk和onCancel分别会在点击确定和取消的时候被调用,这两个选项完全是函数回调,不像onContentReady这种属于事件回调。getDefaultBtnCallbackProxy用来辅助注册onOk和onCancel:
var getDefaultBtnCallbackProxy = function (callbackName) { return function () { var opts = this.options, callback = opts[callbackName] && typeof opts[callbackName] === 'function' ? opts[callbackName] : ''; return callback && callback.apply(this, arguments); } }
里面的this会被绑定到modal实例上。
4)构造函数:
function ModalDialog(options) { this.options = this.getOptions(options); this.$modal = undefined; this.$modalTitle = undefined; this.$modalBody = undefined; this.$modalFooter = undefined; this.state = undefined; }
这个主要是声明了用到的一些实例变量。
5)关键的原型方法和函数
open: function (state) { this.state = state; !this.$modal && initModal(this, this.options); BackDrop.show(); this.$modal.modal('show'); }
这是个原型方法,组件的初始化也是在这个方法调用的时候执行的(延迟初始化)。
initModal = function (that, opts) { var $modal = createModal(that); that.setTitle(opts.title); that.setContent(opts.content); that.addButtons(opts.buttons); that.setWidth(opts.width); bindHandler(that, opts); $modal.modal();//调用bootstrap的Modal组件 $modal.trigger('contentReady'); }
这是个函数,用来初始化组件。其中:
setTitle是个原型方法,用来设置modal的标题;
setContent是个原型方法,用来设置modal的html内容;
addButtons是个原型方法,用来注册按钮;
setWidth是个原型方法,用来设置modal的宽度;
bindHandler是个函数,用来注册modal的那些事件;
倒数第二步调用$modal.modal()初始化bootstrap的modal组件;
最后一步触发contentReady事件。
bindHandler源码:
bindHandler = function (that, opts) { var $modal = that.$modal; typeof opts.onContentChange === 'function' && $modal.on('contentChange', $.proxy(opts.onContentChange, that)); typeof opts.onContentReady === 'function' && $modal.on('contentReady', $.proxy(opts.onContentReady, that)); typeof opts.onModalShow === 'function' && $modal.on('modalShow', $.proxy(opts.onModalShow, that)); typeof opts.onModalHide === 'function' && $modal.on('modalHide', $.proxy(opts.onModalHide, that)); $modal.on('show.bs.modal', function () { $modal.trigger('modalShow'); }); $modal.on('hidden.bs.modal', function () { $modal.trigger('modalHide'); }); }
为了方便使用,我把onContentChange这几个回调的上下文绑定到了当前的modal实例。最后两个事件侦听就是把bootstrap的事件封装成了我定义的modal事件。
addButtons源码:
addButtons: function (buttons) { var buttons = !$.isArray(buttons) ? [] : buttons, that = this, htmlS = []; buttons.forEach(function (btn) { htmlS.push(btn.html); btn.selector && that.$modal.on('click', btn.selector, $.proxy(function (e) { var self = this, $btn = $(e.currentTarget); //先禁用按钮 $btn[0].disabled = true; var callback = typeof btn.callback === 'function' ? btn.callback : '', ret = callback && callback.apply(self, arguments); if (ret === false) { $btn[0].disabled = false; return; } if (typeof(ret) === 'object' && 'done' in ret && typeof ret['done'] === 'function') { //异步任务只有在成功回调的时候关闭Modal ret.done(function () { that.hide(); }).always(function () { $btn[0].disabled = false; }); } else { $btn[0].disabled = false; that.hide(); } }, that)); }); this.$modalFooter.prepend($(htmlS.join(''))); }
从这个代码可以看出:
当按钮点击之后,按钮就会被禁用;
当按钮返回false的时候,按钮恢复,但是modal不会被关闭,说明当前的一些操作被代码给拦下来了;
当按钮返回的是一个延迟对象的时候,会等到延迟对象完成的时候才会恢复按钮,并且只有在延迟对象resolve的时候才会关闭modal;
否则就恢复按钮,并主动关闭modal。
在这段代码里面考虑了:
按钮的防重复点击,modal的自动关闭以及异步任务的处理。
6)封装Alert和Confirm
Alert和Confirm其实就是一个特殊的modal,另外这两个组件还可以共用一个modal,了解到这些基础之后,组件就可以这样定义:
var Alert, Confirm; (function () { var modal, Proxy = function (isAlert) { return function () { if (arguments.length != 1) return; var msg = typeof arguments[0] === 'string' && arguments[0] || arguments[0].msg || '', onOk = typeof arguments[0] === 'object' && typeof arguments[0].onOk === 'function' && arguments[0].onOk, onCancel = typeof arguments[0] === 'object' && typeof arguments[0].onCancel === 'function' && arguments[0].onCancel, width = typeof arguments[0] === 'object' && arguments[0].width || 400, _onModalShow = function () { this.setWidth(width); this.setContent(msg); this[(isAlert ? 'hide' : 'show') + 'Button']('.btn-cancel'); }, _onModalHide = function () { this.setContent(''); }; //延迟初始化modal if(!modal) { modal = new Modal({ 'title': '操作提示', onModalShow: _onModalShow, onModalHide: _onModalHide, onContentReady: function(){ this.$modalBody.css({ 'padding-top': '30px', 'padding-bottom': '30px' }) } }); } else { //如果modal已经初始化则需要重新监听事件 var $modal = modal.$modal; $modal.off('modalShow modalHide'); $modal.off('modalShow modalHide'); $modal.on('modalShow', $.proxy(_onModalShow, modal)); $modal.on('modalHide', $.proxy(_onModalHide, modal)); } modal.setOptions({ onOk: onOk || $.noop, onCancel: onCancel || $.noop }); modal.open(); } }; Alert = Proxy(true); Confirm = Proxy(); })();
这段代码里:
首先考虑到了延迟初始化这个全局的modal组件;
由于onModalHide和onModalShow这两个回调属于事件回调,在初始化组件的时候通过options传进去的参数,不能通过修改options的方式来更改回调,只能通过重新注册的方式来处理;而onOk和onCancel属于函数回调,只要更改了options里面的引用,回调就能更改;
考虑到Alert和Confirm内容的长短,新加了一个参数width,以便调节框的宽度。
4. 小结
本文介绍的是自己在定义js组件过程中的一些方法和实践,代码偏多,不容易引起人的阅读兴趣,但是文中介绍的方法比较简单,而且这三个组件我已经用到好几个项目里面,从目前来看,能够解决我所有需要的弹框需求,所以我把它推荐出来,希望能给有需要的人带来帮助。