功能梳理
- 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();
})