Related
I'd like to enforce unique usernames in my Firebase app and immediately let the user know if the inserted username is already taken or not.
I looked into AngularJS's ngModel since it has a built-in asyncValidator in its controller (example in Custom Validation), but I'm still stuck in get it work.
html
<form name="settings" novalidate>
<div>
Username:
<input type="text" ng-model="user.username" name="username" app-username><br>
<span ng-show="settings.username.$pending.appUsername">Checking if this username is available...</span>
<span ng-show="settings.username.$error.appUsername">This username is already taken!</span>
</div>
</form>
directive
app.directive('appUsername', function() {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var ref = new Firebase("https://<MY-APP>.firebaseio.com");
ctrl.$asyncValidators.appUsername = function (modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty model valid
return true;
}
var q = ref.child('users').orderByChild('username').equalTo(modelValue);
q.once('value', function (snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
return true;
} else {
// username already exists, ask user for a different name
return false;
}
});
};
};
};
});
Is it possible to use Firebase in an asyncValidator?
Thanks in advance.
Yes, it's possible. However, the issue you're running into isn't Firebase related. Per the Angular docs you linked:
Functions added to the object must return a promise that must be resolved when valid or rejected when invalid.
So you simply need to use a deferred as done in the Angular docs, or use $q(function(resolve, reject) {...}). I've used the latter below.
app.directive('appUsername', function($q) {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var ref = new Firebase("https://<MY-APP>.firebaseio.com");
ctrl.$asyncValidators.appUsername = function (modelValue, viewValue) {
return $q(function(resolve, reject) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty model valid
resolve();
}
var usernameRef = ref.child('users').orderByChild('username').equalTo(modelValue);
usernameRef.once('value', function (snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
resolve();
} else {
// username already exists, ask user for a different name
reject();
}
});
});
};
};
};
});
I'm trying to create a directive that will allow me to bind to hash of other scope properties.
HTML
<div lookup lookup-model="data.countryId"></div>
<div lookup lookup-model="data.stateId" lookup-params="{countryId: data.countryId}"></div>
What I would like to be able to do is every time a value in lookup-params is updated to refresh the lookup with the model of data.stateId. I'm trying to keep this generic since there is likely a variety of different lookup-params I'll want to have.
Is there a way to do this in Angular?
Update
I certainly didn't provide enough detail. Clicked submit too soon. Here is my solution based off feedback from #Olivvv. The suggestion led me to the scope.$eval function.
The goal here was to create a directive that would allow us to use a select with a $http get to seed the options within the select. Some of the $http requests will need a parameter since their is a dependency on another value. For example, a set of states can only be provided when a country value is provided.
The following is the code I pulled together. I'm sure it can be improved, but it is doing the trick at the moment. Please note, I'm using Lodash for some utility functions. You'll also see a scope object "lookupModelObject". This was purely to meet a design need for styling selects. It probably can be ignored if you are only interested in the lookupParams.
HTML Snippet
<div select-lookup lookup-value="block.data.countryId" lookup-type="countries" lookup-placeholder="Select a Country"></div>
<div select-lookup lookup-value="block.data.stateId" lookup-type="states" lookup-params="{countryId: block.data.countryId}" lookup-placeholder="Select a State"></div>
Directive
The important part to point out is how I'm evaluating the attrs.lookupParams. If the attribute exists I use scope.$eval to evaluate the attribute. You'll later see how I added each parameter to scope and added a watcher in case one of the params changed. This would allow me to reset the "state" to null if a different country was selected.
angular.module("foo").directive('selectLookup', ['$http', '$q', function($http, $q) {
return {
restrict: 'A',
replace: true,
templateUrl: 'common/lookups/partials/selectLookupPartial.html',
scope: {
id: "#",
lookupValue: "=",
lookupParams: "="
},
link: function(scope, element, attrs) {
// Initialize the options.
scope.options = [];
var optionsLoaded = false;
var resetLookupModel = function() {
scope.lookupModelObject = {
id: null,
text: attrs.lookupPlaceholder
};
};
// Evaluate and obtain the lookup parameters for this lookup.
var lookupParams = {};
if (attrs.lookupParams) {
lookupParams = scope.$eval(attrs.lookupParams);
}
var updateLookupModelObject = function(value) {
// This function is only relevant if the options have been loaded.
if (optionsLoaded) {
if (value === undefined || value === null) {
resetLookupModel();
}
else {
var item = _.findWhere(scope.options, {id: value});
if (item) {
scope.lookupModelObject = item;
}
else {
resetLookupModel();
}
}
}
};
var fetchValues = _.throttle(function() {
var deferred = $q.defer(),
fetchUrl = "/api/lookup/" + attrs.lookupType,
keys = _.keys(lookupParams);
_.each(keys, function(key, index) {
if (index === 0) {
fetchUrl += "?";
}
else {
fetchUrl += "&";
}
fetchUrl += key + "=" + lookupParams[key];
});
// Empty the options.
scope.options.splice(0, scope.options.length);
$http.get(fetchUrl).then(function(response) {
scope.options = response.data;
optionsLoaded = true;
updateLookupModelObject(scope.lookupValue);
deferred.resolve(scope.options);
});
return deferred.promise;
}, 150);
// Setup the watchers
// If there are lookup params add them to scope so we can watch them.
var keys = _.keys(lookupParams);
_.each(keys, function(key) {
scope[key] = lookupParams[key];
// Setup watchers for each param.
scope.$watch(key, function() {
fetchValues();
});
});
scope.$watch('lookupParams', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
lookupParams = newValue;
fetchValues().then(resetLookupModel);
}
});
scope.$watch('lookupValue', updateLookupModelObject);
scope.$watch('lookupModelObject', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
scope.lookupValue = newValue.id;
}
});
fetchValues();
}
};
}]);
Template
We had a design constraint that forced us to introduce a "select-placeholder". Outside of that the select is the "typical" way to setup a select in Angular.
<div class="container select-container">
<div class="select-placeholder">
<span class="select-placeholder-text">{{ lookupModelObject.text }}</span>
<select class="select-input" data-ng-model="lookupModelObject" id="{{ id }}" data-ng-options="option as option.text for option in options"></select>
</div>
</div>
I solved it with the following function in a "utils" service:
function utils ($parse) {
this.$params = function(attrs, attrName, allowedParams){
var out = {},
parsed = $parse(attrs[attrName])();
if (typeof parsed === 'object'){
angular.forEach(parsed, function(val, key){
if (allowedParams.indexOf(key) !== -1){
this[key] = val;
} else {
//do some logging. i.e ('parameter not recognized :', key, ' list of the allowed params:', allowedParams)
}
}, out);
}else{
out[allowedParams[0]] = attrs[attrName];
}
return out;
};
}
use it that way in your template:
lookup-params="{countryId: '{{data.countryId}}'}"
and in your directive :
var lookupParams = utils.$params(attrs, 'lookup-params', ['countryId','anotherParams', 'etcetera']);
The first of the allowed params can be passed directly as string instead of an object:
lookup-params="{'{{data.countryId}}'}"
will work
I am working on a line of business application that is using Angular to create a SPA around a Node.js api server. I decided on using ui-router cause of the state-machine and their intuitive way of embedding urls but I chanced upon a slight challenge when creating dynamic URLs within a directive.
I am using jQuery Datatables for my data grid as a directive but any action links generated using 'fnRender' don't seem to compile the 'ui-sref' to their respective 'href'links. The directive code is as follows:
app.directive('datatable', function ($http) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
var responsiveHelper;
var breakpointDefinition = {
tablet: 1024,
phone : 480
};
var options = {
bDeferRender: true,
sPaginationType: "full_numbers",
oLanguage: {
sEmptyTable: "No records returned",
sSearch: "<span>Search:</span> ",
sInfo: "Showing <span>_START_</span> to <span>_END_</span> of <span>_TOTAL_</span> entries",
sLengthMenu: "_MENU_ <span>entries per page</span>",
sProcessing: "Loading..."
},
sDom: "lfrtip",
oColVis: {
buttonText: "Change columns <i class='icon-angle-down'></i>"
},
oTableTools: {
sSwfPath: "js/plugins/datatable/swf/copy_csv_xls_pdf.swf"
},
bAutoWidth : false,
fnPreDrawCallback: function () {
if (!responsiveHelper) {
responsiveHelper = new ResponsiveDatatablesHelper($elem, breakpointDefinition);
}
},
fnRowCallback : function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
responsiveHelper.createExpandIcon(nRow);
},
fnDrawCallback : function (oSettings) {
responsiveHelper.respond();
}
};
if (typeof $scope.dtOptions !== 'undefined') {
angular.extend(options, $scope.dtOptions);
}
if (attrs['dtoptions'] === undefined) {
for (property in attrs) {
switch (property) {
case 'sajaxsource':
options['sAjaxSource'] = attrs[property];
break;
case 'sajaxdataprop':
options['sAjaxDataProp'] = attrs[property];
break;
}
}
} else {
angular.extend(options, $scope[attrs['dtoptions']]);
}
if (typeof options['sAjaxSource'] === 'undefined') {
throw "Ajax Source not defined! Use sajaxsource='/api/v1/*'";
}
if (typeof options['fnServerData'] === 'undefined') {
options['fnServerData'] = function (sSource, aoData, resultCb) {
$http.get(sSource, aoData).then(function (result) {
resultCb(result.data);
});
};
}
options.aoColumnDefs = [];
$elem.find('thead th').each(function() {
var colattr = angular.element(this).data();
if (colattr.mdata) {
if (colattr.mdata.indexOf("()") > 1) {
var fn = $scope[colattr.mdata.substring(0, colattr.mdata.length - 2)];
if (typeof fn === 'function') {
options.aoColumnDefs.push({
mData: fn,
sClass: colattr.sclass,
aTargets: [colattr.atargets]
});
} else {
throw "mData function does not exist in $scope.";
}
} else {
options.aoColumnDefs.push({
mData: colattr.mdata,
sClass: colattr.sclass,
bVisible: colattr.bvisible,
aTargets: [colattr.atargets]
});
}
} else {
if (colattr.fnrender.indexOf("()") > 1) {
var fn = $scope[colattr.fnrender.substring(0, colattr.fnrender.length - 2)];
if (typeof fn === 'function') {
options.aoColumnDefs.push({
fnRender: fn,
sClass: colattr.sclass,
aTargets: [colattr.atargets]
});
} else {
throw "fnRender function does not exist in $scope.";
}
} else {
options.aoColumnDefs.push({
fnRender: function (oObj) {
return "<a tooltip class='btn' title='View' ui-sref=\""+colattr.tag+".show({slug:\'"+oObj.aData._id+"\'})\"><center class=\"icon-search\"></center></a>";
},
sClass: colattr.sclass,
bVisible: colattr.bvisible,
aTargets: [colattr.atargets]
});
}
}
});
$elem.dataTable(options);
$(".dataTables_length select").wrap("<div class='input-mini'></div>").chosen({disable_search_threshold: 9999999 });
}
}
});
It runs with out complications and even generates the following anchor tag:
<a ui-sref="organisation.show({slug:'527a44c02aa9ce3a1c3fbc17'})"></a>
However, ui-router doesn't compile it to a respective state url. What could be the issue? Is there some configuration I may have missed?
Thanks
Are you using any other directives or expressions within your data table? I'm guessing those wouldn't work either, because it looks like Angular never gets the opportunity to compile any of the HTML you're generating.
This has nothing to do with uiSref and everything to do with writing directives correctly. In terms of best practices, this directive has way too much code. You should look at decomposing it into multiple nested directives and straight HTML. I'd suggest spending some time learning about transclusion and doing nested directives with directive controllers.
Leaving aside best practice, I just encountered and resolved this issue myself. Because DataTables is modifying the DOM outside of Angular's event loop, the ui-sref attributes don't get compiled. It's a simple fix: you need to call $compile on each row as it's created.
Here's my (much simpler) directive:
function datatable($compile) {
return {
restrict: "A",
link: (scope, elem, attrs) => {
// THE IMPORTANT BIT
scope.options.createdRow = function (row) {
$compile(row)(scope);
};
var api = elem.DataTable(scope.options);
var handleUpdates = function (newData) {
var data = newData || null;
if (data) {
api.clear();
api.rows.add(data).draw();
}
};
scope.$watch("options.data", handleUpdates, true);
},
scope: { options: "=" }
};
}
Basically, what I'm trying to accomplish, is to set focus to the first invalid element after a form submit has been attempted. At this point, I have the element being flagged as invalid, and I can get the $name of the element so I know which one it is.
It's "working" but a "$apply already in progress" error is being thrown...
So I must be doing something wrong here :)
Here's my code so far:
$scope.submit = function () {
if ($scope.formName.$valid) {
// Good job.
}
else
{
var field = null,
firstError = null;
for (field in $scope.formName) {
if (field[0] != '$')
{
if (firstError === null && !$scope.formName[field].$valid) {
firstError = $scope.formName[field].$name;
}
if ($scope.formName[field].$pristine) {
$scope.formName[field].$dirty = true;
}
}
}
formName[firstError].focus();
}
}
My field looping is based on this solution, and I've read over this question a few times. It seems like the preferred solution is to create a directive, but adding a directive to every single form element just seems like overkill.
Is there a better way to approach this with a directive?
Directive code:
app.directive('ngFocus', function ($timeout, $log) {
return {
restrict: 'A',
link: function (scope, elem, attr) {
scope.$on('focusOn', function (e, name) {
// The timeout lets the digest / DOM cycle run before attempting to set focus
$timeout(function () {
if (name === attr.ngFocusId) {
if (attr.ngFocusMethod === "click")
angular.element(elem[0]).click();
else
angular.element(elem[0]).focus();
}
});
})
}
}
});
Factory to use in the controller:
app.factory('focus', function ($rootScope, $timeout) {
return function (name) {
$timeout(function () {
$rootScope.$broadcast('focusOn', name);
}, 0, false);
};
});
Sample controller:
angular.module('test', []).controller('myCtrl', ['focus', function(focus) {
focus('myElement');
}
Building a directive is definitely the way to go. There is otherwise no clean way to select in element in angularjs. It's just not designed like this. I would recommend you to check out this question on this matter.
You wouldn't have to create a single directive for every form-element. On for each form should suffice. Inside the directive you can use element.find('input');. For the focus itself I suppose that you need to include jQuery and use its focus-function.
You can howerever - and I would not recommend this - use jQuery directly inside your controller. Usually angular form-validation adds classes like ng-invalid-required and the like, which you can use as selector. e.g:
$('input.ng-valid').focus();
Based on the feedback from hugo I managed to pull together a directive:
.directive( 'mySubmitDirty', function () {
return {
scope: true,
link: function (scope, element, attrs) {
var form = scope[attrs.name];
element.bind('submit', function(event) {
var field = null;
for (field in form) {
if (form[field].hasOwnProperty('$pristine') && form[field].$pristine) {
form[field].$dirty = true;
}
}
var invalid_elements = element.find('.ng-invalid');
if (invalid_elements.length > 0)
{
invalid_elements[0].focus();
}
event.stopPropagation();
event.preventDefault();
});
}
};
})
This approach requires jquery as the element.find() uses a class to find the first invalid element in the dom.
I tried to use ng-model on input tag with type file:
<input type="file" ng-model="vm.uploadme" />
But after selecting a file, in controller, $scope.vm.uploadme is still undefined.
How do I get the selected file in my controller?
I created a workaround with directive:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var reader = new FileReader();
reader.onload = function (loadEvent) {
scope.$apply(function () {
scope.fileread = loadEvent.target.result;
});
}
reader.readAsDataURL(changeEvent.target.files[0]);
});
}
}
}]);
And the input tag becomes:
<input type="file" fileread="vm.uploadme" />
Or if just the file definition is needed:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function () {
scope.fileread = changeEvent.target.files[0];
// or all selected files:
// scope.fileread = changeEvent.target.files;
});
});
}
}
}]);
I use this directive:
angular.module('appFilereader', []).directive('appFilereader', function($q) {
var slice = Array.prototype.slice;
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$render = function() {};
element.bind('change', function(e) {
var element = e.target;
$q.all(slice.call(element.files, 0).map(readFile))
.then(function(values) {
if (element.multiple) ngModel.$setViewValue(values);
else ngModel.$setViewValue(values.length ? values[0] : null);
});
function readFile(file) {
var deferred = $q.defer();
var reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsDataURL(file);
return deferred.promise;
}
}); //change
} //link
}; //return
});
and invoke it like this:
<input type="file" ng-model="editItem._attachments_uri.image" accept="image/*" app-filereader />
The property (editItem.editItem._attachments_uri.image) will be populated with the contents of the file you select as a data-uri (!).
Please do note that this script will not upload anything. It will only populate your model with the contents of your file encoded ad a data-uri (base64).
Check out a working demo here:
http://plnkr.co/CMiHKv2BEidM9SShm9Vv
How to enable <input type="file"> to work with ng-model
Working Demo of Directive that Works with ng-model
The core ng-model directive does not work with <input type="file"> out of the box.
This custom directive enables ng-model and has the added benefit of enabling the ng-change, ng-required, and ng-form directives to work with <input type="file">.
angular.module("app",[]);
angular.module("app").directive("selectNgFiles", function() {
return {
require: "ngModel",
link: function postLink(scope,elem,attrs,ngModel) {
elem.on("change", function(e) {
var files = elem[0].files;
ngModel.$setViewValue(files);
})
}
}
});
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<h1>AngularJS Input `type=file` Demo</h1>
<input type="file" select-ng-files ng-model="fileArray" multiple>
<code><table ng-show="fileArray.length">
<tr><td>Name</td><td>Date</td><td>Size</td><td>Type</td><tr>
<tr ng-repeat="file in fileArray">
<td>{{file.name}}</td>
<td>{{file.lastModified | date : 'MMMdd,yyyy'}}</td>
<td>{{file.size}}</td>
<td>{{file.type}}</td>
</tr>
</table></code>
</body>
This is an addendum to #endy-tjahjono's solution.
I ended up not being able to get the value of uploadme from the scope. Even though uploadme in the HTML was visibly updated by the directive, I could still not access its value by $scope.uploadme. I was able to set its value from the scope, though. Mysterious, right..?
As it turned out, a child scope was created by the directive, and the child scope had its own uploadme.
The solution was to use an object rather than a primitive to hold the value of uploadme.
In the controller I have:
$scope.uploadme = {};
$scope.uploadme.src = "";
and in the HTML:
<input type="file" fileread="uploadme.src"/>
<input type="text" ng-model="uploadme.src"/>
There are no changes to the directive.
Now, it all works like expected. I can grab the value of uploadme.src from my controller using $scope.uploadme.
I create a directive and registered on bower.
This lib will help you modeling input file, not only return file data but also file dataurl or base 64.
{
"lastModified": 1438583972000,
"lastModifiedDate": "2015-08-03T06:39:32.000Z",
"name": "gitignore_global.txt",
"size": 236,
"type": "text/plain",
"data": "data:text/plain;base64,DQojaWdub3JlIHRodW1ibmFpbHMgY3JlYXRlZCBieSB3aW5kb3dz…xoDQoqLmJhaw0KKi5jYWNoZQ0KKi5pbGsNCioubG9nDQoqLmRsbA0KKi5saWINCiouc2JyDQo="
}
https://github.com/mistralworks/ng-file-model/
This is a slightly modified version that lets you specify the name of the attribute in the scope, just as you would do with ng-model, usage:
<myUpload key="file"></myUpload>
Directive:
.directive('myUpload', function() {
return {
link: function postLink(scope, element, attrs) {
element.find("input").bind("change", function(changeEvent) {
var reader = new FileReader();
reader.onload = function(loadEvent) {
scope.$apply(function() {
scope[attrs.key] = loadEvent.target.result;
});
}
if (typeof(changeEvent.target.files[0]) === 'object') {
reader.readAsDataURL(changeEvent.target.files[0]);
};
});
},
controller: 'FileUploadCtrl',
template:
'<span class="btn btn-success fileinput-button">' +
'<i class="glyphicon glyphicon-plus"></i>' +
'<span>Replace Image</span>' +
'<input type="file" accept="image/*" name="files[]" multiple="">' +
'</span>',
restrict: 'E'
};
});
For multiple files input using lodash or underscore:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
return _.map(changeEvent.target.files, function(file){
scope.fileread = [];
var reader = new FileReader();
reader.onload = function (loadEvent) {
scope.$apply(function () {
scope.fileread.push(loadEvent.target.result);
});
}
reader.readAsDataURL(file);
});
});
}
}
}]);
function filesModelDirective(){
return {
controller: function($parse, $element, $attrs, $scope){
var exp = $parse($attrs.filesModel);
$element.on('change', function(){
exp.assign($scope, this.files[0]);
$scope.$apply();
});
}
};
}
app.directive('filesModel', filesModelDirective);
I had to do same on multiple input, so i updated #Endy Tjahjono method.
It returns an array containing all readed files.
.directive("fileread", function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var readers = [] ,
files = changeEvent.target.files ,
datas = [] ;
for ( var i = 0 ; i < files.length ; i++ ) {
readers[ i ] = new FileReader();
readers[ i ].onload = function (loadEvent) {
datas.push( loadEvent.target.result );
if ( datas.length === files.length ){
scope.$apply(function () {
scope.fileread = datas;
});
}
}
readers[ i ].readAsDataURL( files[i] );
}
});
}
}
});
I had to modify Endy's directive so that I can get Last Modified, lastModifiedDate, name, size, type, and data as well as be able to get an array of files. For those of you that needed these extra features, here you go.
UPDATE:
I found a bug where if you select the file(s) and then go to select again but cancel instead, the files are never deselected like it appears. So I updated my code to fix that.
.directive("fileread", function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var readers = [] ,
files = changeEvent.target.files ,
datas = [] ;
if(!files.length){
scope.$apply(function () {
scope.fileread = [];
});
return;
}
for ( var i = 0 ; i < files.length ; i++ ) {
readers[ i ] = new FileReader();
readers[ i ].index = i;
readers[ i ].onload = function (loadEvent) {
var index = loadEvent.target.index;
datas.push({
lastModified: files[index].lastModified,
lastModifiedDate: files[index].lastModifiedDate,
name: files[index].name,
size: files[index].size,
type: files[index].type,
data: loadEvent.target.result
});
if ( datas.length === files.length ){
scope.$apply(function () {
scope.fileread = datas;
});
}
};
readers[ i ].readAsDataURL( files[i] );
}
});
}
}
});
If you want something a little more elegant/integrated, you can use a decorator to extend the input directive with support for type=file. The main caveat to keep in mind is that this method will not work in IE9 since IE9 didn't implement the File API. Using JavaScript to upload binary data regardless of type via XHR is simply not possible natively in IE9 or earlier (use of ActiveXObject to access the local filesystem doesn't count as using ActiveX is just asking for security troubles).
This exact method also requires AngularJS 1.4.x or later, but you may be able to adapt this to use $provide.decorator rather than angular.Module.decorator - I wrote this gist to demonstrate how to do it while conforming to John Papa's AngularJS style guide:
(function() {
'use strict';
/**
* #ngdoc input
* #name input[file]
*
* #description
* Adds very basic support for ngModel to `input[type=file]` fields.
*
* Requires AngularJS 1.4.x or later. Does not support Internet Explorer 9 - the browser's
* implementation of `HTMLInputElement` must have a `files` property for file inputs.
*
* #param {string} ngModel
* Assignable AngularJS expression to data-bind to. The data-bound object will be an instance
* of {#link https://developer.mozilla.org/en-US/docs/Web/API/FileList `FileList`}.
* #param {string=} name Property name of the form under which the control is published.
* #param {string=} ngChange
* AngularJS expression to be executed when input changes due to user interaction with the
* input element.
*/
angular
.module('yourModuleNameHere')
.decorator('inputDirective', myInputFileDecorator);
myInputFileDecorator.$inject = ['$delegate', '$browser', '$sniffer', '$filter', '$parse'];
function myInputFileDecorator($delegate, $browser, $sniffer, $filter, $parse) {
var inputDirective = $delegate[0],
preLink = inputDirective.link.pre;
inputDirective.link.pre = function (scope, element, attr, ctrl) {
if (ctrl[0]) {
if (angular.lowercase(attr.type) === 'file') {
fileInputType(
scope, element, attr, ctrl[0], $sniffer, $browser, $filter, $parse);
} else {
preLink.apply(this, arguments);
}
}
};
return $delegate;
}
function fileInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
element.on('change', function (ev) {
if (angular.isDefined(element[0].files)) {
ctrl.$setViewValue(element[0].files, ev && ev.type);
}
})
ctrl.$isEmpty = function (value) {
return !value || value.length === 0;
};
}
})();
Why wasn't this done in the first place? AngularJS support is intended to reach only as far back as IE9. If you disagree with this decision and think they should have just put this in anyway, then jump the wagon to Angular 2+ because better modern support is literally why Angular 2 exists.
The issue is (as was mentioned before) that without the file api
support doing this properly is unfeasible for the core given our
baseline being IE9 and polyfilling this stuff is out of the question
for core.
Additionally trying to handle this input in a way that is not
cross-browser compatible only makes it harder for 3rd party solutions,
which now have to fight/disable/workaround the core solution.
...
I'm going to close this just as we closed #1236. Angular 2 is being
build to support modern browsers and with that file support will
easily available.
Alternatively you could get the input and set the onchange function:
<input type="file" id="myFileInput" />
document.getElementById("myFileInput").onchange = function (event) {
console.log(event.target.files);
};
Try this,this is working for me in angular JS
let fileToUpload = `${documentLocation}/${documentType}.pdf`;
let absoluteFilePath = path.resolve(__dirname, fileToUpload);
console.log(`Uploading document ${absoluteFilePath}`);
element.all(by.css("input[type='file']")).sendKeys(absoluteFilePath);