この記事では、Bootstrap のポップアップ ウィンドウ コンポーネント Modal を紹介します。このコンポーネントはシンプルで使いやすく、美しい効果があり、非常に実用的です。
ブラウザーが提供するアラート ボックスと確認ボックスのエクスペリエンスが悪く、ブラウザーにはダイアログ ボックスの形式でカスタム HTML を表示する標準のポップアップ機能が提供されていないため、多くのプロジェクトはダイアログ コンポーネントをカスタマイズします。この記事では、プロジェクト内のブートストラップ モーダル コンポーネントに基づいてアラート、確認、およびモーダル ダイアログ ボックスをカスタマイズした経験を紹介します。これが参考になれば幸いです。
1. 表示例
詳細なコードは、上記のダウンロード リンクからソース コードをダウンロードすることで理解できます。コードの量は、3 つのコンポーネントを合計しても 200 行を超えるだけです。
JavaScript コンポーネントの開発経験がある場合は、このレベルのコードをすぐに理解できると思います。また、実際のニーズに近いシナリオをシミュレートするデモをソース コードで提供します。
1) ユーザーはインターフェース上のボタンをクリックして、以前に定義されたモーダル ボックスを開きます:
2) ユーザーが開いたモーダル ボックスにいくつかのフォームに入力して [OK] をクリックすると、いくつかの検証がトリガーされます。
メールアドレスが未入力の場合:
メールアドレスを入力した後:
これら 2 つのプロンプトは、実際には、アラートと確認の効果を示すために組み込まれています。実際には、それほど厄介な機能はありません。
3) パスワードが空であるというプロンプトが表示された場合、注意深い人は [OK] ボタンが無効な状態であることに気づくでしょう。これは、非同期タスクが正常に完了する前に、[OK] ボタンが最終的にいくつかの非同期タスクを完了することを期待しているためです。モーダルコンポーネントは閉じられず、クリックされたボタンが繰り返しクリックされることを制御できること。
admin@admin として入力すると、送信が成功したことを示すプロンプトが表示され、すべてのポップアップ ボックスが閉じられます。
電子メールに他の値を入力すると、送信失敗のプロンプトが表示され、そこにモーダル ボックスが表示されたままになります:コンポーネント定義、特に登録ボタンでは、AOPプログラミング処理を追加し、jqueryの遅延オブジェクトを使用して、必要な非同期プログラミングを実現しました。詳細については、ソースコードを読んでください。質問がある場合は、コメントを残してください。地域交流と教育。
2. コンポーネントの要件
1) カスタムアラートボックス
プロトタイプは次のとおりです:
呼び出し時には最大 2 つのパラメータが必要です。1 つの msg は表示されるプロンプトの内容を渡すために使用され、1 つの onOk は [OK] ボタンがクリックされたときのコールバックを処理するために使用されます。
の 2 つの呼び出し形式があります。
第一种是没有回调的情况,那么直接传递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组件过程中的一些方法和实践,代码偏多,不容易引起人的阅读兴趣,但是文中介绍的方法比较简单,而且这三个组件我已经用到好几个项目里面,从目前来看,能够解决我所有需要的弹框需求,所以我把它推荐出来,希望能给有需要的人带来帮助。