65.9K
CodeProject 正在变化。 阅读更多。
Home

用于文件浏览和拖放的自定义 AngularJS 指令

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (13投票s)

2014 年 9 月 4 日

CPOL

7分钟阅读

viewsIcon

36746

downloadIcon

606

描述了如何在 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

'A'

'M'

'AEC'

将指令匹配到元素('E')、属性('A')、类('C')或注释('M')。默认值为 'EA'。
更换 true

True 表示用此指令替换匹配的元素。False 表示将此指令嵌入到匹配的元素中。默认值为 false

优先级 10

如果一个元素上有多个自定义指令,则定义它们的执行顺序。值越高表示越早执行。默认值为 0

require

'?ngModel'

['^?myCalendar', 'myClock']

在同一元素级别上搜索一个或多个命名的指令控制器。如果找到,则将它们传递给此指令的链接函数。如果未找到,则抛出错误。每个传入的名称可以带有 '^' 或 '?' 或两者兼有作为前缀。'^' 表示也在所有祖先元素中搜索。'?' 表示如果未找到则不抛出错误。

作用域 (scope)
true

{
  name: '@',
  age: '=newAge',
  select: '&onSelect'
}

False 表示使用现有的作用域,即最近的外部控制器的作用域。

True 表示创建一个新的作用域,该作用域继承自最近的外部控制器的作用域。您可以在新作用域中访问所有外部作用域中的对象。

您也可以使用对象字面量来创建一个隔离作用域,在该作用域中您无法访问任何外部作用域的对象。但您仍然可以使用 $parent 来访问外部作用域。

默认值为 false

template

'<span ng-transclude><span>'

此指令的内联模板。它不能与 templateUrl 一起使用。

templateUrl

'myTpl.html'

此指令的模板 URL。它不能与 template 一起使用。

transclude true

如果为 true,则此指令匹配的元素的内容将被移动到此指令模板内的 ng-transclude 匹配的元素中。默认值为 false

link

function(scope, element, attrs, ngModel) { }

当 Angular 将此指令链接到其作用域时调用。我们可以在此处初始化作用域并添加事件监听器。它等于 compile 返回的 post 链接函数。
compile

function(element, attrs, transclude) { }

当 Angular 编译模板到 DOM 时调用。在编译时作用域不可用。可以选择性地返回 prepost 链接函数。

控制器 (controller)

function($scope, $element, $attrs, $transclude) { }

一个控制器构造函数,用于向其他指令公开 API。另一个指令可以通过 require 将此控制器注入其链接函数。这是不同指令之间进行通信的最佳方式。

让我们在接下来的部分进一步讨论一些复杂的话题

  • 范围
  • 编译和链接

范围

默认情况下,scopefalse,这意味着使用最近的外部控制器的作用域。有时,我们不想污染外部 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>

userNameuserAge 都是 outerController 的成员变量,onIncomeChanged 是它的成员函数。非常重要的是要注意函数参数 - newIncome。我们应该通过一个对象字面量来传递该值,正如 myDirectivelink 函数所指示的那样。

编译和链接

对于每个自定义指令,compilelink 之间存在以下差异

  • compile 在调用顺序上先于 link
  • link 可以访问 scope,而 compile 则不能,因为在编译时 scope 尚未创建。
  • compile 只执行一次,而 link 当匹配的元素可以通过 ngRepeat 重复时会执行多次。
  • 我们可以在 compile 中返回 prepost 链接函数。实际上,post 等同于 link。代码可以写成如下
compile: function (element, attrs, transclude) {
    return {
        pre: function (scope, element, attrs) { ... },
        post: function (scope, element, attrs) { ... }
    };
}

无论差异如何,我们可以使用 compilelink 来实现通用目的。

解释 fuFileBrowser

用法

此指令为 HTML5 文件控件 - <input type="file"/> 启用 ngModel 绑定。要使用此指令,我们需要

  1. 通过 <script src="directives/filebrowser.js"></script> 引用脚本文件。
  2. 在模块声明中注入依赖项,例如 angular.module('demoApp', ['fu.directives.fileBrowser'])
  3. 在 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 非常相似

  1. 通过 <script src="directives/filedropper.js"></script> 引用脚本文件。
  2. 在模块声明中注入依赖项,例如 angular.module('demoApp', ['fu.directives.fileDropper'])
  3. 在 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 中找到样式类。

参考文献

© . All rights reserved.