用于文件浏览和拖放的自定义 AngularJS 指令
描述了如何在 AngularJS 中创建自定义指令,并提供示例来启用文件浏览和拖放的模型绑定。
引言
在我的工作中,我发现 AngularJS v1.2.16 无法为 <input type="file"/>
绑定模型。它也无法为可拖放的 <div>
绑定模型。因此,我不得不编写两个自定义指令,通过 ngModel
来实现这些类型的模型绑定,以避免使用 jQuery。
附件包含这两个指令的源代码和一个演示如何使用这些指令的示例。您可以将其提取并部署为 IIS 或任何其他 Web 服务器上的 Web 应用程序。请注意,如果直接在 Web 浏览器中(使用 file:/// 协议)打开 HTML 文件,演示可能无法正常工作。
有关更多详细信息和高级用法,请参阅 AngularJS 官方网站。AngularJS 确实是一个庞大而强大的框架。
自定义指令基础
首先,我将简要介绍如何编写自定义指令。如果您已经了解,可以跳过本章。
我们可以像下面的伪代码一样在模块中定义自定义指令
angular.module('myMod', ['depMod', ...]).directive('myDirective', function (injected, ...) {
return {
restrict: ...,
priority: ...,
scope: ...,
......
};
});
这个自定义指令的名称是 'myDirective
',它是驼峰式命名的。然后在 HTML 中,我们应该使用 'my-directive
' 作为名称。
请注意,我们可以声明自定义指令所依赖的其他模块,并将这些模块的服务注入到工厂函数中。工厂函数返回一个设置对象,用于告诉 AngularJS 如何创建指令。该设置对象可以包含以下字段
字段 | 示例 | 描述 |
restrict | ' ' ' | 将指令匹配到元素('E ')、属性('A ')、类('C ')或注释('M ')。默认值为 'EA '。 |
更换 | true |
|
优先级 | 10 | 如果一个元素上有多个自定义指令,则定义它们的执行顺序。值越高表示越早执行。默认值为 |
require | '
| 在同一元素级别上搜索一个或多个命名的指令控制器。如果找到,则将它们传递给此指令的链接函数。如果未找到,则抛出错误。每个传入的名称可以带有 ' |
作用域 (scope) | true
{
name: '@',
age: '=newAge',
select: '&onSelect'
}
|
您也可以使用对象字面量来创建一个隔离作用域,在该作用域中您无法访问任何外部作用域的对象。但您仍然可以使用 默认值为 |
template | ' | 此指令的内联模板。它不能与 |
templateUrl | ' | 此指令的模板 URL。它不能与 |
transclude | true | 如果为 |
link |
| 当 Angular 将此指令链接到其作用域时调用。我们可以在此处初始化作用域并添加事件监听器。它等于 compile 返回的 post 链接函数。 |
compile |
| 当 Angular 编译模板到 DOM 时调用。在编译时作用域不可用。可以选择性地返回 |
控制器 (controller) |
| 一个控制器构造函数,用于向其他指令公开 API。另一个指令可以通过 require 将此控制器注入其链接函数。这是不同指令之间进行通信的最佳方式。 |
让我们在接下来的部分进一步讨论一些复杂的话题
- 范围
- 编译和链接
范围
默认情况下,scope
为 false
,这意味着使用最近的外部控制器的作用域。有时,我们不想污染外部 scope
。因此,我们可以将 scope
设置为 true
来创建一个新的 scope
,该作用域继承最近外部 scope
的所有对象。然后,我们可以向这个新的 scope
添加对外部控制器不可见的新对象。
此外,我们可能希望创建一个全新的 scope
,而不继承外部 scope
的对象。在这种情况下,我们可以使用对象字面量创建一个隔离 scope
,如下面的代码片段所示
angular.module('myMod', []).directive('myDirective', function () {
return {
...
template: '<h5>{{name}}</h5><h5>{{age}}</h5>',
scope: {
name: '@',
age: '=newAge',
incomeChanged: '&'
},
link: function (scope) {
if (scope.incomeChanged) {
scope.incomeChanged({ newIncome: 1234 });
}
}
};
});
新的 scope
具有 3 个成员,它们使用 3 种不同的方式绑定外部控制器。
name: '@'
表示此成员获取匹配元素的 'name
' 属性的值。该值以纯string
的形式传入,并且只是一个单向绑定。age: '=newAge'
表示此成员与由匹配元素的 'new-age
' 属性指定的外部控制器对象进行双向绑定。incomeChanged: '&'
表示此成员是匹配元素的 'income-changed
' 属性指定的函数。这是将回调函数从外部控制器注入自定义指令的好方法。
如示例所示,如果我们希望属性名与成员名相同,则可以省略属性名。
现在我们可以在 HTML 中使用该指令,如下所示
<div ng-controller="outerController">
<my-directive name="{{userName}}"
new-age="userAge"
income-changed="onIncomeChanged(newIncome)">
</my-directive>
</div>
userName
和 userAge
都是 outerController
的成员变量,onIncomeChanged
是它的成员函数。非常重要的是要注意函数参数 - newIncome
。我们应该通过一个对象字面量来传递该值,正如 myDirective
的 link
函数所指示的那样。
编译和链接
对于每个自定义指令,compile
和 link
之间存在以下差异
compile
在调用顺序上先于link
。link
可以访问scope
,而compile
则不能,因为在编译时scope
尚未创建。compile
只执行一次,而link
当匹配的元素可以通过ngRepeat
重复时会执行多次。- 我们可以在
compile
中返回pre
和post
链接函数。实际上,post
等同于link
。代码可以写成如下
compile: function (element, attrs, transclude) {
return {
pre: function (scope, element, attrs) { ... },
post: function (scope, element, attrs) { ... }
};
}
无论差异如何,我们可以使用 compile
或 link
来实现通用目的。
解释 fuFileBrowser
用法
此指令为 HTML5 文件控件 - <input type="file"/>
启用 ngModel
绑定。要使用此指令,我们需要
- 通过
<script src="directives/filebrowser.js"></script>
引用脚本文件。 - 在模块声明中注入依赖项,例如
angular.module('demoApp', ['fu.directives.fileBrowser'])
。 - 在 HTML 中使用指令,例如
<input fu-file-browser type="file" ng-model="fileList" />
。
现在我们可以通过外部控制器 `s $scope.fileList` 变量获取选定的文件。
要启用多文件选择,我们可以添加 fu-multiple
属性;要在文件选择后立即清除选择,我们可以添加 fu-resetable
属性。
代码解释
我们可以查看 filebrowser.js。
angular.module('fu.directives.fileBrowser', []).directive('fuFileBrowser', function () {
return {
restrict: 'EA',
require: 'ngModel',
replace: true,
template: '<div><div><input type="file"
style="cursor:pointer"/></div></div>',
link: function (scope, element, attrs, ngModel) {
var container = element.children();
var bindFileControlChange = function () {
var fileControl = container.children();
fileControl.prop('multiple', attrs.fuMultiple !== undefined);
fileControl.change(function (evt) {
scope.$apply(function () {
ngModel.$setViewValue(evt.target.files);
});
if (attrs.fuResetable === undefined) {
return;
}
container.html(container.html()); // Reset must be done on div level
bindFileControlChange(); // Rebind after reset
});
};
bindFileControlChange();
}
};
});
首先,我通过 require:'ngModel'
声明了对 ngModel
控制器的依赖。然后,我将 ngModel
注入到 link
函数中,并使用其 API - $setViewValue
来更改外部控制器 scope
中绑定的值。我们必须在 scope.$apply
中进行更改,才能让 Angular 知道。
您会注意到我为这个指令使用了两级 <div>
。这是为了重置文件控件。我使用 div.html()
函数在每次将选定的文件传递给 ngModel
时重置文件控件。
解释 fuFileDropper
用法
此指令为拖放到 <div>
区域的文件启用 ngModel
绑定。使用它的步骤与 fuFileBrowser
非常相似
- 通过
<script src="directives/filedropper.js"></script>
引用脚本文件。 - 在模块声明中注入依赖项,例如
angular.module('demoApp', ['fu.directives.fileDropper'])
。 - 在 HTML 中使用指令,例如
<div fu-file-dropper ng-model="filesDropped">在此处拖放文件</div>
。
现在您可以通过外部控制器 `$scope.filesDropped` 变量获取拖放的文件。
代码解释
我们可以查看 filedropper.js。
angular.module('fu.directives.fileDropper', []).directive('fuFileDropper', function () {
return {
restrict: 'EA',
require: 'ngModel',
replace: true,
transclude: true,
template: '<div class="fu-drop-area" ng-transclude></div>',
link: function (scope, element, attrs, ngModel) {
var dropZone = element;
var dropZoneDom = element.get(0);
dropZoneDom.addEventListener('dragover', function (evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
dropZone.addClass("dragover");
}, false);
dropZoneDom.addEventListener('dragleave', function (evt) {
evt.stopPropagation();
evt.preventDefault();
dropZone.removeClass("dragover");
}, false);
dropZoneDom.addEventListener('drop', function (evt) {
evt.stopPropagation();
evt.preventDefault();
dropZone.removeClass("dragover");
scope.$apply(function () {
ngModel.$setViewValue(evt.dataTransfer.files);
});
}, false);
}
};
});
ngModel
的注入与 fuFileBrowser
相同。我将匹配元素的内容转接到模板的 <div>
中。我必须使用事件监听器来监视拖放事件。您可以在演示的 main.css 中找到样式类。
参考文献
- AngularJS 官方网站 (https://angularjs.org/)