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请求返回的表单内容单必须是如下格式:

  1. <div class="modal-content">
  2. <div class="modal-header">
  3. … 此处是标题提示
  4. </div>
  5. <div class="modal-body">
  6. <form class="form-horizontal" role="form">
  7. … 此处放置表单元素
  8. </form>
  9. </div>
  10. <div class="modal-footer">
  11. … 此处可以放置按钮
  12. </div>
  13. </div>

3)一般表单都有验证,大部分都会使用类似jQuery.validate这样的插件,但这种类型的插件一般都只是在DOM加载结束后,对当前页面所有的form元素进行检测并绑定相应事件。由于弹窗里的表单是后来通过ajax加载的,所以一般都不会起作用。其解决办法就是,在弹窗表单显示以后,手工对弹窗里的form元素绑定验证事件(在shown.bs.formmodal上绑定事件回调)。

下面,我们就带着这些问题,来一步一步扩展Form弹窗。首先定义FormModal类函数。

  1. // FormModal类定义
  2. var FormModal = function (element, options) {
  3. this.$element = $(element);
  4. this.super = this.$element.data('bs.modal'); // 获取自定义属性bs.modal的值
  5. this.options = options;
  6.  
  7. this.$element.on('click.submit.formmodal', '[data-form="submit"]',
  8. $.proxy(this.submit, this));
  9. this.$element.on('click.reset.formmodal', '[data-form="reset"]',
  10. $.proxy(this.reset, this));
  11. this.$element.on('click.cancel.formmodal', '[data-form="cancel"]',
  12. $.proxy(this.cancel, this));
  13.  
  14. var that = this; // 防止污染作用域,用临时变量that
  15.  
  16. this.$element.on("show.bs.modal", function (e) {
  17. that.$element.trigger(e = $.Event('show.bs.formmodal'));
  18. if (e.isDefaultPrevented()) return;
  19. });
  20. this.$element.on("shown.bs.modal", function (e) {
  21. that.$form = that.$element.find('form');
  22. that.$element.trigger(e = $.Event('shown.bs.formmodal'));
  23. if (e.isDefaultPrevented()) return;
  24. });
  25. this.$element.on("hide.bs.modal", function (e) {
  26. that.$element.trigger(e = $.Event('hide.bs.formmodal'));
  27. if (e.isDefaultPrevented()) return;
  28. });
  29. this.$element.on("hidden.bs.modal", function (e) {
  30. that.$element.trigger(e = $.Event('hidden.bs.formmodal'));
  31. if (e.isDefaultPrevented()) return;
  32. });
  33. }
  34. // 默认设置
  35. FormModal.DEFAULTS = {
  36. cacheForm: false, // 默认不用缓存,每次都加载最新的内容
  37. closeAfterCancel: true // 默认取消后,直接关闭弹窗
  38. }

上述代码和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原型方法。具体代码如下:

  1. // 单击“确认”按钮的行为
  2. FormModal.prototype.submit = function (e) {
  3. if (e) e.preventDefault(); // 先阻止冒泡行为
  4.  
  5. this.$element.trigger(e = $.Event('beforeSubmit.bs.formmodal'));
  6. // 提交前触发事件,主要用于处理相关代码
  7. if (e.isDefaultPrevented()) return;
  8.  
  9. this.$form.submit();
  10. this.$element.trigger(e = $.Event('afterSubmit.bs.formmodal'));
  11. // 提交后触发事件,主要用于处理相关代码
  12. if (e.isDefaultPrevented()) return;
  13. }
  14. // 单击“重置”按钮的行为
  15. FormModal.prototype.reset = function (e) {
  16. if (e) e.preventDefault(); // 先阻止冒泡行为
  17.  
  18. var resetAction = function () {
  19. this.$element.trigger(e = $.Event('beforeReset.bs.formmodal'));
  20. // 重置前触发事件
  21. if (e.isDefaultPrevented()) return;
  22. this.$form.each(function () {
  23. this.reset(); // jQuery不支持reset,需要转换为DOM对象
  24. // 再调用原生reset方法
  25. });
  26. this.$element.trigger(e = $.Event('afterReset.bs.formmodal'));
  27. // 重置后触发事件
  28. if (e.isDefaultPrevented()) return;
  29. }
  30. if (this.super.isShown) return resetAction.call(this);
  31.  
  32. this.$element.one("shown.bs.formmodal", $.proxy(resetAction, this));
  33. this.show();
  34. }

submit原型方法主要就是触发beforeSubmit、afterSubmit事件以及提交表单。而reset原型实现相对比较复杂:首先,jQuery不支持reset方法,所以需要将符合条件的form元素转换为DOM对象;然后调用DOM上的reset方法;在重置的时候,首先要先判断弹窗内容是不是重置了,只有显示了,才能直接重置,否则在shown上注册一个回调(回调内容是reset操作),在显示以后,才执行reset操作,其主要目的是当开发人员在调用reset方法时,可以直接传入reset参数(如$.formmodal('reset');而不至于出错。

FormModal暴露的所有事件如表6-1所示。

6.2.3 Form弹窗扩展 - 图1

6.2.3 Form弹窗扩展 - 图2

jQuery.fn.formmodal插件在定义的时候,和infomodal稍有不同,其目的是在这个地方处理缓存问题,也就是判断是否使用绑定在元素上的原有实例(本例中是bs.formmodal和bs.modal),需要通过参数来判断。具体代码如下:

  1. $.fn.formmodal = function (option, _relatedTarget) {
  2. return this.each(function () { // 根据选择器,遍历所有符合规则的元素
  3. var $this = $(this)
  4. var options = $.extend({}, FormModal.DEFAULTS, $this.data(),
  5. typeof option == 'object' && option)
  6. // 将默认参数、选择器所在元素上的自定义属性(data-开头)和option参数的值
  7. // 合并在一起,作为options参数
  8. // 优先级:后面的参数优先级高于前面的参数
  9.  
  10. var data = options.cacheForm && $this.data('bs.formmodal')
  11. // 使用缓存时,才获取原来绑定的实例
  12. options.show = false;
  13. if (!options.cacheForm) { // 如果不用缓存,基本modal实例也要先
  14. // 清空,然后重新加载远程的html内容
  15. $this.data('bs.modal', null);
  16. }
  17.  
  18. $this.modal(options, _relatedTarget); // 重新加载内容
  19.  
  20. // 如果值不存在,则将formmodal实例设置为bs.formmodal的值
  21. if (!data) $this.data('bs.formmodal', (data = new FormModal(this, options)))
  22.  
  23. // 如果option传递了string,则表示要执行某个方法
  24. // 比如传入了show,则要执行formmodal实例的show方法,data["show"]相当于data.show()
  25. if (typeof option == 'string') {
  26. data[option](_relatedTarget)
  27. }
  28. else {
  29. data.show(_relatedTarget);
  30. }
  31. })
  32. }

其他内容没有特别的地方,可以参考本节最后的完整源代码。我们来看一个使用示例:

  1. <button class="btn btn-warning" data-toggle="formmodal" data-target="#AddPopup"
  2. data-backdrop="static">添加</button>
  3. <div class="modal fade" id="AddPopup" data-cache-form="true" data-remote="form.html">
  4. </div>

要记得,ajax获取的内容,必须是完整的内容(以modal-dialog样式开始)。示例如下:

  1. <div class="modal-dialog">
  2. <div class="modal-content">
  3. <div class="modal-header">
  4. <button type="button" class="close" data-dismiss="modal" aria-
  5. hidden="true">×</button>
  6. <h4 class="modal-title" id="H1">Email设置</h4>
  7. </div>
  8. <div class="modal-body">
  9. <form class="form-horizontal" role="form">
  10. <div class="form-group">
  11. <label class="col-sm-2 control-label">Email</label>
  12. <div class="col-sm-8"><input type="text" name="test"
  13. class="form-control" /></div>
  14. <span class="col-sm-1 control-label" id="messages"></span>
  15. </div>
  16. </form>
  17. </div>
  18. <div class="modal-footer">
  19. <button type="button" class="btn btn-success" data-form="submit">
  20. 保存</button>
  21. <button type="button" class="btn btn-danger" data-form="reset">
  22. 重置</button>
  23. <button type="button" class="btn btn-default" data-form="cancel">
  24. 取消</button>
  25. </div>
  26. </div>
  27. </div>

上述示例的运行效果如图6-4所示。

6.2.3 Form弹窗扩展 - 图3 图6-4 ajax获取弹窗内容的运行效果

记住,如果要在弹窗里的表单上应用验证控件,需要在它的shown事件上重新应用一下验证触发事件。以jQuery.validate插件为例,其触发代码如下:

  1. $('#AddPopup').on('shown.bs.formmodal', function (e) {
  2. $('#AddPopup').find('form').validate({
  3. sendForm: false,
  4. description: {
  5. test: {
  6. required: '<div class="label label-danger">Email必填!</div>',
  7. pattern: '<div class="label label-danger">Pattern</div>',
  8. conditional: '<div class="label label-danger">Conditional</div>',
  9. valid: '<div class="label label-success">输入合法!</div>'
  10. }
  11. },
  12. valid: function () {
  13. // console.log("valid"); 验证失败时的操作
  14. },
  15. invalid: function () {
  16. // console.log("invalid");验证成功时的操作
  17. }
  18. });
  19. });

注意

❑上述示例是以ajax请求为例的,如果弹窗不请求ajax,则直接在声明式代码里写全表单就可以了,没有什么影响。注意,如果要在弹出前重置表单内容,直接调用$.formmodal('reset')即可。

❑上述示例中,ajax加载的form元素是放在modal-body样式里(即按钮在form元素外)的,如果把form元素放在modal-dialog外,全部包含了按钮和表单控件,则也可以直接使用原生的submit和reset按钮,而不需要使用data-form="submit"和data-form="reset"自定义属性。

Form弹窗的完整源代码如下:

  1. +function ($) {
  2. "use strict";
  3.  
  4. // FormModal类定义
  5. var FormModal = function (element, options) {
  6. this.$element = $(element);
  7. this.super = this.$element.data('bs.modal'); // 获取自定义属性bs.modal的值
  8. this.options = options;
  9.  
  10. this.$element.on('click.submit.formmodal',
  11. '[data-form="submit"]', $.proxy(this.submit, this));
  12. this.$element.on('click.reset.formmodal',
  13. '[data-form="reset"]', $.proxy(this.reset, this));
  14. this.$element.on('click.cancel.formmodal',
  15. '[data-form="cancel"]', $.proxy(this.cancel, this));
  16.  
  17. var that = this; // 防止污染作用域,用临时变量that
  18.  
  19. this.$element.on("show.bs.modal", function (e) {
  20. that.$element.trigger(e = $.Event('show.bs.formmodal'));
  21. if (e.isDefaultPrevented()) return;
  22. });
  23. this.$element.on("shown.bs.modal", function (e) {
  24. that.$form = that.$element.find('form');
  25. that.$element.trigger(e = $.Event('shown.bs.formmodal'));
  26. if (e.isDefaultPrevented()) return;
  27. });
  28. this.$element.on("hide.bs.modal", function (e) {
  29. that.$element.trigger(e = $.Event('hide.bs.formmodal'));
  30. if (e.isDefaultPrevented()) return;
  31. });
  32. this.$element.on("hidden.bs.modal", function (e) {
  33. that.$element.trigger(e = $.Event('hidden.bs.formmodal'));
  34. if (e.isDefaultPrevented()) return;
  35. });
  36. }
  37. // 默认设置
  38. FormModal.DEFAULTS = {
  39. cacheForm: false,
  40. closeAfterCancel: true
  41. }
  42. // 反转弹窗状态
  43. FormModal.prototype.toggle = function (_relatedTarget) {
  44. return this[!this.super.isShown ? 'show' : 'hide'](_relatedTarget)
  45. // 如果是关闭状态,则打开弹窗,否则关闭
  46. }
  47. // 打开弹窗
  48. FormModal.prototype.show = function (_relatedTarget) {
  49. this.super.show(_relatedTarget);
  50. }
  51. // 关闭弹窗
  52. FormModal.prototype.hide = function (e) {
  53. if (e) e.preventDefault(); // 先阻止冒泡行为
  54. this.super.hide();
  55. }
  56. // 单击“确认”按钮的行为
  57. FormModal.prototype.submit = function (e) {
  58. if (e) e.preventDefault(); // 先阻止冒泡行为
  59.  
  60. this.$element.trigger(e = $.Event('beforeSubmit.bs.formmodal'));
  61. // 提交前触发事件,主要用于处理相关代码
  62.  
  63. if (e.isDefaultPrevented()) return;
  64.  
  65. this.$form.submit();
  66. this.$element.trigger(e = $.Event('afterSubmit.bs.formmodal'));
  67. // 提交后触发事件,用于处理相关代码
  68. if (e.isDefaultPrevented()) return;
  69. }
  70. // 单击“重置”按钮的行为
  71. FormModal.prototype.reset = function (e) {
  72. if (e) e.preventDefault(); // 先阻止冒泡行为
  73. var resetAction = function () {
  74. this.$element.trigger(e = $.Event('beforeReset.bs.formmodal'));
  75. // 重置前触发事件
  76. if (e.isDefaultPrevented()) return;
  77.  
  78. this.$form.each(function () {
  79. this.reset(); // jQuery不支持reset,需要转换为DOM对象
  80. // 再调用原生reset方法
  81. });
  82.  
  83. this.$element.trigger(e = $.Event('afterReset.bs.formmodal'));
  84. // 重置后触发事件
  85. if (e.isDefaultPrevented()) return;
  86. }
  87. if (this.super.isShown) return resetAction.call(this);
  88.  
  89. this.$element.one("shown.bs.formmodal", $.proxy(resetAction, this));
  90. this.show();
  91. }
  92. // 单击“取消”按钮的行为
  93. FormModal.prototype.cancel = function (e) {
  94. if (e) e.preventDefault(); // 先阻止冒泡行为
  95.  
  96. var e = $.Event('cancel.bs.formmodal');
  97. this.$element.trigger(e); // 取消前,先触发事件
  98.  
  99. if (e.isDefaultPrevented()) return;
  100. if (this.options.closeAfterCancel) {
  101. this.hide(e); // 如果设置了data-close-after-cancel=true
  102. // 参数,则关闭弹窗
  103. }
  104. }
  105. // formmodal 插件定义
  106. var old = $.fn.formmodal
  107. // 如果定义了其他的formmodal插件,则保留它(以便在noConflict(解决防冲突)之后,
  108. // 可以继续使用该旧插件代码)
  109.  
  110. $.fn.formmodal = function (option, _relatedTarget) {
  111. return this.each(function () { // 根据选择器,遍历所有符合规则的元素
  112. var $this = $(this)
  113. var options = $.extend({}, FormModal.DEFAULTS, $this.data(),
  114. typeof option == 'object' && option)
  115. // 将默认参数、选择器所在元素的自定义属性(data-开头)、
  116. // option参数这3种的值合并在一起,作为options参数
  117. // 优先级:后面的参数优先级高于前面的参数
  118.  
  119. var data = options.cacheForm && $this.data('bs.formmodal')
  120. // 获取自定义属性bs.modal的值
  121.  
  122. options.show = false;
  123. if (!options.cacheForm) {
  124. // 如果不用缓存,则先清空实例,然后重新加载远程的html内容
  125. $this.data('bs.modal', null);
  126. }
  127.  
  128. $this.modal(options, _relatedTarget);
  129. // 如果值不存在,则将formmodal实例设置为bs.formmodal的值
  130. if (!data) $this.data('bs.formmodal',
  131. (data = new FormModal(this, options)))
  132. // 如果option传递了string,则表示要执行某个方法
  133. // 比如传入了show,则要执行formmodal实例的show方法,
  134. // data["show"]相当于data.show();
  135. if (typeof option == 'string') {
  136. data[option](_relatedTarget)
  137. }
  138. else {
  139. data.show(_relatedTarget);
  140. }
  141. })
  142. }
  143. $.fn.formmodal.Constructor = FormModal;
  144. // 重设插件构造器,可以通过该属性获取插件的真实类函数
  145.  
  146. // formmodal防冲突
  147. $.fn.formmodal.noConflict = function () {
  148. $.fn.formmodal = old
  149. return this
  150. }
  151.  
  152. // formmodal DATA-API
  153. $(document).on('click.bs.formmodal.data-api',
  154. '[data-toggle="formmodal"]', function (e) {
  155. // 监测所有拥有自定义属性data-toggle="modal"的元素上的单击事件
  156. var $this = $(this)
  157. var href = $this.attr('href') // 获取href属性值
  158. var $target = $($this.attr('data-target') ||
  159. (href && href.replace(/.*(?=#[^\s]+$)/, '')))
  160. // 获取data-target属性值,如果没有,则获取href值,该值是所弹出元素的id
  161.  
  162. // 如果弹窗元素上设置了data-formmodal属性值,则option值是字符串toggle
  163. // 否则将remote值(如果有的话)、弹窗元素上的自定义属性值集合、触发元素上的
  164. // 自定义属性值集合进行合并,作为option选项对象
  165. var option = $target.data('formmodal') ? 'toggle' : $.extend({
  166. remote: !/#/.test(href) && href }, $target.data(), $this.data())
  167.  
  168. e.preventDefault() // 阻止默认行为
  169. $target
  170. .formmodal(option, this) // 给弹窗元素绑定formmodal插件(即实例化
  171. // formmodal),并传入option参数
  172. .one('hide', function () {
  173. $this.is(':visible') && $this.focus()
  174. // 定义一次hide事件,使所单击元素获得焦点
  175. })
  176. })
  177. }(jQuery);