I have two directives, one checks the size of the file, while the other one ensures that the user is uploading a valid file format. They are both assigned to a input=file element, and seperate they work, but together, the validFileSize directive seems to cause the selectNgValidationFiles to not work.
//view
<input type="file" name="uploadedDocs" id="uploadedDocs" data-ng-model="fileStore.file" select-ng-validation-files valid-file-size multiple>
//controller
function validFileSize($parse){
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, el, attrs, ngModel) {
var model = $parse(attrs.ngModel);
var modelSetter = model.assign;
var maxSize = 2000; //2000 B
el.bind('change', function() {
scope.$apply(function() {
scope.fileStore.maxSizeError = false;
if (el[0].files.length > 1) {
modelSetter(scope, el[0].files);
} else {
modelSetter(scope, el[0].files[0]);
}
if(el[0].files.length > 0) {
var fileSize = el[0].files[0].size;
if (fileSize === 0) {
scope.fileStore.maxSizeError = true;
}
}
});
});
}
}
}
function selectNgValidationFiles() { //if files to be uploaded vary in future, add condition to check type or create new directive
return {
require: "ngModel",
link: function postLink(scope,elem,attrs,ngModel) {
var validFormats = ['pdf', 'PDF', 'doc', 'DOC', 'docx', 'DOCX', 'jpg', 'JPG', 'jpeg', 'JPEG','png', 'PNG', 'gif', 'GIF', 'pptx', 'PPTX', 'csv', 'CSV', 'xlsx', 'XLSX', 'zip', 'ZIP'];
elem.bind('change', function () {
validFile(false);
scope.$apply(function () {
ngModel.$render();
});
});
ngModel.$render = function () {
console.log('elem : ',elem)
ngModel.$setViewValue(elem[0].files[0]);
};
function validFile(bool) {
ngModel.$setValidity('pdfIncorrect', bool);
}
ngModel.$parsers.push(function(value) {
var ext = value.name.substr(value.name.lastIndexOf('.')+1);
if(ext=='') return;
if(validFormats.indexOf(ext) == -1){
console.log('not valid format')
return value;
}
validFile(true);
return value;
});
}
}
};
question
What is causing my directives to break one another? I assume one is overwriting the other somehow but I can't work out how to fix it.
Could you not merge them into one directive or one service function?
Check file format first, if ok then proceed with file size check.
It's probably because you are now calling the $scope apply function twice (one in each directive), which generally does not work well.
Related
I'm writing an AngularJS application and I'm searching for a way to unit test every single aspect.
In this particular case, I need to unit test a custom directive which I've written that represents a control.
The directive can be found here:
var officeButton = angular.module('OfficeButton', []);
officeButton.directive('officeButton', function() {
return {
restrict: 'E',
replace: false,
scope: {
isDefault: '#',
isDisabled: '#',
control: '=',
label: '#'
},
template: '<div class="button-wrapper" data-ng-click="onClick()">' +
'<a href="#" class="button normal-button">' +
'<span>{{label}}</span>' +
'</a>' +
'</div>',
controller: ['$scope', function($scope) {
var event = this;
var api = {
changeLabel: function(label) {
$scope.label = label;
},
enable: function() {
$scope.isDisabled = false;
},
disable: function() {
$scope.isDisabled = true;
},
setAsDefault: function() {
$scope.isDefault = true;
},
removeDefault: function() {
$scope.isDefault = false;
}
};
event.onClick = function() {
if (typeof $scope.control.onClick === 'function') { $scope.control.onClick(); }
};
$.extend($scope.control, api);
function Init() {
if ($scope.isDefault === 'true') { $scope.isDefault = true; }
else { $scope.isDefault = false; }
}
Init();
}],
link: function(scope, element, attributes, controller) {
scope.$watch('isDefault', function(value) {
if (value === 'true' || value) { $('a', element).addClass('button-default'); }
else { $('a', element).removeClass('button-default'); }
});
scope.onClick = function() { controller.onClick(); }
}
}
});
This directive can be called by using the following HTML snippet:
<office-button label="Office Web Controls" control="buttonController"></office-button>
Now, this directive exposes an API which functions such as changeLabel, enable, disable, ....
Now, those functions are not defined on the load of the application, meaning if at the bottom of my HTML I call the following code:
$scope.buttonController.changeLabel('Office Web Controls for Web Applications Directive Demo');
It will throw an error because the changeLabel() method is not defined.
In order to make it function, I need to wrap those calls in an angular.ready function, such as:
angular.element(document).ready(function () {
$scope.buttonController.changeLabel('Office Web Controls for Web Applications Directive Demo');
});
Here's a plunker for your information.
Now, I'm writing unit tests using Jasmine, and here's what I have for the moment:
describe('Office Web Controls for Web Applications - Button Testing.', function() {
// Provides all the required variables to perform Unit Testing against the 'button' directive.
var $scope, element;
var buttonController = {};
// Loads the directive 'OfficeButton' before every test is being executed.
beforeEach(module('OfficeButton'));
// Build the element so that it can be called.
beforeEach(inject(function($rootScope, $compile) {
// Sets the $scope variable so that it can be used in the future.
$scope = $rootScope;
$scope.control = buttonController;
element = angular.element('<office-button control="buttonController"></office-button>');
$compile(element)($scope);
$scope.$digest();
}));
it('Should expose an API with certain functions.', function() {
});
});
Now, in the it function, I would like to test if the $scope.control does expose the API as defined in the directive.
The problem is that the page needs to be ready before the API is available.
Any tought on how to change the code or how to unit test this correctly?
I've found the issue, it was just a wrong configuration on the unit test.
When using this code:
$scope.control = buttonController;
element = angular.element('<office-button control="buttonController"></office-button>');
I must change the element to:
$scope.control = buttonController;
element = angular.element('<office-button control="control"></office-button>');
I have to get a picture in a modal just after its upload. So I'm opening a modal ($scope.openResizeModal()), and putting my picture in my scope.
uploads.client.controller.js
$scope.openResizeModal = function (flow) {
$scope.pictureToCrop = {};
$scope.pictureToCrop = flow.files[0];
}
Then I try to print in on my HTML view :
resize-modal.client.view.html
<img-cropped src="{{pictureToCrop}}" selected='selected(cords)'></img-cropped>
And AngularJS tells :
Error: [$interpolate:interr] Can't interpolate: {{pictureToCrop}}
TypeError: Converting circular structure to JSON
I even tried with a classic
<img src={{pictureToCrop}} />
But it's the same.
Here is my directive :
angular.module('uploads').directive('imgCropped', function () {
return {
restrict: 'E',
replace: true,
scope: { src: '#', selected: '&' },
link: function (scope, element, attr) {
var myImg;
var clear = function () {
if (myImg) {
myImg.next().remove();
myImg.remove();
myImg = undefined;
}
};
scope.$watch('src', function (nv) {
clear();
if (nv) {
element.after('<img />');
myImg = element.next();
myImg.attr('src', nv);
$(myImg).Jcrop({
trackDocument: true,
onSelect: function (x) {
scope.$apply(function () {
scope.selected({ cords: x });
});
},
aspectRatio: 1/1.4
}, function () {
// Use the API to get the real image size
var bounds = this.getBounds();
pwGlobal.boundx = bounds[0];
pwGlobal.boundy = bounds[1];
});
}
});
scope.$on('$destroy', clear);
}
};
});
Thanks all !
It was a flow internal problem :
var fileReader = new FileReader();
fileReader.readAsDataURL(flow.files[0].file);
fileReader.onload = function (event) {
$scope.pictureToCrop = event.target.result;
};
Solved my problem. The image is converted in base64 encode.
I got any application where I need to display file from urls I got in database. Now this file can be an image and it can be a pdf. So I need to set some binding dynamically. I looked on internet and object tag looked promising but it is not working in IE11. It is working fine in Chrome and Firefox. SO that is why I am asking here for help.
I have created a directive just to make sure If we have to do any dom manipulation. Here goes my directive code.
mainApp.directive("displayFile", function () {
return {
restrict: 'AE', // only activate on element attribute
scope: {
displayFile: "=",
fileType:"="
},
link: function (scope, elem, attrs) {
scope.filePath = "";
var element = angular.element(elem);
// observe the other value and re-validate on change
scope.$watch('displayFile', function (val) {
if (val !== "") {
scope.filePath = val;
scope.type="application/"+ fileType;
//element.attr("data", scope.filePath)
}
});
},
template: '<object data="{{filePath}}" type="{{type}}">'
}
});
My html for directive
<div data-display-pdf="fileUrl" file-type="type"></div>
Attaching an image also for IE and Chrome/FF output
Above image is a comparison between IE and FF
Final cut of directive which is working on IE11, Chrome and Firefox
use it like
<div data-display-file="fileObject"></div>
where fileObject is like
$scope.fileObject = {
fileUrl: "",
type: ""
}
mainApp.directive("displayFile", function () {
var updateElem = function (element) {
return function (displayFile) {
element.empty();
var objectElem = {}
if (displayFile && displayFile.type !== "") {
if (displayFile.type === "pdf") {
objectElem = angular.element(document.createElement("object"));
objectElem.attr("data", displayFile.fileUrl);
objectElem.attr("type", "application/pdf");
}
else {
objectElem = angular.element(document.createElement("img"));
objectElem.attr("src", displayFile.fileUrl);
}
}
element.append(objectElem);
};
};
return {
restrict: "EA",
scope: {
displayFile: "="
},
link: function (scope, element) {
scope.$watch("displayFile", updateElem (element));
}
};
});
Is there a simple way to place a single tri-state checkbox on a web-page and bind it to a boolean model so the latter can take true, false or null values?
The closest solution I found so far is http://jsfiddle.net/HB7LU/454/ but it has a flaw when setting up an initial view state (as there is no way to get a model value during first rendering). Any other suggestions deal with multiple child checkboxes and solves the problem by watching on them.
http://jsfiddle.net/xJhEG/ I made it in a commercial project. Tristates are true, false, null (not "unknown")
.directive('indeterminate', [function() {
return {
require: '?ngModel',
link: function(scope, el, attrs, ctrl) {
var truthy = true;
var falsy = false;
var nully = null;
ctrl.$formatters = [];
ctrl.$parsers = [];
ctrl.$render = function() {
var d = ctrl.$viewValue;
el.data('checked', d);
switch(d){
case truthy:
el.prop('indeterminate', false);
el.prop('checked', true);
break;
case falsy:
el.prop('indeterminate', false);
el.prop('checked', false);
break;
default:
el.prop('indeterminate', true);
}
};
el.bind('click', function() {
var d;
switch(el.data('checked')){
case falsy:
d = truthy;
break;
case truthy:
d = nully;
break;
default:
d = falsy;
}
ctrl.$setViewValue(d);
scope.$apply(ctrl.$render);
});
}
};
}])
Here my Fiddle, starting from TruongSinh and changing
http://jsfiddle.net/xJhEG/25/
without
var truthy = true;
var falsy = false;
var nully = null;
You have take advantage of the indeterminate state of <input type="checkbox">.
MDN web docs:
There exists an indeterminate state of checkboxes, one in which it is not checked or unchecked, but undetermined. This is set using the HTMLInputElement object's indeterminate property via JavaScript (it cannot be set using an HTML attribute).
PLUNKER: TRISTATE DIRECTIVE
HTML
<label>
<input type="checkbox" ng-model="state" indeterminate /> {{state}}
</label>
DIRECTIVE
app.directive('indeterminate', function() {
return {
restrict: 'A',
scope: {
model: '=ngModel'
},
link: function(scope, el, attrs, ctrl) {
var states = [true, false, undefined];
var index = states.indexOf(scope.model);
setIndeterminate();
el.bind('click', function() {
scope.model = states[++index % 3];
setIndeterminate();
});
function setIndeterminate() {
scope.$applyAsync(function() {
el[0].indeterminate = (scope.model === undefined);
});
}
}
};
});
I've created directive, which you can use.
Three-state checkbox AngularJS Directive on GitHub
There is also a post, how it was built: Creating Angular Directive "Three-state checkbox
You can try a DEMO
And the directive looks like that:
angular.module("threeStateCheckbox", [])
.directive("threeStateCheckbox", ['$compile', function($compile){
return {
restrict: "A",
transclude: true,
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
var states = [true, false, null];
var classNames = ["checked", "unchecked", "clear"];
scope.click = function(){
var st;
states.map(function(val, i){
if(ngModel.$modelValue === val){
st = states[(i+1)%3];
}
});
ngModel.$setViewValue(st);
ngModel.$render();
};
scope.tscClassName = function(){
var className;
states.map(function(val, i){
if(ngModel.$modelValue=== val){
className = classNames[i];
}
});
return className;
};
element.attr("class", "tri-sta-che ");
element.attr("ng-click", "click()");
element.attr("ng-class", "tscClassName()");
element.removeAttr("three-state-checkbox");
$compile(element)(scope);
}
};
}]);
Because all previous answers don't work since AngularJS 1.7 (the checkbox model only allows boolean values now and everything else gets converted to boolean), I now found a solution that works until v1.8:
Thanks to #The.Bear for the base.
To use it, simply include <tristate label="someOptionalText" ng-model="yourVariableToBindTo" /> and the corresponding directive.
var app = angular.module('tristatedemo', []);
app.controller('MyCtrl', function() {
this.state = null; //initial state
});
app.directive('tristate', function() {
return {
restrict: 'E',
scope: {
model: '=ngModel',
label: '#'
},
template: '<input type="checkbox" /> {{label}}',
link: function(scope, el, attrs, ctrl) {
var states = [true, false, null];
var index = states.indexOf(scope.model);
setIndeterminate();
el.bind('click', function() {
scope.model = states[++index % 3];
setIndeterminate();
});
function setIndeterminate() {
scope.$applyAsync(function() {
var cb = el.find('input')[0];
cb.checked = scope.model;
cb.indeterminate = (scope.model === null);
});
}
},
};
});
<!DOCTYPE html>
<html ng-app="tristatedemo">
<head>
<script data-require="angular.js#1.8.x" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.min.js" data-semver="1.8.2"></script>
</head>
<body ng-controller="MyCtrl as ctrl">
{{(ctrl.state === null) ? "null" : ctrl.state}}<br> <!-- only for demo -->
<tristate label="abc" ng-model="ctrl.state" />
</body>
</html>
A while ago I asked about "Angular.js rendering SVG templates in directives", where I was replacing the DOM nodes that angular makes when rendering templates, with SVG nodes. I got a response that answered it for me, but I realized that I lost all the databindings from angular.
See Plunkr (click update): http://plnkr.co/edit/HjOpqc?p=preview
How do I replace these DOM nodes with SVG nodes, and leave my angular bindings intact? I tried using $compile to make it work (as I've done with regular html), but its just not working.
code:
var svgNS = 'http://www.w3.org/2000/svg';
app.directive('path', ngSvg('path'));
app.directive('g', ngSvg('g'));
function ngSvg(type) {
return function($timeout, $compile) {
return {
restrict: 'E',
link: function(scope, el, attr) {
//skip nodes if they are already svg
if (el[0].namespaceURI === svgNS) {
return;
}
// I would expect the chunk of code below to work,
// but it does not with ng-repeat
// var newAttr = {};
// _.each(el[0].attributes, function(at) {
// newAttr[at.nodeName] = at.value;
// });
// var path = makeNode(type, el, newAttr);
// var parent = path.cloneNode(true);
// $compile(parent)(scope);
// var children = el.children();
// $(parent).append(children);
// $timeout(function() {
// el.replaceWith(parent);
// })
// this works for rendering, but does not update the svg elements
// when update is clicked
$timeout(function() {
var newAttr = {};
_.each(el[0].attributes, function(at) {
newAttr[at.nodeName] = at.value;
});
var path = makeNode(type, el, newAttr);
var parent = path.cloneNode(true);
var children = el.children();
$(parent).append(children);
el.replaceWith(parent);
});
}
}
}
}
/* Create a shape node with the given settings. */
function makeNode(name, element, settings) {
// var ns = 'http://www.w3.org/2000/svg';
var node = document.createElementNS(svgNS, name);
for (var attribute in settings) {
var value = settings[attribute];
if (value !== null && value !== null && !attribute.match(/\$/) &&
(typeof value !== 'string' || value !== '')) {
node.setAttribute(attribute, value);
}
}
return node;
}
This issue is solved in Angular 1.3 and here is an implementation of some custom svg directives with the behaviors you would expect from an Angular directive. There is now a special requirement is on the directive declaration as follows templateNamespace: 'svg'
Also notice I am overriding some reserved attributes, for example, x and height in regards to <rect/>. To retain more control over these you can leverage ng-attr as such '<rect ng-attr-width="{{ ngWidth }}" />
JSFiddle Link
Here is a custom <rect/> and <circle/>
app.directive('ngRect', [function () {
return {
templateNamespace: 'svg',
replace: true,
template: '<rect ng-attr-width="{{ ngWidth }}" ng-attr-height="{{ ngHeight }}" ng-attr-x="{{ ngX }}" ng-attr-y="{{ ngY }}" ng-click="ngRectClick()"/>',
scope: {
'ngHeight': '=',
'ngWidth': '='
},
link: function (scope, elem, attrs) {
scope.ngRectClick = function() {
console.log(elem);
}
}
}
}]);
app.directive('ngCircle', [function () {
return {
templateNamespace: 'svg',
replace: true,
template: '<circle ng-attr-cx="{{ ngCx }}" ng-attr-cy="{{ ngCy }}" ng-attr-r="{{ ngR }}" ng-attr-fill="{{ ngFill }}" ng-click="ngCircleClick()"/>',
scope: {
'ngCx': '=',
'ngCy': '=',
'ngR': '=',
'ngFill': '='
},
link: function (scope, elem, attrs) {
scope.ngCircleClick = function() {
console.log(elem);
}
}
}
}]);