Angular one time binding without a watch - javascript

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...

Related

How to implement reverse one time bind ng-if expression in AngularJS?

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

What is the point of controller.$viewValue/controller.$modelValue?

I'm unclear what the relation is between scope.ngModel and controller.$viewValue/controller.$modelValue/controller.$setViewValue() is, and specifically, what the point of the latter three is. For example, see this jsfiddle:
<input type="text" ng-model="foo" my-directive>
and:
myApp.directive('myDirective', function($timeout) {
return {
require: 'ngModel',
restrict: 'A',
scope: { ngModel: '=' },
link: function (scope, element, attrs, controller) {
function log() {
console.log(scope.ngModel);
console.log(controller.$viewValue);
console.log(controller.$modelValue);
}
log();
controller.$setViewValue("boorb");
log();
scope.$watch('ngModel', function (val) {
console.log("val is now", val);
});
$timeout(function () {
log();
}, 2000);
}
}
});
With the controller being:
function MyCtrl($scope, $timeout) {
$scope.foo = 'ahha';
$timeout(function () {
$scope.foo = "good";
}, 1000);
}
The output is:
(index):45 ahha
(index):46 NaN
(index):47 NaN
(index):45 ahha
(index):46 boorb
(index):47 boorb
(index):53 val is now ahha
(index):53 val is now good
(index):45 good
(index):46 boorb
(index):47 boorb
controller.$viewValue did not start out as the value of the foo variable. Further, controller.$setViewValue("boorb") didn't influence scope.ngModel at all, nor was the update reflected in the HTML. Thus it seems there is no relation between scope.ngModel and controller.$viewValue. It seems that with anything I'd want to do, I would just use scope.ngModel, and watch those values. What is ever the point of using controller.$viewValue and controller.$modelValue or keeping them up to date with scope.ngModel?
scope: { ngModel: '=' }, creates an isolated scope for the directive, which means that changes to foo in the directive will no longer be reflected in the parent scope of MyCtrl.
Also, changes made by $setViewValue() will not get reflected in the DOM until controller.$render() is called, which tells Angular to update the DOM in the next digest cycle.
But to answer the question, NgModelController and its methods are really only necessary if you need to create some extra-special-custom-fancy data-binding directives. For normal data input and validation, you shouldn't ever need to use it. From the documentation (emphasis mine):
[NgModelController] contains services for data-binding, validation, CSS updates, and value formatting and parsing. It purposefully does not contain any logic which deals with DOM rendering or listening to DOM events. Such DOM related logic should be provided by other directives which make use of NgModelController for data-binding to control elements. Angular provides this DOM logic for most input elements.
The confusion here is coming from sticking a directive onto an existing directive, namely ngInput.
Instead, consider a fresh directive:
<my-directive ng-model="ugh">Sup</my-directive>
With:
$rootScope.ugh = 40;
And:
.directive('myDirective', function () {
return {
require: "ngModel",
// element-only directive
restrict: "E",
// template turns the directive into one input tag
// 'inner' is on the scope of the *directive*
template: "<input type='text' ng-model='inner'/>",
// the directive will have its own isolated scope
scope: { },
link: function (scope, element, attrs, ngModelCtrl) {
// formatter goes from modelValue (i.e. $rootScope.ugh) to
// view value (in this case, the string of twice the model
// value + '-'
ngModelCtrl.$formatters.push(function (modelValue) {
return ('' + (modelValue * 2)) + '-';
});
// render does what is necessary to display the view value
// in this case, sets the scope.inner so that the inner
// <input> can render it
ngModelCtrl.$render = function () {
scope.inner = ngModelCtrl.$viewValue;
};
// changes on the inner should trigger changes in the view value
scope.$watch('inner', function (newValue) {
ngModelCtrl.$setViewValue(newValue);
});
// when the view value changes, it gets parsed back into a model
// value via the parsers, which then sets the $modelValue, which
// then sets the underlying model ($rootScope.ugh)
ngModelCtrl.$parsers.push(function (viewValue) {
var sub = viewValue.substr(0, viewValue.length-1);
return parseInt(sub)/2;
});
}
};
})
Try it on Plunker.
Note that typeof ugh stays "number", even though the directive's view value is of a different type.

Angular directives settings pattern

I'm looking for a good design pattern to provide angular directives render acording to some global specified parametrs.
For example, I have some factory called "Settings", that holds the value "directiveColor: red".
When i do the link in my directive, I ask the Settings about my directiveColor value. Everything is working fine - I got red and put element on the page. But I have hundreds of this elements on the page, and every directive before render ask for settings... I think it's not very good way.
What will you recomend?
UPD
factory
app.factory('Settings', function() {
var data = {
//...
directiveColor: red //set by user
}
//...
GetSettings : function () {return data}
}
directve
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
link: function(scope, elem, attr) {
scope.data = {
//...
color: Settings.GetSettings().directiveColor
};
};
}]);
//later "color" used in template through the scope
That's how it works for now (works fine). But every time, when I render directive (many many times on the page, ngRepeat for table data), my directive ask for Settings to choose its color. I think, it is not good. Or maybe not?
There are two considerations here. First, you are right that it is not optimal, and directive actually provides a way to do that call once, read about Compile-PreLink-PostLink in angular directives. Basically you want this call in Compile step if it is the same for all directives in your app.
Second consideration is that Settings.GetSettings().directiveColor will give really really small overhead if GetSettings() returns just an object that you only create once ( and that is what happened as angular factories are singletons )
In your case you can do
app.factory('Settings', function() {
var data = {
directiveColor: 'red' //set by user
}
return {
GetSettings : function () {return data}
}
})
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
compile: function(elem, attrs) {
var color = Settings.GetSettings().directiveColor
return function postLink(scope, elem, attr) {
scope.data = {
color: color
};
}
}
}
}])
instead of declaring link property on directive.

Directive listen to controller variable change

Just starting out in AngularJS and trying to figure out the best practice for listening to events when a variable within a controller changes. The only way I have gotten it to work is with an emit, as follows.
For example:
var app = angular.module("sampleApp", [])
app.controller("AppCtrl", function($scope){
$scope.elements = [
{
name: "test"
}
]
$scope.addElement = function() {
$scope.elements.push({
name: "test" + $scope.elements.length
})
$scope.$emit('elementsChanged', $scope.elements);
}
})
app.directive('render', function() {
var renderFunc = function() {
console.log("model updated");
}
return {
restrict: 'E',
link: function(scope, element, attrs, ngModel) {
scope.$on('elementsChanged', function(event, args) {
renderFunc();
})
}
}
})
This seems a bit wonky, and I feel like I'm working against the point of angular. I've tried to have a $watch on a model, but that doesn't seem to be working. Any help on this would be very appreciated, thanks!
I'm going to assume you're using unstable Angular, because $watchCollection is only in the unstable branch.
$watchCollection(obj, listener)
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays, this implies watching the array items; for object maps, this implies watching the properties). If a change is detected, the listener callback is fired.
The 'Angular' way of doing this would be to watch an attribute in your directive.
<render collection='elements'></render>
Your directive
app.directive('render', function() {
var renderFunc = function() {
console.log("model updated");
}
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.$watchCollection(attrs.collection, function(val) {
renderFunc();
});
}
}
})
If you're doing this on stable angular, you can pass true as the last argument to scope.$watch, which will watch for equality rather than reference.
$watch(watchExpression, listener, objectEquality)
objectEquality (optional) boolean
Compare object for equality rather than for reference.
What's happening here is the collection attribute on the DOM element specifies which property on our scope we should watch. The $watchCollection function callback will be executed anytime that value changes on the scope, so we can safely fire the renderFunc().
Events in Angular should really not be used that often. You were right in thinking there was a better way. Hopefully this helps.

How to Create recursive Angular.js Templates without isolating scope?

I have a recursive data structure I am trying to represent in Angular.js. a simplified demo is available here:
http://plnkr.co/edit/vsUHLYMfI4okbiVlCK7O?p=preview
In the Preview, I have the following HTML for a recursive object:
<ul>
<li ng-repeat="person in people">
<span ng-click="updateClicks(person)">{{person.name}}</span>
<ul>
<li ng-repeat="kid in person.kids">
<span ng-click="updateClicks(kid)">{{kid.name}}</span>
</li>
</ul>
</li>
</ul>
In my application, the view is much more complex. I would like to have a way to generate the template html for each person in a recursive fashion. I tried doing this with a directive, however I ran into issues with infinite loops when I did not isolate the scope. And when I did isolate the scope, I was no longer able to call functions that are tied to the controller (in this example, the updateClicks function, however in my application there are several).
How can I generate html for these objects recursively, and still be able to call functions belonging to a controller?
I think the best way to do this is with an $emit.
Let's say your recursive directive looks like this:
directive('person', function($compile){
return{
restrict: 'A',
link: function(scope, element, attributes){
//recursive bit, if we've got kids, compile & append them on
if(scope.person.kids && angular.isArray(scope.person.kids)) {
$compile('<ul><li ng-repeat="kid in person.kids" person="kid"></li></ul>')(scope, function(cloned, scope){
element.find('li').append(cloned);
});
}
},
scope:{
person:'='
},
template: '<li><span ng-click="$emit(\'clicked\', person)">{{person.name}}</span></li>'
}
});
notice the ng-click="$emit(clicked, person)" code, don't be distracted the \, that's just there to escape. $scope.$emit will send an event all the way up your scope chain, so that in your controller, your clicked function stays mostly unchanged, but now instead of being triggered by ng-click, you're listening for the event.
$scope.$on('clicked', function(event, person){
person.clicks++;
alert(person.name + ' has ' + person.clicks + ' clicks!');
});
cool thing is that the event object even has the isolated scopes from your recursed directives.
Here's the fully working plnkr: http://plnkr.co/edit/3z8OXOeB5FhWp9XAW58G?p=preview
even went down to tertiary level to make sure recursion was working.
Recursive tree with angular directive without scope isolation, forces you to simulate isolation by using different scope properties per depth level.
I didn't find any so I wrote my own.
Let's say your HTML is :
<body ng-app="App" ng-controller="AppCtrl">
<div test="tree.children" test-label="tree.label">{{b}}</div>
</body>
Then you have a main module and a controller adding a tree to the scope :
var App = angular.module('App', []);
App.controller('AppCtrl', function($scope, $timeout) {
// prodive a simple tree
$scope.tree = {
label: 'A',
children: [
{
label: 'a',
children: [
{ label: '1' },
{ label: '2' }
]
},
{
label: 'b',
children: [
{ label: '1' },
{ label: '2' }
]
}
]
};
// test that pushing a child in the tree is ok
$timeout(function() {
$scope.tree.children[1].children.push({label: 'c'});
},2000);
$timeout(function() {
// test that changing a label is ok
$scope.tree.children[1].label = 'newLabel';
},4000);
});
Finally consider the following implementation of the directive test :
App.directive('test', function($compile) {
// use an int to suffix scope properties
// so that inheritance does not cause infinite loops anymore
var inc = 0;
return {
restrict: 'A',
compile: function(element, attr) {
// prepare property names
var prop = 'test'+(++inc),
childrenProp = 'children_'+prop,
labelProp = 'label'+prop,
childProp = 'child_'+prop;
return function(scope, element, attr) {
// create a child scope
var childScope = scope.$new();
function observeParams() {
// eval attributes in current scope
// and generate html depending on the type
var iTest = scope.$eval(attr.test),
iLabel = scope.$eval(attr.testLabel),
html = typeof iTest === 'object' ?
'<div>{{'+labelProp+'}}<ul><li ng-repeat="'+childProp+' in '+childrenProp+'"><div test="'+childProp+'.children" test-label="'+childProp+'.label">{{'+childProp+'}}</div></li></ul></div>'
: '<div>{{'+labelProp+'}}</div>';
// set scope values and references
childScope[childrenProp]= iTest;
childScope[labelProp]= iLabel;
// fill html
element.html(html);
// compile the new content againts child scope
$compile(element.contents())(childScope);
}
// set watchers
scope.$watch(attr.test, observeParams);
scope.$watch(attr.testLabel, observeParams);
};
}
};
});
All the explanations are in the comments.
You may have a look at the JSBin.
My implementation can of course be improved.

Categories

Resources