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
Related
I'm having trouble with Angular's one time binding.
Let's say I want to use ngIf with one time binding, something like this:
<div ng-if="::showImage">
<img src="somesource" img-preloader/>
</div>
In this case angular creates a watch for the expression inside the if.
Once it has been resolved to a none-undefined value the watch is removed.
If it was resolved to a truthly value only then the descendant html tree is added to the DOM and subsequently rendered.
Now this is all great but I'd really like to avoid the initial watch, just parse the expression, and if its undefined - only then set up a watch. The reason being is fairly complex in my scenario but basically I have some mechanism that temporarily disables unneeded watches...
So I was looking for alternatives to the built-in angular's one time binding and came across angular-once.
Angular-once implements one-time-binding in a different way, it sets up a temp watch only if the expression is parsed to undefined, so if it resolves in the initial attempt no watch is created. Sounds great.
So I could do something like this:
<div once-if="showImage">
<img src="somesource" img-preloader/>
</div>
But, here's the problem - apparently the descendant HTML tree is first rendered by default and then if once-if resolves to false the descendant nodes are removed from the DOM.
Here's the snippet that does it:
{
name: 'onceIf',
priority: 600,
binding: function (element, value) {
if (!value) {
element.remove();
}
}
},
This is bad behavior for me, as creating the descendant tree is a no-go and results in other problems, for instance - in the above example the img will be downloaded.
So I'm looking for a way to do one-time-binding in directives like ngIf without setting up a watch if the expression parses successfully and without pre-rendering the descendant tree.
I was trying to avoid this, but for now I ended up implementing custom directives based on Angular's standard ones but with the necessary added functionality.
ngIf derived directive:
app.directive('watchlessIf', ['$animate', '$compile', '$parse', function($animate, $compile, $parse) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
function valueChangedAction(value) {
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = $compile.$$createComment('end watchlessIf', $attr.watchlessIf);
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).then(function() {
previousElements = null;
});
block = null;
}
}
}
var block, childScope, previousElements;
if ($attr.watchlessIf.startsWith("::")) {
var parsedExpression = $parse($attr.watchlessIf)($scope);
if (parsedExpression != null) {
valueChangedAction(parsedExpression);
return;
}
}
$scope.$watch($attr.watchlessIf, valueChangedAction);
}
};
}]);
ngBind derived directive:
app.directive('watchlessBind', ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'AC',
compile: function watchlessBindCompile(templateElement) {
$compile.$$addBindingClass(templateElement);
return function watchlessBindLink(scope, element, attr) {
function valueChangedAction(value) {
element.textContent = (typeof value == "undefined") ? '' : value;
}
$compile.$$addBindingInfo(element, attr.watchlessBind);
element = element[0];
if (attr.watchlessBind.startsWith("::")) {
var parsedExpression = $parse(attr.watchlessBind)(scope);
if (parsedExpression != null) {
valueChangedAction(parsedExpression);
return;
}
}
scope.$watch(attr.watchlessBind, valueChangedAction);
};
}
};
}]);
Notes:
Unfortunately with such approach I'll have to implement similar directives for other Angular directives as well where I'd like to support potentially watch-less one time binding.
I'm using private angular stuff inside the directives, like the $$tlb option, although i really shouldn't...
(I'm sorry if my question title isn't very good, I couldn't think of a better one. Feel free to suggest better options.)
I'm trying to create a reusable "property grid" in Angular, where one can bind an object to the grid, but in such a way that presentation of the object can be customized somewhat.
This is what the directive template looks like (the form-element isn't important to my question, so I'll leave it out):
<div ng-repeat="prop in propertyData({object: propertyObject})">
<div ng-switch on="prop.type">
<div ng-switch-when="text">
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
model="propertyObject[prop.key]"
focus-events-enabled="false">
</form-element>
</div>
</div>
</div>
and, the directive code:
angular.module("app.shared").directive('propertyGrid', ['$log', function($log) {
return {
restrict: 'E',
scope: {
propertyObject: '=',
propertyData: '&'
}
templateUrl: 'views/propertyGrid.html'
};
}]);
Here's an example usage:
<property-grid edit-mode="true"
property-object="selectedSite"
property-data="getSitePropertyData(object)">
</property-grid>
And the getSitePropertyData() function that goes with it:
var lastSite;
var lastSitePropertyData;
$scope.getSitePropertyData = function (site) {
if (site == undefined) return null;
if (site == lastSite)
return lastSitePropertyData;
lastSite = site;
lastSitePropertyData = [
{key:"SiteName", value:site.SiteName, editable: true, type:"text"},
//{key:"Company.CompanyName", value:site.Company.CompanyName, editable: false, type:"text"},
{key:"Address1", value:site.Address1, editable: true, type:"text"},
{key:"Address2", value:site.Address2, editable: true, type:"text"},
{key:"PostalCode", value:site.PostalCode, editable: true, type:"text"},
{key:"City", value:site.City, editable: true, type:"text"},
{key:"Country", value:site.Country, editable: true, type:"text"},
{key:"ContactName", value:site.ContactName, editable: true, type:"text"},
{key: "ContactEmail", value: site.ContactEmail, editable: true, type:"email"},
{key: "ContactPhone", value: site.ContactPhone, editable: true, type:"text"},
{key: "Info", value: site.Info, editable: true, type:"text"}
];
return lastSitePropertyData;
};
The reason I'm going through such a "property data" function and not just binding directly to properties on the object is that I need to control the order of the properties, as well as whether they should even be shown to the user at all, and also what kind of property it is (text, email, number, date, etc.) for the sake of presentation.
At first, as you can tell from the value property remnant in the getSitePropertyData() function, I first tried providing the values directly from this function, but that wouldn't bind to the object, so changes either in the object or form the property grid didn't sync back and forth. Next up, then, was using the key idea, which lets me do this: propertyObject[prop.key]—which works great for direct properties, but as you can see, I had to comment out the "Company" field, because it's a property of a property, and propertyObject["a.b"] doesn't work.
I'm struggling to figure out what to do here. I need the bindings to work, and I need to be able to use arbitrarily deep properties in my bindings. I know this kind of thing is theoretically possible; I've seen it done for instance in UI Grid, but such projects have so much code that I would probably spend days finding out how they do it.
Am I getting close, or am I going about this all wrong?
You want to run an arbitrary Angular expression on an object. That is exactly the purpose of $parse (ref). This service can well... parse an Angular expression and return a getter and setter. The following example is an oversimplified implementation of your formElement directive, demonstrating the use of $parse:
app.directive('formElement', ['$parse', function($parse) {
return {
restrict: 'E',
scope: {
label: '#',
name: '#',
rootObj: '=',
path: '#'
},
template:
'<label>{{ label }}</label>' +
'<input type="text" ng-model="data.model" />',
link: function(scope) {
var getModel = $parse(scope.path);
var setModel = getModel.assign;
scope.data = {};
Object.defineProperty(scope.data, 'model', {
get: function() {
return getModel(scope.rootObj);
},
set: function(value) {
setModel(scope.rootObj, value);
}
});
}
};
}]);
I have altered slightly the way the directive is used, hopefully without changing the semantics:
<form-element type="text"
label-translation-key="{{prop.key}}"
label="{{prop.key}}"
name="{{prop.key}}"
root-obj="propertyObject"
path="{{prop.key}}"
focus-events-enabled="false">
Where root-obj is the top of the model and path is the expression to reach the actual data.
As you can see, $parse creates the getter and setter function for the given expression, for any root object. In the model.data property, you apply the accessor functions created by $parse to the root object. The entire Object.defineProperty construct could be replaced by watches, but that would only add overhead to the digest cycle.
Here is a working fiddle: https://jsfiddle.net/zb6cfk6y/
By the way, another (more terse and idiomatic) way to write the get/set would be:
Object.defineProperty(scope.data, 'model', {
get: getModel.bind(null, scope.rootObj),
set: setModel.bind(null, scope.rootObj)
});
If you are using lodash you can use the _.get function to achieve this.
You can store _.get in the controller of your property-grid and then use
model="get(propertyObject,prop.key)"
in your template. If you need this functionality in multiple places in your application (and not just in property-grid) you could write a filter for this.
The problem with this is that you can't bind your model this way and thus you can't edit the values. You can use the _.set function and an object with a getter and a setter to make this work.
vm.modelize = function(obj, path) {
return {
get value(){return _.get(obj, path)},
set value(v){_.set(obj, path,v)}
};
}
You can then use the function in the template:
<div ng-repeat="prop in propertyData({object: propertyObject})">
<input type="text"
ng-model="ctrl.modelize(propertyObject,prop.key).value"
ng-model-options="{ getterSetter: true }"></input>
</div>
For a reduced example see this Plunker.
If you don't use lodash you can use this simplified version of the _.get function that I extracted from lodash.
function getPath(object, path) {
path = path.split('.')
var index = 0
var length = path.length;
while (object != null && index < length) {
object = object[path[index++]];
}
return (index && index == length) ? object : undefined;
}
This function makes sure that you won't get any Cannot read property 'foo' of undefined errors. This is useful especially if you have long chains of properties where there might be an undefined value. If you want to be able to use more advanced paths (like foo.bar[0]) you have to use the full _.get function from lodash.
And here is a simplified version of _.set also extracted form lodash:
function setPath(object, path, value) {
path = path.split(".")
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = path[index]
if (typeof nested === 'object') {
var newValue = value;
if (index != lastIndex) {
var objValue = nested[key];
newValue = objValue == null ?
((typeof path[index + 1] === 'number') ? [] : {}) :
objValue;
}
if (!(hasOwnProperty.call(nested, key) && (nested[key] === value)) ||
(value === undefined && !(key in nested))) {
nested[key] = newValue;
}
}
nested = nested[key];
}
return object;
}
Keep in mind that these extracted functions ignore some edge cases that lodash handles. But they should work in most cases.
When you creating the lastSitePropertyData you can create the object in this way to not hardcode it
function createObject (){
for(var key in site){
lastSitePropertyData.push({key:key, value:site[key], editable: true, type:"text"});
} }
And later use function to get data something like this
function getKey(prop){
if(typeof prop.value === 'object'){
return prop.value.key; //can run loop create a go deep reccursive method - thats upto u
}
else return prop.key;
}
function getValue(prop){
if(typeof prop === 'object'){
return prop.value.value; //have tp run loop get value from deep reccursive method - thats upto u
}
else return prop.value;
}
That way can be use in html {{getKey(prop)}} and {{getValue(prop}}
For working demo please have look this link - https://jsfiddle.net/718px9c2/4/
Note: Its just idea for accessing json data in better way, I am not using angular in demo.
Another idea is to do smth like this.
If you wont to avoid making object.proto dirty (this is always good idea) just move this functionality into the other module.
(function () {
'use strict';
if (Object.hasOwnProperty('getDeep')) {
console.error('object prototype already has prop function');
return false;
}
function getDeep(propPath) {
if (!propPath || typeof propPath === 'function') {
return this;
}
var props = propPath.split('.');
var result = this;
props.forEach(function queryProp(propName) {
result = result[propName];
});
return result;
}
Object.defineProperty(Object.prototype, 'getDeep', {
value: getDeep,
writable: true,
configurable: true,
enumerable: false
});
}());
I have something similar used to show data in grids, those grids may show the same objects yet no the same columns. however I don't handle that in one go.
I have a type service where I declare my types and some default configuration
I have a grid service which generates the grid definition options according to what I specified.
In the controller I instantiate the grid using the grid service, specifying the ordering of the columns and some specific configurations, which override the default ones. The grid service itself generate appropriate configuration for filtering, ordering using the type definition of the fields.
I am a strong advocate of best practices, especially when it comes to angular but I can't manage to use the brand new $validators pipeline feature as it should be.
The case is quite simple: 1 input enhanced by a directive using $parser, $formatter and some $validators:
<input name="number" type="text" ng-model="number" number>
Here is the (simplified) directive:
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
$controller.$parsers.push(function (value) {
return isFinite(value)? parseInt(value): undefined;
});
$controller.$formatters.push(function (value) {
return value.toString() || '';
});
$controller.$validators.minNumber = function(value) {
return value && value >= 1;
};
$controller.$validators.maxNumber = function(value) {
return value && value <= 10;
};
}
};
}]);
I made a little plunk to play with :)
The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.
NB: Before angular 1.3, I was able to do this using ngModelController API directly in $parser/$formatter. I can still do that with 1.3, but that would not be "angular-way".
NB2: In my app I am not really using numbers, but quantities.The problem remains the same.
It looks like you want some parsing to happen after validation, setting the model to the last valid value rather than one derived from the view. However, I think the 1.3 pipeline works the other way around: parsing happens before validation.
So my answer is to just do it as you would do it in 1.2: using $parsers to set the validation keys and to transform the user's input back to the most recent valid value.
The following directive does this, with an array of validators specified within the directive that are run in order. If any of the previous validators fails, then the later ones don't run: it assumes one validation error can happen at a time.
Most relevant to your question, is that it maintains the last valid value in the model, and only overwrites if there are no validation errors occur.
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
var lastValid;
$controller.$parsers.push(function(value) {
value = parseInt(value);
lastValid = $controller.$modelValue;
var skip = false;
validators.forEach(function(validator) {
var isValid = skip || validator.validatorFn(value);
$controller.$setValidity(validator.key, isValid);
skip = skip || !isValid;
});
if ($controller.$valid) {
lastValid = value;
}
return lastValid;
});
$controller.$formatters.push(function(value) {
return value.toString() || '';
});
var validators = [{
key: 'isNumber',
validatorFn: function(value) {
return isFinite(value);
}
}, {
key: 'minNumber',
validatorFn: function(value) {
return value >= 1;
}
}, {
key: 'maxNumber',
validatorFn: function(value) {
return value <= 10;
}
}];
}
};
}]);
This can be seen working at http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview
I think you are over-thinking this in terms of Angular-way vs. not Angular-way. Before 1.3 using $parsers pipeline was the Angular-way and now it's not?
Well, the Angular-way is also that ng-model sets the model to undefined (by default) for invalid values. Follow that Angular-way direction and define another variable to store the "lastValid" value:
<input ng-model="foo" ng-maxlength="3"
ng-change="lastValidFoo = foo !== undefined ? foo : lastValidFoo"
ng-init="foo = lastValidFoo">
No need for a special directive and it works across the board in a way that doesn't try to circumvent what Angular is doing natively - i.e. the Angular-way. :)
As of Angular 1.3 you can use the ngModelOptions directive to have greater control as to when your model value updates. Take a look at this updated Plunker to show you how to achieve the functionality you are looking for: http://plnkr.co/edit/DoWbvlFMEtqF9gvJCjPF?p=preview
Basically you define the model as a getterSetter and only return the new value if it is valid:
$scope.validNumber = function(value) {
return angular.isDefined(value) ? ($scope.number = value) : $scope.number;
}
$scope.modelOptions = {
getterSetter: true,
allowInvalid: false
};
Then to use this code update your as follows:
<input name="number" type="text" ng-model="validNumber" ng-model-options="modelOptions" number>
I really hope this answers all of your questions, please let me know if I can help any more.
Leon.
Here is my plnkr with the relevant code:
$controller.$$runValidators = function(originalRun) {
var lastModelValue, lastViewValue;
return function() {
var ctrl = this;
var doneCallback = arguments[arguments.length-1];
arguments[arguments.length-1] = function(allValid) {
doneCallback(allValid);
console.log(allValid);
console.log('valid:' +allValid+ ' value:' +ctrl.$viewValue);
if (ctrl.$viewValue) {
lastViewValue= allValid ? ctrl.$viewValue : lastViewValue | '';
lastModelValue= allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$modelValue = allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$$writeModelToScope();
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = lastViewValue;
ctrl.$render();
}
console.log(ctrl.$viewValue + ' '+lastViewValue);
// console.log( ctrl.$modelValue);
};
originalRun.apply(this, arguments);
}
}($controller.$$runValidators);
Can it be a valid solution?
the only way i think you can intercept the angular validation flow is override the $$runValidators. Maybe this code need a little bit of tweaking but works.
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'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);