Introduction
In my work, I found AngularJS v1.2.16 could not bind model for <input type="file"/>
. And it could not bind model for droppable <div>
either. So I had to write two custom directives to enable these kinds of model binding via ngModel
in order to avoid using jQuery.
The attachment contains the source code of the two directives and a demo indicating how to use the directives. You can extract and deploy it as a web application on IIS or any other web servers. Note the demo may not work properly if you directly open the HTML file in the web browser (with the file:/// protocol).
For more details and advanced usages, please refer to AngularJS's official web site. AngularJS is really a gigantic and powerful framework.
Custom Directive Basis
First let me briefly introduce how to write a custom directive. You could skip this chapter if you already know it.
We can define a custom directive in a module like the following pesudo-code:
angular.module('myMod', ['depMod', ...]).directive('myDirective', function (injected, ...) {
return {
restrict: ...,
priority: ...,
scope: ...,
......
};
});
This custom directive name is 'myDirective
' which is camel cased. Then we should use 'my-directive
' as the name in HTML.
Note we can declare other modules that the custom directive relies on and inject services of those modules into the factory function. The factory function returns a setting object to tell AngularJS how to create the directive. The setting object can contain the following fields:
Field | Example | Description |
restrict | 'A '
'M '
'AEC '
| Match the directive to Element ('E '), Attribute ('A '), Class ('C ') or Comment ('M '). The default value is 'EA '. |
replace | true | True to replace the matched element by this directive. False to embed this directive into the matched element. The default value is false .
|
priority | 10 | Define the execution order if there are multiple custom directives in one element. High value means early execution. The default value is 0 .
|
require | '?ngModel '
['^?myCalendar', 'myClock']
| Search one or multiple named directive controllers in the same element level. Pass them to this directive's link function if found. Throw an error if not found. Each pass-in name can be prefixed by '^ ' or '? ' or both. '^ ' means to search in all ancestors as well. '? ' means not to throw an error if not found.
|
scope |
true
{
name: '@',
age: '=newAge',
select: '&onSelect'
}
| False to use the existing scope which is the nearest outer controller's scope.
True to create a new scope which inherits from the nearest outer controller's scope. You can access all outer scoped objects in the new scope.
You can also use an object literal to create an isolated scope in which you cannot access any outer scoped objects. But you can still use $parent to access the outer scope.
The default value is false .
|
template | '<span ng-transclude><span> '
| The inline template for this directive. It cannot be used together with templateUrl .
|
templateUrl | 'myTpl.html '
| The URL of the template for this directive. It cannot be used together with template .
|
transclude | true | If it is true , the contents of the element matched by this directive will be moved into the element matched by ng-transclude inside this directive's template. The default value is false .
|
link | function(scope, element, attrs, ngModel) { }
| Called when Angular is linking this directive to its scope. We can initialize scope and add event listeners here. It equals the post link function returned from compile . |
compile | function(element, attrs, transclude) { }
| Called when Angular is compiling the template to DOM. Scope is not available in compiling time. Optionally, we can return pre and post link functions.
|
controller | function($scope, $element, $attrs, $transclude) { }
| A controller constructor to expose APIs to other directives. Another directive can inject this controller to its link function via require . This is the best way for different directives to communicate. |
Let's talk further about some complex topics in the following sections:
Scope
By default, scope
is false
which means to use the nearest outer controller's scope. Sometimes, we don't want to contaminate the outer scope
. So we can set scope
to true
to create a new scope
which inherits all objects of the nearest outer scope
. Then we can add to this new scope
new objects which are invisible to outer controllers.
Moreover, we may want to create a purely new scope
without inheriting outer scope
's objects. In this case, we can create an isolated scope
using an object literal like the following code piece:
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 });
}
}
};
});
The new scope
has 3 members that use 3 different ways to bind outer controllers.
name: '@'
means this member gets the value of the 'name
' attribute of the matched element. The value is passed in as a pure string
and is just a one-way binding. age: '=newAge'
means this member has a two-way binding with an outer controller's object specified by the 'new-age
' attribute of the matched element. incomeChanged: '&'
means this member is a function specified by the 'income-changed
' attribute of the matched element. This is a good way to inject a callback function from the outer controller to the custom directive.
As the example demonstrates, we can omit the attribute name if we want it to be the same as the member name.
Now we can use the directive in HTML as shown below:
<div ng-controller="outerController">
<my-directive name="{{userName}}"
new-age="userAge"
income-changed="onIncomeChanged(newIncome)">
</my-directive>
</div>
Both userName
and userAge
are outerController
's member variables and onIncomeChanged
is its member function. It's very important to notice the function argument - newIncome
. We should pass the value in an object literal as myDirective
's link
function indicates.
Compile and Link
For every custom directive, compile
and link
have the following differences:
compile
is antecedent to link
in the calling sequence. link
can access scope
while compile
cannot because scope
is not created yet during compliling time. compile
only executes once while link
executes multiple times when the matched element can be duplicated by ngRepeat
. - We can return
pre
and post
link functions in compile
. Actually, post
equals to link
. Code could be written like below:
compile: function (element, attrs, transclude) {
return {
pre: function (scope, element, attrs) { ... },
post: function (scope, element, attrs) { ... }
};
}
Regardless of the difference, we can use either compile
or link
for a general purpose.
Explain fuFileBrowser
Usage
This directive enables ngModel
binding for the HTML5 file control - <input type="file"/>
. To use this directive, we need to:
- Reference the script file via
<script src="directives/filebrowser.js"></script>
. - Inject the dependency in module declaration like
angular.module('demoApp', ['fu.directives.fileBrowser'])
. - Use the directive in HTML like
<input fu-file-browser type="file" ng-model="fileList" />
.
Now we can get the selected file through the outer controller's $scope.fileList
variable.
To enable selecting multiple files, we can add the fu-multiple
attribute; to clean the selections immediately after files are selected, we can add the fu-resetable
attribute.
Code Explanation
We can have a look in 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());
bindFileControlChange();
});
};
bindFileControlChange();
}
};
});
First, I declared the dependency on the ngModel
's controller by require:'ngModel'
. Then, I injected ngModel
to the link
function and used its API - $setViewValue
to change the binded value in the outer controller's scope
. We must make the change in scope.$apply
to let Angular know it.
You can notice I used two levels of <div>
for this directive. It is for resetting the file control. I used the div.html()
function to reset the file control every time when I have passed the selected files to ngModel
.
Explain fuFileDropper
Usage
This directive enables ngModel
binding for files dropped to a <div>
area. The steps to use it are very similar to fuFileBrowser
:
- Reference the script file via
<script src="directives/filedropper.js"></script>
. - Inject the dependency in module declaration like
angular.module('demoApp', ['fu.directives.fileDropper'])
. - Use the directive in HTML like
<div fu-file-dropper ng-model="filesDropped">Drop files here</div>
.
Now you can get the dropped files through the outer controller's $scope.filesDropped
variable.
Code Explanation
We can have a look in 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);
}
};
});
The injection of ngModel
is the same as fuFileBrowser
. I transcluded the content of the matched element into <div>
of the template. I had to use event listeners to monitor drag-drop events. You can find the style classes in main.css of the demo.
References