功能梳理

  • modal组件的模版应该有三种类型:内置的alert和confirm,以及自定义模版(字符串或者模版URL)
  • 必须返回Promise,便于拓展
  • 自定义控制器
  • 自定义scope 一般情况下不会需要传入特定的scope,从rootScope上new一个即可,但有时候modal里的数据与某个scope相关的时候就很有用了,可以直接继承这个scope里的数据,不需要再手动的注入
  • 支持controllerAs语法

关键点分析

为了保持良好的模块化和可复用性,应该使用service来构建modal
angular.module('foo', [])
.factory('modal', function () {
    function Modal = function () {
        this.show = function (option) {
            …
        }
    }
    return new Modal();
})
$templateRequest获取模版文件

$templateRequest用来获取模版文件,然后angular会将获取到的模版内容用$templateCache缓存起来,避免重复请求,如果请求出错或者模版内容为空将会抛出一个$compile错误,为了更好的体验,我们可以设置第二个参数为true,疏略这个错误,然后在这个函数返回的promise中做错误处理

默认模版有两种alert,confirm,通过type字段来配置
字符串模版使用template字段配置
html模版文件地址使用templateUrl来配置

// 默认模版
<div class="modal-wrap">
    <div class="modal-mask"></div>
    <div class="modal-content">
        <div class="modal-header">
            <div ng-click="close()" class="modal-close">X</div>
        </div>
        <div class="modal-body">
            <div class="container">
                <h3>{{message}}</h3>
            </div>
        </div>
        <div class="modal-footer form-footer">
            <button ng-if="type === 'confirm'" class="btn btn-submit" ng-click="confirm()">确定</button>
            <button class="btn btn-close" ng-click="close()">取消</button>
        </div>
    </div>
</div>
// 模版获取函数
var defalutType = ['alert', 'confirm'];
function getTemplate (type, template, url) {
    var d = $q.defer();
    if (type) {
        if (defalutType.indexOf(type) < 0) {
        	d.reject({error: 'modal类型只有alert和confirm'});
        } else {
            d.resolve({
            		type: type,
            		tpl: defaultTpl
            	})
        }
    }
    else if (template) {
        d.resolve({
            type: 'custom',
            tpl: template
        });
    } else if (url) {
        $templateRequest(url, true)
        .then(function (res) {
            d.resolve({
                type: 'custom',
                tpl: res
            });
        }, function (e) {
            d.reject(e);
        })
    } else {
        d.reject({error: '缺少模版定义'});
    }
    return d.promise;
}
编译模版,绑定作用域以及controller

拿到模版后下一步就是要编译模版,绑定作用域,angular提供了$compile函数

var postLinkFn = $compile(tpl);

$compile函数会遍历模版中的DOM节点,收集指令,然后返回一个用来绑定作用域的函数 创建scope

var modalScope = (options.scope || $rootScope).$new();

如果自定义了scope就直接继承定义的scope,否则从根作用域上继承scope 这里不能直接使用定义的scope 因为后面在关闭modal的时候,需要销毁作用域,如果不继承一个新的作用域将导致传入的作用域被销毁,程序会出错。 链接scope

var modalDom = linkFn(modalScope);

绑定控制器 使用$controller(constructor, locals)函数,这个函数接受两个参数 第一个就是一般的控制器函数 第二个比较重要,是一个对象,用来指定控制器的依赖,必须指定$scope依赖,否则这个controller就没啥意义了 例如:指定的控制器是

function fooController ($scope, dep1, dep2) {
    …
}

为了在实例化控制器的时候,能够正确的获取到对应的依赖,需要在locals做对应的配置

    {
        $scope: $specialScope //传入想绑定的scope
        dep1: xxx, // 传入对应的依赖
        dep2: xxx
    }

因此,还需要配置一个控制器依赖

// 传递给$controller函数的控制器依赖
var controllerDependance = {
    $scope: modalScope,
    $element: modalDom,
    close: function (cb) {
        closeDefer.resolve(cb);
        removeModal();
    }
}
if (tplObj.type === 'confirm') {
    controllerDependance.confirm = function (cb) {
        confirmDefer.resolve(cb);
        removeModal();
    }
}
//实例化controller
var modalController = $controller(options.controller, controllerDependance);

传入close,confirm两个方法,以便在controller中拓展回调函数,这里主要是为了给close和confirm的promise传递数据

支持controllerAs语法

if (options.controllerAs) {
    modalScope[options.controllerAs] = modalController;
}

controllerAs 是块语法糖,相当于在scope对象中以指定的名称注入controller实例,不同的是此时的controller应该是一个构造器函数,使用this.xxx = xxx 的方式来定义属性

构造promise

调用show方法创建modal之后,可以通过promise获得这个对象,

var modalDefer = $q.defer();
var closeDefer = $q.defer();
var confirmDefer = $q.defer();

var modalObject = {
    controller: modalController,
    scope: modalScope,
    element: modalDom,
    close: closeDefer.promise
};
if (tplObj.type === 'confirm') {
    modalObject.confirm = confirmDefer;
}
d.resolve(modalObject);

然后可以进一步做开发,比如修改scope中的数据,如果element上挂在了第三方的jquery插件,可以通过很方便的调用,另外最重要的就是拿到close和confirm的promise中的数据作进一步的处理 指定close与confirm的回调

    modal.show({…})
    .then(function(modalObj) {
        modalObj.close.then(…);
        modalObj.confirm.then(…);
    },function(e) {})

此处的close与confirm是promise对象,用来配置下一步,而在控制器中的则是用来关闭modal并返回数据的函数

关闭modal
function removeModal () {
    $animate.leave(modalDom)
    .then(
        function () {
            modalScope.$destroy();
            modalScope = null;
            d = null;
            closeDeferred = null;
            confirmDefer = null;
            modalObject = null;
            controllerDependance = null;
            modalDom = null;
        },
        function (e) {
            modalDefer.reject(e);
        })
}

从dom中移除modal节点以后,还要销毁作用域,删除相关变量,避免不必要的性能浪费

示例

    modal.show({
            type: 'confirm',
            controller: function ($scope, close, confirm, foo, bar) {
                $scope.message = 'hello world';
                $scope.close = function () {
                    close('close the modal')
                }
                $scope.close = function () {
                    confirm('confirm the action');
                }
            },
            injection: {
                foo: 'injection1',
                bar: 'injection2'
            }
        }).then(function (modalObj) {
            modalObj.close.then(function (res) {
                alert(res);
            }, function (e) {
            })

            modalObj.confirm.then(function (res) {
                alert(res);
            }, function (e) {
            })
        }, function (e) {
        })

API说明

type ['alert','confirm']
controller将自动注入close函数,另外在在confirm的情况下还会注入confirm函数,若要使用,必须在controller参数中声明依赖

message 在type为alert或者confirm的情况下,显示在modal上的提示语,可通过injection或者controller注入

injection 用来注入数据。若要使用,必须在controller的参数重声明依赖

模版定义

template: 模版字符串
templateUrl:  模版地址    

controllerAs 若指定,那么controller不需要再注入$scope,以this.xxx = xxx 的语法来定义model即可

promise show方法将返回一个promise,参数为modal对象,用modal.close和modal.confirm来配置回调事件

完整代码

    angular.module('modalModule', [])
    .factory('modalService', function ($animate, $document, $compile,$templateRequest, $controller, $http, $rootScope, $q) {
        function ModalService () {
        var defaultTpl = '<div class="modal-wrap">\
                            <div class="modal-mask"></div>\
                            <div class="modal-content">\
                                <div class="modal-header">{{title}}\
                                    <div ng-click="close()" class="modal-close">X</div>\
                                </div>\
                                <div class="modal-body">\
                                    <div class="container">\
                                        <h3>{{message}}</h3>\
                                    </div>\
                                </div>\
                                <div class="modal-footer form-footer">\
                                    <button ng-show="type === \'confirm\'" class="btn btn-submit" ng-click="confirm()">确定</button>\
                                    <button class="btn btn-close" ng-click="close()">取消</button>\
                                </div>\
                            </div>\
                        </div>';
        var defalutType = ['alert', 'confirm'];

        function getTemplate (type, template, url) {
            var d = $q.defer();
            if (type) {
                if (defalutType.indexOf(type) < 0) {
                    d.reject({error: 'modal类型只有alert和confirm'});
                } else {
                    d.resolve({
                        type: type,
                        tpl: defaultTpl
                    })
                }
            }
            else if (template) {
                d.resolve({
                    type: 'custom',
                    tpl: template
                });
            } else if (url) {
                $templateRequest(url, true).then(function (res) {
                    d.resolve({
                        type: 'custom',
                        tpl: res
                    });
                }, function (e) {
                    d.reject(e);
                })
            } else {
                d.reject({error: '缺少模版定义'});
            }
            return d.promise;
        }
        // 在指定的父节点上插入子节点,modal一般作为body的直接子节点
        
        function appendChild (parent, ele) {
            var childs = parent.children;
            if (childs.length > 0) {
                return $animate.enter(ele, parent, childs[childs.length - 1])
            }
            return $animate.enter(ele, parent);
        }
        this.show = function (options) {
            var d = $q.defer();

            // 指定控制器
            var controllerName = options.controller;
            if (!controllerName) {
                return d.reject({error: '缺少控制器定义'});
            }
            // scope;
            var modalScope = (options.scope || $rootScope).$new();

            getTemplate(options.type, options.template, options.templateUrl)
            .then(function (tplObj) {

                modalScope.type = tplObj.type;
                var linkFn = $compile(tplObj.tpl);
                var modalDom = linkFn(modalScope);

                // 传递给$controller函数的控制器依赖
                var controllerDependance = {
                    $scope: modalScope,
                    $element: modalDom,
                    close: function (cb) {
                        closeDefer.resolve(cb);
                        removeModal();
                    }
                }
                function removeModal () {
                    $animate.leave(modalDom)
                    .then(
                        function () {
                            modalScope.$destroy();
                            modalScope = null;
                            d = null;
                            closeDeferred = null;
                            confirmDefer = null;
                            modalObject = null;
                            controllerDependance = null;
                            modalDom = null;
                        },
                        function (e) {
                        })
                }
                var closeDefer = $q.defer();
                var confirmDefer = $q.defer();

                if (tplObj.type === 'confirm') {
                    controllerDependance.confirm = function (cb) {
                        confirmDefer.resolve(cb);
                        removeModal();
                    }
                }
                
                // 合并选项中的数据
                if (options.inject) {
                    angular.extend(controllerDependance, options.inject);
                }

                var modalController = $controller(options.controller, controllerDependance);

                if (options.controllerAs) {
                    modalScope[options.controllerAs] = modalController;
                }

                appendChild(document.body, modalDom);
                
                var modalObject = {
                    controller: modalController,
                    scope: modalScope,
                    element: modalDom,
                    close: closeDefer.promise
                };

                if (tplObj.type === 'confirm') {
                    modalObject.confirm = confirmDefer.promise;
                }
                d.resolve(modalObject);

            }, function (e) {
                d.reject(e)
            });
            return d.promise;
        }

    }
    return new ModalService();
    })