自定义AngularJS Material模态框

AngularJS Material是Material Design设计方式使用AngularJS实现的,该项目提供了一套基于Material Design 系统设计的可重复使用的,易于测试的UI组件。它的layout使用的是flex来布局的,因此大家就知道了他的兼容性了,但是这个是未来的方向。和AngularJS UI Bootstrap相比,AngularJS Material组件更丰富,比如统一外观的Select(下拉框)、移动设备系统才有的Switch(开关)、Swipe(侧滑)、Bottom Sheet(底部弹出)等组件。AngularJS Material交互性更好、动感更强,更适合做响应式Web App。

看了一下AngularJS Material官网的Demo,不管是Dialog还是Panel组件似乎缺少封装。如果你和我一样在项目中需要自定义AngularJS Material模态框,请往下看。

我觉得模态框的头部是最需要封装的。想一想模态框头部是不是都是标题加关闭按钮。标题最好能接受父作用域设置的参数,头部以外的区域很多时候会变动,因此我们可以使用transclude将指令内部的元素嵌入到你的模板中去。看下面代码:

/**
 * 模态窗指令:对应模态窗服务的视图
 * @param  dialogTitle {String} 设置障碍模态窗标题
 */
angular
  .module('myApp')
  .directive('riaDialog', function() {
    return {
      scope: {
        dialogTitle:'@'
      },
      restrict: "EA",
      transclude: true,
      replace: true,
      template: '<div class="dialog" role="dialog" aria-label="Dialog" layout="column" ng-cloak>\n' +
      '  <md-toolbar class="dialog-header">\n' +
      '    <div class="md-toolbar-tools" layout="row" layout-align="space-between center">\n' +
      '      <h4>{{::dialogTitle}}</h4>\n' +
      '      <md-button class="md-icon-button" title="Close"\n' +
      '        aria-label="Close dialog"\n' +
      '        ng-click="$parent.closeDialog();">\n' +
      '        <md-icon>\n' +
      '          <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fit="" preserveAspectRatio="xMidYMid meet" focusable="false">\n' +
      '            <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>\n' +
      '          </svg>\n' +
      '        </md-icon>\n' +
      '      </md-button>\n' +
      '    </div>\n' +
      '  </md-toolbar>\n' +
      '  <ng-transclude></ng-transclude>\n' +
      '</div>\n'
    };
  });

我们只对riaDialog指令设置了一个独立作用域(Scope)属性dialogTitle。点击关闭按钮将会调用父作用域中的closeDialog方法,这种行为通常是统一的,因此我们用一个服务来封装它。看下面代码:

/**
 * 模态窗服务
 */

angular
  .module('myApp')
  .factory('dialog', ["$mdPanel", function($mdPanel) {
    return {

      /**
       * 打开带模板的模态窗
       * @param templateUrl {String} 内容模块地址 
       * @param locals {Object} 指定共享数据
       * @param clickOutsideToClose {Boolean} 点击模态窗外面关闭
       */
      open: function(templateUrl, locals, clickOutsideToClose) {
        var panelRef = null;
        var position = $mdPanel.newPanelPosition()
          .absolute()
          .center();

        var config = {
          attachTo: 'body',
          controller: ['$scope', function($scope) {
            $scope.locals = locals;
            $scope.closeDialog = function() {
              panelRef && panelRef.close().then(function() {
                panelRef.destroy();
              });
            }
          }],
          templateUrl: templateUrl,
          hasBackdrop: true,
          panelClass: 'ria-dialog',
          position: position,
          trapFocus: false,
          zIndex: 40,
          clickOutsideToClose: clickOutsideToClose || false,
          escapeToClose: true,
          focusOnOpen: true
        };

        return $mdPanel.open(config).then(function(mdPanelRef) {
          panelRef = mdPanelRef;
        });
      }
    }
  }]);

dialog服务是对$mdPanel的封装,只有一个open方法,本质上是对$mdPanel.open方法的调用。为什么不选用dialog?个人觉得mdPanel更底层一点,配置更灵活。我们这里使用了三个参数,只有第一个是必选的。模态框关联的控制器可以对返回的panelRef的注入引用, 这个引用可以关闭、隐藏和显示模态框。模态框头部与嵌入内容是平级的,因此关闭按钮要调用closeDialog函数,加上$parent属性就行了,没必要再加一个独立作用域属性。

注意由于codepen对文件的限制,本来是可以通过md-svg-icon设置svg按钮图标,最终改成直接嵌入svg代码。还有本来中通过templateUrl弹出动态内容,最终使用$templateCache前将模板缓存到一个定义模板的JavaScript文件中,这样就不需要通过XHR来加载模板了。最终效果:https://codepen.io/riafan/pen/bvEwwP

打开模态框是一个异步的过程,因此如果模态框的内容没加载完,表单验证会有问题。这又是AngularJS人一个“坑”。怎样解决这一个问题呢?可以使用angular.bootstrap手动启动该AngularJS应用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注