I've created my own angular directive which will add and remove a class based off of a condition passed into it like below:
app.directive("alerter", function ($interval, $compile) {
return {
restrict: "A",
link: function ($scope, elem, attrs) {
var loginExpired = attrs.alerter;
var addClassInterval = undefined;
var timer = 3000;
var className = "alert";
addClassInterval = $interval(addClass, timer);
function addClass() {
if (elem.hasClass(className)) {
elem.removeClass(className);
} else if (loginExpired) {
elem.addClass(className);
}
$compile(elem)($scope);
}
}
}
});
This can then be used on an element as an attribute like below:
<div alerter="{{model.loginTime > model.expiryTime}}"> ... </div>
However, even when alerter evaluates to false, it still adds the class, but I'm not sure why? Also the $interval seems to not be working as intended, here is a Plunker I've created to demonstrate:
http://plnkr.co/edit/YKb6YARLaBnsevuoxv3G?p=preview
Thanks!
Edit
When I remove $compile(elem)($scope); it fixes the issue I was having with $interval however, I know that one of the conditions passed in is always false but it still applies the class to it
If you would like to parse the values via attr you will need to parse the value of attr.alerter as it is coming in a string (not a boolean)
you can use this code to do this:
var loginExpired = $parse(attrs.alerter)();
Note: you will need to inject in the $parse service
Another (less optimal solution) is to just do a string comparison of attrs.alerter === 'true'
var loginExpired = attrs.alerter;
this will always be true because you are getting the attribute value which is "true" or "false" both not empty strings hence always truthy if you want to use attrs and not an isolated scope then you'll have to check something like
var loginExpired = /true/.test(attrs.alerter);
Related
I have a custom AngularJS component which might be used on a single web page over 200 times. The page ends up implementing over 4000 watchers -- which is more than AngularJS's prefered maximum amount of watchers -- and makes the page really slow.
The actual problem is that there is a lot of unneeded watchers left from some ng-if and other AngularJS expressions inside the component template which no longer where going to change their values.
For normal ng-if's the fix was easy:
<div ng-if="::$ctrl.isInitialized()">Ready!</div>
...where $ctrl.isInitialized() would either return a true (when the component was initialized) or undefined (until it was).
Returning undefined here will make AngularJS keep the watcher active until it returns something else, in this case the value true, and then will add the div in the DOM.
There is no ng-not="expression" like there is ng-hide. This works well with ng-hide, except of course the div is still in the DOM after the controller has been initialized, which is not the perfect solution.
But how can you implement it so, that the <div> will be in the DOM until the controller has been initialized and will be removed after?
Although there is no ng-not directive, it was easy to implement from AngularJS source code:
var ngNotDirective = ['$animate', '$compile', function($animate, $compile) {
function getBlockNodes(nodes) {
// TODO(perf): update `nodes` instead of creating a new object?
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes;
for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
blockNodes = jqLite(slice.call(nodes, 0, i));
}
blockNodes.push(node);
}
}
return blockNodes || nodes;
}
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, previousElements;
$scope.$watch($attr.ngNot, function ngNotWatchAction(value) {
if (!value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = $compile.$$createComment('end ngNot', $attr.ngNot);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).done(function(response) {
if (response !== false) previousElements = null;
});
block = null;
}
}
});
}
};
}];
This is the same implementation as ng-if except it has reverted if (!value) check.
It can be used like this:
<div ng-not="::$ctrl.isInitialized() ? true : undefined">Loading...</div>
It is easy to verify that there is no useless watchers by adding a console.log() in $ctrl.isInitialized() -- this function will be called just few times until it returns true and the watcher is removed -- as well as the div, and anything inside it.
kind of quick patch: angular allows ternary operator in expressions after v1.1.5 I guess.
So you can make something like:
<div ng-if="::$ctrl.isInitialized() === undefined? undefined: !$ctrl.isInitialized()">
As far as I can see undefined does not have special meaning in angular expression - it's treated as another (not defined yet) variable in $scope. So I had to put it there explicitly:
$scope = undefined;
Alternative option is writing short helper:
function isDefined(val) {
return angular.isDefined(val) || undefined;
}
To use it later as
ng-if="::isDefined($ctrl.isInitialized()) && !$ctrl.isInitialized()"
But since you say there are too many places for doing that - for sure making own component as you coded above looks better
I have a directive which has this piece of code:
return {
restrict: "E",
scope: {
moduleProgress: "=",
modules: "=",
parentScroll: "="
},
templateUrl: 'app/program/module.list.template.html',
link: function(scope,el,attrs,ctrl,tf) {
$templateRequest("app/program/chapter.list.template.html").then(function(template) {
var template = angular.element(template);
var elements = [];
console.log(scope);
attrs.$observe("modules", function(val) {
scope.modules.forEach(function(module) {
module.open = false;
});
});
For some reason, the first time I navigate to the view that contains this directive everything works fine, but from the second time onwards I always get a Cannot call method 'forEach' of undefined error. I read through some similar questions on SO which mentioned interpolated attributes not being instantly available on the link function, so they reccomended the use of $observe. I even added a console.log(scope) and the modules property shows up on the object. Any idea of what might be causing this?
It should be rather $scope.$watch rather that attrs.$observe as $observe required # in isolated scope of directive & on html it would be modules="{{module}}", For big object DOM will get messed up so I'd prefer you to use $watch which make sense
Code
scope.$watch("modules", function(newVal) {
if(newVal){
scope.modules.forEach(function(module) {
module.open = false;
});
}
});
I'm trying to create a custom compile function, to make it easier to dynamically add HTML to a page.
The argument htmlStr is the incoming HTML to compile. The argument value is a variable that can be added to the scope. The argument compiledHTMLFunc is a function that will be executed with the compiled object. Here's my code:
function compileHTML (htmlStr, value, compiledHTMLFunc)
{
var $injector = angular.injector (["ng", "angularApp"]);
$injector.invoke (function ($rootScope, $compile)
{
$rootScope.value = value;
var obj = angular.element (htmlStr);
var obj2 = $compile (obj)($rootScope);
if (compiledHTMLFunc != null)
compiledHTMLFunc (obj2);
});
}
Here's how I use the function:
compileHTML ("<button class = \"btn btn-primary\">{{ value }}</button>", "Ok", function (element)
{
$(document.body).append (element);
});
Whenever I try to compile the following HTML, the inline {{ value }} doesn't get compiled. Even if I simply change it to {{ 1+1 }}. Why is this?
Update: I dunno why I didn't create a fiddle earlier, here's an example: http://jsbin.com/vuxazuzu/1/edit
The problem appears to be pretty simple. Since you invoke compiler from outside of angular digest cycle you have to invoke it manually to boost the process, for example by wrapping compiledHTMLFunc into $timeout service call:
function compileHTML (htmlStr, scope, compiledHTMLFunc) {
var $injector = angular.injector(["ng", "angularApp"]);
$injector.invoke(function($rootScope, $compile, $timeout) {
$rootScope = angular.extend($rootScope, scope);
var obj = $compile(htmlStr)($rootScope);
if (compiledHTMLFunc != null) {
$timeout(function() {
compiledHTMLFunc(obj);
});
}
});
}
compileHTML('<button class="btn btn-primary">{{value}}</button>', {value: 'Ok'}, function(element) {
angular.element(document.body).append(element);
});
I also improved your code a little. Note how now compileHTML accepts an object instead of single value. It adds more flexibility, so now you can use multiple values in template.
Demo: http://plnkr.co/edit/IAPhQ9i9aVVBwV9MuAIE?p=preview
And here is your updated demo: http://jsbin.com/vuxazuzu/2/edit
I'm having trouble with a bi-directional binding in an ng-repeat. I would expect for the below $watch to be triggered when you select a color from the list.
$scope.$watch('favoriteColors', function (newValue) {
console.log('example-favoriteColors', newValue);
});
I would expect for Orange to appear in $scope.favoriteColors when checked.
Example: http://plnkr.co/edit/k5SEQw4XFnxriD2I8ZG7?p=preview
directive('checkBox', function () {
return {
replace: true,
restrict: 'E',
//require: '^ngModel',
scope: {
'externalValue': '=ngModel',
'value': '&'
},
template: function (el, attrs) {
var html =
'<div class="ngCheckBox">'+
'<span ng-class="{checked: isChecked}">' +
'<input type="checkbox" ng-model="isChecked"/>'+
'</span>'+
'</div>';
return html;
},
controller: ['$scope', '$timeout', function ($scope, $timeout) {
var initialized = false;
console.log($scope.value());
if (angular.isArray($scope.externalValue)) {
$scope.isChecked = $scope.externalValue.indexOf($scope.value()) > 0;
} else {
$scope.isChecked = !!$scope.externalValue;
}
$scope.$watch('isChecked', function (newValue) {
if (angular.isDefined(newValue)) {
//add or remove items if this is an array
if (angular.isArray($scope.externalValue)) {
var index = $scope.externalValue.indexOf($scope.value());
if(index > -1) {
$scope.externalValue.splice(index, 1);
} else if (initialized) {
$scope.externalValue.push($scope.value());
}
} else {
//simple boolean value
$scope.externalValue = newValue;
}
if (initialized)
console.log($scope.externalValue);
}
});
$timeout(function () {
initialized = true;
});
}],
link: function (scope, el, attrs) {
}
};
});
Please check out this plunk: http://plnkr.co/edit/pbHz4ohBPi7iYq6uJI8X?p=preview
There were lots of changes. Some of them are:
The template needs not be a function, since it is static.
The initialized (and consequently the $timeout) is not needed.
I implemented my own indexOf function; there is a chance the objects are not the same in == sense, but equals in the x.name === y.name sense; (I have some doubts about this though)
The add or remove items if this is an array part was wrong; you need to update the array based on the value of isChecked, not based on whether the item already exists in the array (indexOf).
Initialize favoriteColors as an array, not as a single object, to be consistent, i.e. $scope.favoriteColors = [$scope.colors[1]];
(minor) Added a little more descriptive log when favoriteColors change.
Use $watch("favoriteColors", function() {...}, true) to watch for changes inside the array (not the true last argument).
I think it's because you need to be referencing a property on an object instead of the flat array. When you pass a primitive data structure like an array, it gets passed by reference and thus the updates aren't passed along properly. (Post by Mark Rajcok.)
I went ahead and showed this by hacking your plunkr a little bit. I changed $scope.favoriteColors = $scope.colors[1]; to $scope.favoriteColors = {value:$scope.colors[1]}; and changed <check-box ng-model="favoriteColors" value="color"> to <check-box ng-model="favoriteColors.value" value="color">.
Plunkr
You can see in the plunkr that when you hit the checkboxes the console.log statements now go off under the $watch function.
I see that you're using angular-form-ui's checkbox directive.
Use $watchCollection (link to documentation) instead of $watch for watching arrays for changes
Initialize $scope.favoriteColors as an array containing the values that should be checked
I've reverted your changes to angular-form-ui.js as those changes broke the directive. They are now exactly as the code appears in the latest commit on Github (checkbox.js). Only one thing has changed, the initialization of the angular-form-ui module by adding [] as the second argument to that first line.
Here is the updated plunker: http://plnkr.co/edit/mlUt46?p=preview
I'm trying to get an evaluated attribute from my custom directive, but I can't find the right way of doing it.
I've created this jsFiddle to elaborate.
<div ng-controller="MyCtrl">
<input my-directive value="123">
<input my-directive value="{{1+1}}">
</div>
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
element.val("value = "+attr.value);
}
});
What am I missing?
Notice: I do update this answer as I find better solutions. I also keep the old answers for future reference as long as they remain related. Latest and best answer comes first.
Better answer:
Directives in angularjs are very powerful, but it takes time to comprehend which processes lie behind them.
While creating directives, angularjs allows you to create an isolated scope with some bindings to the parent scope. These bindings are specified by the attribute you attach the element in DOM and how you define scope property in the directive definition object.
There are 3 types of binding options which you can define in scope and you write those as prefixes related attribute.
angular.module("myApp", []).directive("myDirective", function () {
return {
restrict: "A",
scope: {
text: "#myText",
twoWayBind: "=myTwoWayBind",
oneWayBind: "&myOneWayBind"
}
};
}).controller("myController", function ($scope) {
$scope.foo = {name: "Umur"};
$scope.bar = "qwe";
});
HTML
<div ng-controller="myController">
<div my-directive my-text="hello {{ bar }}" my-two-way-bind="foo" my-one-way-bind="bar">
</div>
</div>
In that case, in the scope of directive (whether it's in linking function or controller), we can access these properties like this:
/* Directive scope */
in: $scope.text
out: "hello qwe"
// this would automatically update the changes of value in digest
// this is always string as dom attributes values are always strings
in: $scope.twoWayBind
out: {name:"Umur"}
// this would automatically update the changes of value in digest
// changes in this will be reflected in parent scope
// in directive's scope
in: $scope.twoWayBind.name = "John"
//in parent scope
in: $scope.foo.name
out: "John"
in: $scope.oneWayBind() // notice the function call, this binding is read only
out: "qwe"
// any changes here will not reflect in parent, as this only a getter .
"Still OK" Answer:
Since this answer got accepted, but has some issues, I'm going to update it to a better one. Apparently, $parse is a service which does not lie in properties of the current scope, which means it only takes angular expressions and cannot reach scope.
{{,}} expressions are compiled while angularjs initiating which means when we try to access them in our directives postlink method, they are already compiled. ({{1+1}} is 2 in directive already).
This is how you would want to use:
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function ($parse) {
return function (scope, element, attr) {
element.val("value=" + $parse(attr.myDirective)(scope));
};
});
function MyCtrl($scope) {
$scope.aaa = 3432;
}
.
<div ng-controller="MyCtrl">
<input my-directive="123">
<input my-directive="1+1">
<input my-directive="'1+1'">
<input my-directive="aaa">
</div>
One thing you should notice here is that, if you want set the value string, you should wrap it in quotes. (See 3rd input)
Here is the fiddle to play with: http://jsfiddle.net/neuTA/6/
Old Answer:
I'm not removing this for folks who can be misled like me, note that using $eval is perfectly fine the correct way to do it, but $parse has a different behavior, you probably won't need this to use in most of the cases.
The way to do it is, once again, using scope.$eval. Not only it compiles the angular expression, it has also access to the current scope's properties.
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
element.val("value = "+ scope.$eval(attr.value));
}
});
function MyCtrl($scope) {
}
What you are missing was $eval.
http://docs.angularjs.org/api/ng.$rootScope.Scope#$eval
Executes the expression on the current scope returning the result. Any exceptions in the expression are propagated (uncaught). This is useful when evaluating angular expressions.
For an attribute value that needs to be interpolated in a directive that is not using an isolated scope, e.g.,
<input my-directive value="{{1+1}}">
use Attributes' method $observe:
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
attr.$observe('value', function(actual_value) {
element.val("value = "+ actual_value);
})
}
});
From the directive page,
observing interpolated attributes: Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
If the attribute value is just a constant, e.g.,
<input my-directive value="123">
you can use $eval if the value is a number or boolean, and you want the correct type:
return function (scope, element, attr) {
var number = scope.$eval(attr.value);
console.log(number, number + 1);
});
If the attribute value is a string constant, or you want the value to be string type in your directive, you can access it directly:
return function (scope, element, attr) {
var str = attr.value;
console.log(str, str + " more");
});
In your case, however, since you want to support interpolated values and constants, use $observe.
The other answers here are very much correct, and valuable. But sometimes you just want simple: to get a plain old parsed value at directive instantiation, without needing updates, and without messing with isolate scope. For instance, it can be handy to provide a declarative payload into your directive as an array or hash-object in the form:
my-directive-name="['string1', 'string2']"
In that case, you can cut to the chase and just use a nice basic angular.$eval(attr.attrName).
element.val("value = "+angular.$eval(attr.value));
Working Fiddle.
For the same solution I was looking for Angularjs directive with ng-Model.
Here is the code that resolve the problem.
myApp.directive('zipcodeformatter', function () {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (v) {
if (v) {
console.log('value changed, new value is: ' + v + ' ' + v.length);
if (v.length > 5) {
var newzip = v.replace("-", '');
var str = newzip.substring(0, 5) + '-' + newzip.substring(5, newzip.length);
element.val(str);
} else {
element.val(v);
}
}
});
}
};
});
HTML DOM
<input maxlength="10" zipcodeformatter onkeypress="return isNumberKey(event)" placeholder="Zipcode" type="text" ng-readonly="!checked" name="zipcode" id="postal_code" class="form-control input-sm" ng-model="patient.shippingZipcode" required ng-required="true">
My Result is:
92108-2223
var myApp = angular.module('myApp',[]);
myApp .directive('myDirective', function ($timeout) {
return function (scope, element, attr) {
$timeout(function(){
element.val("value = "+attr.value);
});
}
});
function MyCtrl($scope) {
}
Use $timeout because directive call after dom load so your changes doesn`'t apply