Quick question, so I'm using tg-dynamic-directive to loop through a json file and output following tree. (See image attached)
So the problem is, when the "tree" gets very long there are some serious performance problems because the browser needs to render a lot of items. (I'm talking about 1000 or longer). So what I'm trying to do is initially only load first 2 levels and the rest will be collapsed. When a user clicks expand arrow of each element I need to render its children. (If that makes sense). So basically run tg-dynamic-directive again.
When page starts rendering and function that returns template with the html is fired I have this to check if its first 2 levels:
$scope.getView = function (item) {
// Check if item is defined
if(typeof item != 'undefined') {
// Load course, label and module first!
if(item.type == 'course' || item.type == 'label' || item.type == 'module' || item.type == 'course_assessment' || item.type == 'module_assessment') {
// Return Template
return 'nestable_item.html';
} else {
// Otherwise return null
return null;
}
} else {
return null;
}
};
Then what I need to do is call that directive again when expand arrow is clicked.
This is the directive:
angular.module('tg.dynamicDirective', [])
.directive('tgDynamicDirective', ['$compile',
function($compile) {
'use strict';
function templateUrlProvider(getView, ngModelItem) {
if (getView) {
if (typeof getView === 'function') {
var templateUrl = getView(ngModelItem) || '';
if (templateUrl) {
return templateUrl;
}
} else if (typeof getView === 'string' && getView.length) {
return getView;
}
}
return '';
}
return {
restrict: 'E',
require: '^ngModel',
scope: true,
template: '<div ng-include="templateUrl"></div>',
link: function(scope, element, attrs, ngModel) {
scope.$watch(function() {
var ngModelItem = scope.$eval(attrs.ngModel);
var getView = scope.$eval(attrs.tgDynamicDirectiveView);
scope.ngModelItem = ngModelItem;
return templateUrlProvider(getView, ngModelItem);
}, function(newValue, oldValue) {
scope.templateUrl = newValue;
});
}
};
}
]);
My question is how can I fire tg-dynamic-directive again when expand arrow is clicked from the controller.
Use $rootScope.broadcast("XXXXXX"); and catch it
$rootScope.$on("XXXXXX", function() {
// function call
})
in directive and call the function you want.
or
use document.createElement("tg-dynamic-directive");
Related
Here is a sample angular directive to prevent typing non-numeric keys (StackOverflow answer).
I want to write something like this fiddle to use the is-number directive in several inputs. Please consider that since I have various different directives in my inputs, I cannot use the same template as suggested in the update of mentioned answer above.
var $scope;
var app = angular.module('myapp', []);
app.controller('Ctrl', function($scope) {
$scope.myNnumber1 = 1;
$scope.myNnumber2 = 1;
});
app.directive('isNumber', function () {
return {
require: 'ngModel',
link: function (scope, element) {
scope.$watch(element.ngModel, function(newValue,oldValue) {
newValue = String(newValue);
newValue = newValue.replace('۰', '0').replace('۱', '1').replace('۲', '2').replace('۳', '3').replace('۴', '4').replace('۵', '5').replace('۶', '6').replace('۷', '7').replace('۸', '8').replace('۹', '9');
var arr = String(newValue).split("");
if (arr.length === 0) return;
if (arr.length === 1 && (arr[0] == '-' || arr[0] === '.' )) return;
if (arr.length === 2 && newValue === '-.') return;
if (isNaN(newValue)) {
element.ngModel = oldValue;
}
});
}
};
Update:
Please consider that I need to do some processes to convert non English numbers and so on. I created a new fiddle here based on the the Angular_10's answer. Now, every thing is fine except the cursor position while typing Persian numbers. When I type A Persian Number, it is replaced with English equivalent number, but the cursor suddenly jumps to the end.
OK ! Looking at your requirement I've took liberty and wrote more customised directive.
Here is the fiddle for the same
Problem
The example from which you referred and made changes to the given directive is causing the issue.
Your $scope variable names are wrong in HTML/JS ($scope.myNnumber1 = 1;
$scope.myNnumber2 = 1; in JS and in HTML it was ng-model="myNumber1")
You are accessing element ng-model and trying to modify it through directive which is bad practice and also the root cause for directive to not to work.As you are not changing the ng-model value but in turn modifying HTML element value which angular will not recognise.
More over using $watch in directive is not always preferable for performance sake.
Solution
app.directive('isNumber', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attr, ctrl) {
function inputValue(val) {
if (val) {
var numeric = val.replace(/[^- 0-9]/g, '');
if (numeric !== val) {
ctrl.$setViewValue(numeric );
ctrl.$render();
}
return numeric;
}
return undefined;
}
ctrl.$parsers.push(inputValue);
}
};
});
When controller communication is required from directive we can pass Controller as 4 param in the link function.From that Ctrl param we can modify/view things from controller scope.
Using some basic regex expression to find out what is the entered input and set it in the controller scope object view value.
ctrl.$setViewValue(numeric); //to set the value in the respective ngModdel
ctrl.$render(); //to display the changed value
More about $setViewValue
I finally used the below directive. This directive converts persian number and do not let no numbers to be typed in the text box. Special thanks to Angular_10. I awarded 50 bounties to him for his help.
app.directive('fixPersianAndNoNumberInput', function ($filter) {
return {
require: 'ngModel',
restrict: 'EA',
link: function (scope, element, attr, controller) {
function inputValue(val) {
if (val) {
let numeric = parseInt(String(val).replace('۰', '0').replace('۱', '1').replace('۲', '2').replace('۳', '3').replace('۴', '4').replace('۵', '5').replace('۶', '6').replace('۷', '7').replace('۸', '8').replace('۹', '9').replace(' ', '000').replace(/[^- 0-9]/g, ''));
if (numeric !== val) {
controller.$setViewValue(numeric);
controller.$render();
let value;
let updateOn, debounce;
if (controller.$options) {
if (controller.$options.getOption) {
updateOn = controller.$options.getOption('updateOn');
debounce = controller.$options.getOption('debounce');
} else {
updateOn = controller.$options.updateOn;
debounce = controller.$options.debounce;
}
}
if (updateOn === 'blur' || debounce) {
value = controller.$viewValue;
for (let i = controller.$parsers.length - 1; i >= 0; i--) {
value = controller.$parsers[i](value);
}
} else {
value = controller.$$rawModelValue;
}
for (let j = controller.$formatters.length - 1; j >= 0; j--) {
value = controller.$formatters[j](value);
}
controller.$viewValue = value;
controller.$render();
}
return numeric;
}
return undefined;
}
controller.$parsers.push(inputValue);
controller.$formatters.push((value) => {
if ([undefined, null, ''].indexOf(value) === -1) {
return $filter('currency')(value, '', 0);
}
return value;
});
}
};
});
I have the following directive that I'm using for validation on a multi-select to allow for (dynamic) length validation of the number of selected items.
(function() {
'use strict';
angular
.module('myModule')
.directive('listLength', listLength);
listLength.$inject = ['$parse'];
function listLength($parse) {
var directive = {
require: 'ngModel',
restrict: 'A',
link: link
};
function link(scope, elem, attr, ngModel) {
var length = 0;
var exp = $parse(attr.listLength);
if (exp.constant) {
// Single value, no need to watch
length = exp(scope);
} else {
// We have an expression, need to watch for changes
scope.$watch(attr.listLength, function(newVal, oldVal) {
length = newVal;
});
}
//For DOM -> model validation
ngModel.$parsers.unshift(function(value) {
if (!angular.isUndefined(value) && value !== "") {
var valid = value.length === length;
ngModel.$setValidity('listLength', valid);
return valid ? value : undefined;
}
return value;
});
//For model -> DOM validation
ngModel.$formatters.unshift(function(value) {
if (!angular.isUndefined(value) && value !== "") {
var valid = value.length === length;
ngModel.$setValidity('listLength', valid);
}
return value;
});
}
return directive;
}
})();
In order to have it update properly, I need to trigger the validation to be run when the expression (attr.listLength) updates, but it's not obvious to me how to achieve this. I tried setting ngModel.$dirty to true but it still does not update.
Try change the watch parameter like that:
scope.$watch("listLength", function(newVal, oldVal) {
length = newVal;
});
Looks like the only way to do this in Angular 1.2 is to use ngModel.$setViewValue, which triggers all the validation functions. So if I do
scope.$watch(attr.listLength, function(newVal, oldVal) {
length = newVal;
ngModel.$setViewValue(ngModel.$viewValue); // Set view value to itself...
});
It then triggers the validation functions. In angularjs 1.3+ this can be done with ngModel.$validate()
My approach is this it is translating one time when i applied on element on angular template but when i applied multiple directive as template making multiple time api call so how to avoid it.
good solution please comment
var directive = {
restrict: 'AE',
scope: false,
multiElement: true,
link: link
};
$rootScope.cityName = cookieService.getCookie('lang');
$rootScope.translateThese = [];
$rootScope.translationData = '';
$rootScope.translationCounter = 0;
function link(scope, element, attr) {
scope.$evalAsync(function() {
$rootScope.translateThese.push(element[0]);
scope.$on('translateAPISuccess', function(e, data) {
angular.forEach($rootScope.translateThese, function(elem) {
var translatedString = $rootScope.translationData[elem.innerHTML.trim().toLowerCase()];
elem.innerHTML = translatedString;
// $compile(element.contents())(scope);
})
})
});
// This is a 0 second timeout to push the execution of this
// directive to the next digest cycle where all the dynamic values
// are present. I can also use scope.$evalSync for this purpose.
//
// #example
// $timeout(function() {
//
$timeout(function() { // We could also use scope.$watch here watching $viewContentLoaded // scope.$watch('$viewContentLoaded', function(e, u) {
$rootScope.translationCounter++;
var translateThese = [];
// if (scope.$last === true && scope.translationCounter === $rootScope.translateThese.length) { //we can use this but scope.$last isn't working
if ($rootScope.translationCounter === $rootScope.translateThese.length) {
$rootScope.translateThese.forEach(function(sentence) {
// trim.apply(sentence).split(" ").forEach(function(v) {
translateThese.push(sentence.innerHTML.replace(/[^a-zA-Z ]/g, ""));
// })
})
dataService.staticTranslation('guj', translateThese).then(function (response) {
$rootScope.translationData = response.response;
scope.$emit('translateAPISuccess', response.response);
// When using the translateAPI from horizontal
// $rootScope.translationData = JSON.parse(response.response).StaticLangTranslationApplicationResponse.StaticLangTranslationApplication.translations;
// scope.$emit('translateAPISuccess', JSON.parse(response.response).StaticLangTranslationApplicationResponse.StaticLangTranslationApplication.translations);
});
}
});
}
return directive;
}
You could look at using the Angular Translate service: https://github.com/angular-translate/angular-translate
It offers a filter, directive and service for translating your content.
I wanted to create directive in angular that would display error message if entered value is not in valid format.
What I finally came with is:
http://plnkr.co/edit/l2CWu8u6sMtSj3l0kdvd?p=preview
app.directive('kbDecimalValidation', function ($parse, $rootScope, $compile) {
return {
restrict: 'E',
scope: {
inputFieldRef: '=?',
kbModel: '=ngModel',
kbRequired: '#required',
inputName: '#'
},
template: '<span ng-form="kbDecimalValidationForm">' +
'<input ng-model="kbModel" ng-required="kbRequired" ' +
'size="6"' +
'ng-pattern="/^[0-9]+(\\.[0-9][0-9]?)?$/" ' +
'/>' +
'<div ng-show="!kbDecimalValidationForm[inputName].$valid && kbDecimalValidationForm[inputName].$error.required"' +
'style="color: red; font-weight: bold">Field is required</div>' +
'<div ng-show="!kbDecimalValidationForm[inputName].$valid && kbDecimalValidationForm[inputName].$error.pattern"' +
'style="color: red; font-weight: bold">Bad format format,<br />allowed: "0.00"' +
'</div>' +
'</span>',
replace: true,
priority: 50,
controller: function($scope){
$scope.$watch(
'kbDecimalValidationForm[inputName]',
function (value) {
$scope.inputFieldRef = value;
});
},
compile: function (tElement, tAttrs, transclude) {
if($.tempKbDecimalValidationGUID == undefined){
$.tempKbDecimalValidationGUID = 0;
}
var guidInputName = 'XXX' + ++$.tempKbDecimalValidationGUID + 'XXX';
$(tElement).find('input').attr('name', guidInputName); //it is here to force angular to assign value to: $scope.kbDecimalValidationForm[guidInputName]
//there is no expression in name, so angular won't add it to $$watchers
return {
pre: function preLink($scope, iElement, iAttrs, controller) {
//$(iElement).find('input').attr('name', iAttrs.inputName); //it doesn't work if there is expression in inputName,
// expression will be evaluated later (after linkFunction)
// and the name assigned here will be updated (re-parsed by angular watch)
},
post: function postLink($scope, iElement, iAttrs, controller) {
$scope.kbDecimalValidationForm[iAttrs.inputName] = $scope.kbDecimalValidationForm[guidInputName]; //rewrite value to make it available by parsed name
$(iElement).find('input').attr('name', iAttrs.inputName); //assign parsed name - GUID didn't contain expression, so it is not in $$watchers,
// so it won't be replaced by angular
}
}
}
};
});
but I'm sure it is not propper way to do it. I expirience a lot of problems with it. Can somebody tell me what is the propper way to achieve it?
PS: The problem I'm facing right now with the above directive is: when I use it in ng-repeat, and reorder repeated source the directive does not work correctly. I suspect the problem is with my "hacking coding" (the tempKbDecimalValidationGUID, and $scope.kbDecimalValidationForm variables)
For Angular 1.2.x, you will have to use the ngModel.$parsers and $formatters pipelines for validation. Angular 1.3 has the dedicated $validators and even $asyncValidators pipelines. So the outline of a validation solution for 1.2.x would be:
.directive("kbDecimalValidation", function() {
function parseDecimal(value) {
// implement the conversion from a string to number, e.g. (simpistic):
var val = parseFloat(value);
// return a number (for success), null (for empty input), or a string (describing the error on error)
}
function formatDecimal(value) {
// format a number to a string that will be displayed; the inverse of parseDecimal()
// throw error if something goes wrong
}
return {
restrict: "A",
require: "ngModel",
link: function(scope, elem, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
var val = parseDecimal(value);
if( typeof val === "string" ) {
// an error occured
ngModel.$setValidity("kbDecimal", false);
// return undefined!
}
else {
ngModel.$setValidity("kbDecimal", true);
return val;
}
});
ngModel.$formaters.push(function(value) {
if( value == null || typeof value === "number" ) {
ngModel.$setValidity("kbDecimal", true);
try {
return formatDecimal(value);
}
catch(e) {
ngModel.$setValidity("kbDecimal", false);
return "";
}
}
else {
ngModel.$setValidity("kbDecimal", false);
return "";
}
});
}
};
})
Many details will need work, but hopefully you get the idea. The parseDecimal()/formatDecimal() functions could even go to a dedicated Angular service, if they become too complex, or need to be reusable.
About the display of error messages
A quick and dirty way is to use DOM manipulation through the elem argument of link(). E.g.:
link: function(scope, elem, attrs, ngModel) {
...
scope.$watch(
function() { return ngModel.$error.kbDecimal; },
function(newval) {
var container = elem.parent();
// append or remove the message as necessary
...
}
);
}
Another way, less quick but more componentized is to make 2 more directives. One will be placed on the <span ng-form> element (the container), another will display the messages. The HTML would be like:
<span ng-form="..." validation-container>
<input ... kb-decimal-validation />
<validation-messages></validation-messages>
</span>
Both kbDecimalValidation and validationMessages will require the validationContainer; the controller of the validationContainer will have a method, called by the kbDecimalValidation, to get notified about the $error object. It will also expose a copy of the $error object. The validationMessages will $watch that object and display or hide the appropriate messages.
I have two directives:
// Generated by CoffeeScript 1.6.3
app.directive("focusMe", function() {
return {
scope: {
focus: "=focusMe"
},
link: function(scope, element) {
return scope.$watch("focus", function(value) {
if (value === true) {
element[0].focus();
return scope.focus = false;
}
});
}
};
});
and:
// Generated by CoffeeScript 1.6.3
app.directive("cleanMe", function() {
return {
scope: {
clean: "=cleanMe"
},
link: function(scope, element) {
return scope.$watch("clean", function(value) {
if (value === true) {
element[0].value = "";
return scope.clean = false;
}
});
}
};
});
and this input (angularUI):
<input type="text" ng-model="addUserSelected" typeahead="emp.value for emp in allUsers | filter:$viewValue | limitTo:5" typeahead-editable="false" typeahead-on-select="addLine($item.id)" focus-me="usersFocusInput" clean-me="usersCleanInput">
I get this error:
Error: [$compile:multidir] http://errors.angularjs.org/1.2.3/$compile/multidir?p0=cleanMe&p1=focusMe&p…2%20focus-me%3D%22usersFocusInput%22%20clean-me%3D%22usersCleanInput%22%3E
what do I do wrong?
If I remove the clean-me property from the html it works.
Thanks
There is no real need for isolated scopes here. Use a "normal" directive scope and the directive will just inherit from the parent, like this:
// Generated by CoffeeScript 1.6.3
app.directive("focusMe", function() {
return {
link: function(scope, element, attrs) {
return scope.$watch(attrs.focusMe, function(focusMe) {
if (focusMe.value === true) {
element[0].focus();
return scope[attrs.focusMe].value = false;
}
});
}
};
});
// Generated by CoffeeScript 1.6.3
app.directive("cleanMe", function() {
return {
link: function(scope, element, attrs) {
return scope.$watch(attrs.cleanMe, function(cleanMe) {
if (cleanMe.value === true) {
element[0].value = "";
return scope[attrs.cleanMe].value = false;
}
});
}
};
});
Ignore this part if you already know how inheritance works, just adding for completeness:
Note that I am using the [attrs.focusMe].value, not just [attrs.focusMe]. The reason is how inheritance works in javascript. These directives are child scopes, so if you try to do scope[attrs.focusMe] = false you will set a local variable in the scope of the directive, i.e. you will not affect the parent scope (the controller where it is used). However, if you make the focusMe model (whatever it is) an object in the parent scope and then change a value on that object, then it will not set a local variable, it will instead update the parent. So:
scope[attrs.focusMe] = false; // Sets a local variable, does not affect the parent
scope[attrs.focusMe].value = false; // Updates focusMe on the parent
Here is a good answer about inheritance if you want an in depth guide: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
You have two directives which require isolated scope on the same element which is not allowed.
The reason this is not allowed is because if you have some template {{...}} code inside the directive, then it will be unclear which scope it should take its values from.
Consider instead of isolating scope, using attribute.$observe to watch the cleanMe and focusMe properties and acting on those.
app.directive("focusMe", function() {
return {
link: function(scope, element, attributes) {
attributes.$observe("focusMe", function(value) {
if (value === true) {
element[0].focus();
scope.focus = false;
}
});
}
};
});
and:
app.directive("cleanMe", function() {
return {
link: function(scope, element, attributes) {
attributes.$observe("cleanMe", function(value) {
if (value === true) {
element[0].value = "";
return scope.clean = false;
}
});
}
};
});
I found the solution finally :)
// Generated by CoffeeScript 1.6.3
app.directive("focusMe", function() {
return {
link: function(scope, element, attributes) {
return scope.$watch(attributes.focusMe, function(value) {
if (scope[value] === true) {
element[0].focus();
return scope[attributes.focusMe] = false;
}
});
}
};
});
// Generated by CoffeeScript 1.6.3
app.directive("cleanMe", function() {
return {
link: function(scope, element, attributes) {
return scope.$watch(attributes.cleanMe, function(value) {
if (value === true) {
element[0].value = "";
return scope[attributes.cleanMe] = false;
}
});
}
};
});
In the html usersFocusInput and usersCleanInput are parameters in the scope that is wht I use scope[attributes.focusMe] to get this parameter and change him to false.
<input type="text" ng-model="addUserSelected" typeahead="emp.value for emp in allUsers | filter:$viewValue | limitTo:5" typeahead-editable="false" typeahead-on-select="addLine($item.id)" focus-me="usersFocusInput" clean-me="usersCleanInput">