6.2.3 Form弹窗扩展
在平时的Web开发中,除了用弹窗提示信息以外,另外一个最常用的方式就是在弹窗里进行表单处理,如添加账户、修改密码等。但一般很少用于处理字段比较多的表单(其实也可以,只不过弹窗里要设置固定高度,以便在内容多的时候呈现滚动条)。本章我们就来看看,在Modal插件的基础上,如何扩展该功能。
弹窗里的表单一般有如下几个特点:
❑添加类表单在弹出时,往往需要表单是空的,如果添加过一次(再关闭弹窗),再次弹出时需要手工清空表单内容。
❑表单内容往往可能是通过ajax加载的html表单代码。
第1个特点带来的问题的解决方案比较简单,直接在shown.bs.formmodal事件上注册一个回调事件,清空表单里的内容即可。
第2个特点带来的问题比较多,我们分别来说一下。
1)默认的Modal插件请求ajax时会有缓存,如果在弹窗里修改多条记录,那每次ajax请求都是第一次的内容。解决方式是通过某种方式强制重新加载。
2)远程利用remote属性加载html内容将会替换掉整个modal-content元素的所有内容(包括modal-header、modal-body、modal-footer),这是与Bootstrap 2.x系统特别不一样的地方。所以ajax请求返回的表单内容单必须是如下格式:
- <div class="modal-content">
- <div class="modal-header">
- … 此处是标题提示
- </div>
- <div class="modal-body">
- <form class="form-horizontal" role="form">
- … 此处放置表单元素
- </form>
- </div>
- <div class="modal-footer">
- … 此处可以放置按钮
- </div>
- </div>
3)一般表单都有验证,大部分都会使用类似jQuery.validate这样的插件,但这种类型的插件一般都只是在DOM加载结束后,对当前页面所有的form元素进行检测并绑定相应事件。由于弹窗里的表单是后来通过ajax加载的,所以一般都不会起作用。其解决办法就是,在弹窗表单显示以后,手工对弹窗里的form元素绑定验证事件(在shown.bs.formmodal上绑定事件回调)。
下面,我们就带着这些问题,来一步一步扩展Form弹窗。首先定义FormModal类函数。
- // FormModal类定义
- var FormModal = function (element, options) {
- this.$element = $(element);
- this.super = this.$element.data('bs.modal'); // 获取自定义属性bs.modal的值
- this.options = options;
- this.$element.on('click.submit.formmodal', '[data-form="submit"]',
- $.proxy(this.submit, this));
- this.$element.on('click.reset.formmodal', '[data-form="reset"]',
- $.proxy(this.reset, this));
- this.$element.on('click.cancel.formmodal', '[data-form="cancel"]',
- $.proxy(this.cancel, this));
- var that = this; // 防止污染作用域,用临时变量that
- this.$element.on("show.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('show.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("shown.bs.modal", function (e) {
- that.$form = that.$element.find('form');
- that.$element.trigger(e = $.Event('shown.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("hide.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('hide.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("hidden.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('hidden.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- }
- // 默认设置
- FormModal.DEFAULTS = {
- cacheForm: false, // 默认不用缓存,每次都加载最新的内容
- closeAfterCancel: true // 默认取消后,直接关闭弹窗
- }
上述代码和InfoModal类似,但有两个地方不太一样。一个地方是在触发shown.bs.infomodal自定义事件中,多了一句that.$form = that.$element.find('form');代码,该代码是确保在弹窗显示之后,将找到的form元素赋值给一个临时$form变量,以便后面的reset原型方法使用(用于重置表单);另外一个地方是默认参数新加了一个cacheForm,用于设置(如果是ajax请求时)是否要缓存数据,如果不缓存,则每次请求时都获取最新的html内容。
所以Form弹窗插件在声明式用法时,可以定义两个参数,分别是data-cache-form和data-close-after-cancel。
我们来看原型方法。由于很多内容也是和InfoModal类似,所以toggle、show、hide、cancel原型方法都是一样的,不一样的是去除了confirm,增加了submit和reset原型方法。具体代码如下:
- // 单击“确认”按钮的行为
- FormModal.prototype.submit = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- this.$element.trigger(e = $.Event('beforeSubmit.bs.formmodal'));
- // 提交前触发事件,主要用于处理相关代码
- if (e.isDefaultPrevented()) return;
- this.$form.submit();
- this.$element.trigger(e = $.Event('afterSubmit.bs.formmodal'));
- // 提交后触发事件,主要用于处理相关代码
- if (e.isDefaultPrevented()) return;
- }
- // 单击“重置”按钮的行为
- FormModal.prototype.reset = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- var resetAction = function () {
- this.$element.trigger(e = $.Event('beforeReset.bs.formmodal'));
- // 重置前触发事件
- if (e.isDefaultPrevented()) return;
- this.$form.each(function () {
- this.reset(); // jQuery不支持reset,需要转换为DOM对象
- // 再调用原生reset方法
- });
- this.$element.trigger(e = $.Event('afterReset.bs.formmodal'));
- // 重置后触发事件
- if (e.isDefaultPrevented()) return;
- }
- if (this.super.isShown) return resetAction.call(this);
- this.$element.one("shown.bs.formmodal", $.proxy(resetAction, this));
- this.show();
- }
submit原型方法主要就是触发beforeSubmit、afterSubmit事件以及提交表单。而reset原型实现相对比较复杂:首先,jQuery不支持reset方法,所以需要将符合条件的form元素转换为DOM对象;然后调用DOM上的reset方法;在重置的时候,首先要先判断弹窗内容是不是重置了,只有显示了,才能直接重置,否则在shown上注册一个回调(回调内容是reset操作),在显示以后,才执行reset操作,其主要目的是当开发人员在调用reset方法时,可以直接传入reset参数(如$.formmodal('reset');而不至于出错。
FormModal暴露的所有事件如表6-1所示。
jQuery.fn.formmodal插件在定义的时候,和infomodal稍有不同,其目的是在这个地方处理缓存问题,也就是判断是否使用绑定在元素上的原有实例(本例中是bs.formmodal和bs.modal),需要通过参数来判断。具体代码如下:
- $.fn.formmodal = function (option, _relatedTarget) {
- return this.each(function () { // 根据选择器,遍历所有符合规则的元素
- var $this = $(this)
- var options = $.extend({}, FormModal.DEFAULTS, $this.data(),
- typeof option == 'object' && option)
- // 将默认参数、选择器所在元素上的自定义属性(data-开头)和option参数的值
- // 合并在一起,作为options参数
- // 优先级:后面的参数优先级高于前面的参数
- var data = options.cacheForm && $this.data('bs.formmodal')
- // 使用缓存时,才获取原来绑定的实例
- options.show = false;
- if (!options.cacheForm) { // 如果不用缓存,基本modal实例也要先
- // 清空,然后重新加载远程的html内容
- $this.data('bs.modal', null);
- }
- $this.modal(options, _relatedTarget); // 重新加载内容
- // 如果值不存在,则将formmodal实例设置为bs.formmodal的值
- if (!data) $this.data('bs.formmodal', (data = new FormModal(this, options)))
- // 如果option传递了string,则表示要执行某个方法
- // 比如传入了show,则要执行formmodal实例的show方法,data["show"]相当于data.show()
- if (typeof option == 'string') {
- data[option](_relatedTarget)
- }
- else {
- data.show(_relatedTarget);
- }
- })
- }
其他内容没有特别的地方,可以参考本节最后的完整源代码。我们来看一个使用示例:
- <button class="btn btn-warning" data-toggle="formmodal" data-target="#AddPopup"
- data-backdrop="static">添加</button>
- <div class="modal fade" id="AddPopup" data-cache-form="true" data-remote="form.html">
- </div>
要记得,ajax获取的内容,必须是完整的内容(以modal-dialog样式开始)。示例如下:
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-
- hidden="true">×</button>
- <h4 class="modal-title" id="H1">Email设置</h4>
- </div>
- <div class="modal-body">
- <form class="form-horizontal" role="form">
- <div class="form-group">
- <label class="col-sm-2 control-label">Email</label>
- <div class="col-sm-8"><input type="text" name="test"
- class="form-control" /></div>
- <span class="col-sm-1 control-label" id="messages"></span>
- </div>
- </form>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-success" data-form="submit">
- 保存</button>
- <button type="button" class="btn btn-danger" data-form="reset">
- 重置</button>
- <button type="button" class="btn btn-default" data-form="cancel">
- 取消</button>
- </div>
- </div>
- </div>
上述示例的运行效果如图6-4所示。
图6-4 ajax获取弹窗内容的运行效果
记住,如果要在弹窗里的表单上应用验证控件,需要在它的shown事件上重新应用一下验证触发事件。以jQuery.validate插件为例,其触发代码如下:
- $('#AddPopup').on('shown.bs.formmodal', function (e) {
- $('#AddPopup').find('form').validate({
- sendForm: false,
- description: {
- test: {
- required: '<div class="label label-danger">Email必填!</div>',
- pattern: '<div class="label label-danger">Pattern</div>',
- conditional: '<div class="label label-danger">Conditional</div>',
- valid: '<div class="label label-success">输入合法!</div>'
- }
- },
- valid: function () {
- // console.log("valid"); 验证失败时的操作
- },
- invalid: function () {
- // console.log("invalid");验证成功时的操作
- }
- });
- });
注意
❑上述示例是以ajax请求为例的,如果弹窗不请求ajax,则直接在声明式代码里写全表单就可以了,没有什么影响。注意,如果要在弹出前重置表单内容,直接调用$.formmodal('reset')即可。
❑上述示例中,ajax加载的form元素是放在modal-body样式里(即按钮在form元素外)的,如果把form元素放在modal-dialog外,全部包含了按钮和表单控件,则也可以直接使用原生的submit和reset按钮,而不需要使用data-form="submit"和data-form="reset"自定义属性。
Form弹窗的完整源代码如下:
- +function ($) {
- "use strict";
- // FormModal类定义
- var FormModal = function (element, options) {
- this.$element = $(element);
- this.super = this.$element.data('bs.modal'); // 获取自定义属性bs.modal的值
- this.options = options;
- this.$element.on('click.submit.formmodal',
- '[data-form="submit"]', $.proxy(this.submit, this));
- this.$element.on('click.reset.formmodal',
- '[data-form="reset"]', $.proxy(this.reset, this));
- this.$element.on('click.cancel.formmodal',
- '[data-form="cancel"]', $.proxy(this.cancel, this));
- var that = this; // 防止污染作用域,用临时变量that
- this.$element.on("show.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('show.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("shown.bs.modal", function (e) {
- that.$form = that.$element.find('form');
- that.$element.trigger(e = $.Event('shown.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("hide.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('hide.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- this.$element.on("hidden.bs.modal", function (e) {
- that.$element.trigger(e = $.Event('hidden.bs.formmodal'));
- if (e.isDefaultPrevented()) return;
- });
- }
- // 默认设置
- FormModal.DEFAULTS = {
- cacheForm: false,
- closeAfterCancel: true
- }
- // 反转弹窗状态
- FormModal.prototype.toggle = function (_relatedTarget) {
- return this[!this.super.isShown ? 'show' : 'hide'](_relatedTarget)
- // 如果是关闭状态,则打开弹窗,否则关闭
- }
- // 打开弹窗
- FormModal.prototype.show = function (_relatedTarget) {
- this.super.show(_relatedTarget);
- }
- // 关闭弹窗
- FormModal.prototype.hide = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- this.super.hide();
- }
- // 单击“确认”按钮的行为
- FormModal.prototype.submit = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- this.$element.trigger(e = $.Event('beforeSubmit.bs.formmodal'));
- // 提交前触发事件,主要用于处理相关代码
- if (e.isDefaultPrevented()) return;
- this.$form.submit();
- this.$element.trigger(e = $.Event('afterSubmit.bs.formmodal'));
- // 提交后触发事件,用于处理相关代码
- if (e.isDefaultPrevented()) return;
- }
- // 单击“重置”按钮的行为
- FormModal.prototype.reset = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- var resetAction = function () {
- this.$element.trigger(e = $.Event('beforeReset.bs.formmodal'));
- // 重置前触发事件
- if (e.isDefaultPrevented()) return;
- this.$form.each(function () {
- this.reset(); // jQuery不支持reset,需要转换为DOM对象
- // 再调用原生reset方法
- });
- this.$element.trigger(e = $.Event('afterReset.bs.formmodal'));
- // 重置后触发事件
- if (e.isDefaultPrevented()) return;
- }
- if (this.super.isShown) return resetAction.call(this);
- this.$element.one("shown.bs.formmodal", $.proxy(resetAction, this));
- this.show();
- }
- // 单击“取消”按钮的行为
- FormModal.prototype.cancel = function (e) {
- if (e) e.preventDefault(); // 先阻止冒泡行为
- var e = $.Event('cancel.bs.formmodal');
- this.$element.trigger(e); // 取消前,先触发事件
- if (e.isDefaultPrevented()) return;
- if (this.options.closeAfterCancel) {
- this.hide(e); // 如果设置了data-close-after-cancel=true
- // 参数,则关闭弹窗
- }
- }
- // formmodal 插件定义
- var old = $.fn.formmodal
- // 如果定义了其他的formmodal插件,则保留它(以便在noConflict(解决防冲突)之后,
- // 可以继续使用该旧插件代码)
- $.fn.formmodal = function (option, _relatedTarget) {
- return this.each(function () { // 根据选择器,遍历所有符合规则的元素
- var $this = $(this)
- var options = $.extend({}, FormModal.DEFAULTS, $this.data(),
- typeof option == 'object' && option)
- // 将默认参数、选择器所在元素的自定义属性(data-开头)、
- // option参数这3种的值合并在一起,作为options参数
- // 优先级:后面的参数优先级高于前面的参数
- var data = options.cacheForm && $this.data('bs.formmodal')
- // 获取自定义属性bs.modal的值
- options.show = false;
- if (!options.cacheForm) {
- // 如果不用缓存,则先清空实例,然后重新加载远程的html内容
- $this.data('bs.modal', null);
- }
- $this.modal(options, _relatedTarget);
- // 如果值不存在,则将formmodal实例设置为bs.formmodal的值
- if (!data) $this.data('bs.formmodal',
- (data = new FormModal(this, options)))
- // 如果option传递了string,则表示要执行某个方法
- // 比如传入了show,则要执行formmodal实例的show方法,
- // data["show"]相当于data.show();
- if (typeof option == 'string') {
- data[option](_relatedTarget)
- }
- else {
- data.show(_relatedTarget);
- }
- })
- }
- $.fn.formmodal.Constructor = FormModal;
- // 重设插件构造器,可以通过该属性获取插件的真实类函数
- // formmodal防冲突
- $.fn.formmodal.noConflict = function () {
- $.fn.formmodal = old
- return this
- }
- // formmodal DATA-API
- $(document).on('click.bs.formmodal.data-api',
- '[data-toggle="formmodal"]', function (e) {
- // 监测所有拥有自定义属性data-toggle="modal"的元素上的单击事件
- var $this = $(this)
- var href = $this.attr('href') // 获取href属性值
- var $target = $($this.attr('data-target') ||
- (href && href.replace(/.*(?=#[^\s]+$)/, '')))
- // 获取data-target属性值,如果没有,则获取href值,该值是所弹出元素的id
- // 如果弹窗元素上设置了data-formmodal属性值,则option值是字符串toggle
- // 否则将remote值(如果有的话)、弹窗元素上的自定义属性值集合、触发元素上的
- // 自定义属性值集合进行合并,作为option选项对象
- var option = $target.data('formmodal') ? 'toggle' : $.extend({
- remote: !/#/.test(href) && href }, $target.data(), $this.data())
- e.preventDefault() // 阻止默认行为
- $target
- .formmodal(option, this) // 给弹窗元素绑定formmodal插件(即实例化
- // formmodal),并传入option参数
- .one('hide', function () {
- $this.is(':visible') && $this.focus()
- // 定义一次hide事件,使所单击元素获得焦点
- })
- })
- }(jQuery);