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

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.

Related

Angular one time binding without a watch

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

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.

How do you pass an object reference to a directive?

In my app, I have an ng-repeat that iterate through JSON and prints each object to the page. So for example, my ng-repeat prints the animals
[
{
name: "horse",
sound: "Nay",
legs: 4,
},
{
name: "beaver",
sound: "thwack",
legs: 2
}
]
I also want to pass each animal to a directive and possibly add some key, values to them. The problem is, when I add the animal object as an attribute and update it in the directive,
i.e.
<animal this-animal={{animal}}></animal>
and in the directives link function
var animalObj = scope.$eval(attrs.thisAnimal);
animalObj["gestation"] = 10;
it doesn't update in the original JSON. It's like it gets disconnected from the overall array of all animals.
Why? How do I keep it all together? I want updates to individual objects to make changes in the main JSON object.
By using {{model}} in html it will resolve that value and place it into the HTML. In your case the JSON is getting stringified and then coverted back thus making a cloned object. Instead of using {{model}} just pass the name of the value.
<div my-directive="model">
Then access the model value using $parse
module.directive('myDirective', function($parse) {
return {
link: function(scope, element, attrs) {
var val = $parse(attrs.my directive)(scope);
}
};
});
you can use isolate scope. Assuming the array of animals is a scope property of the parent controller, you can do this:
<div ng-repeat="animal in animals">
<animal this-animal="animal"></animal>
</div>
And in the directive code:
module.directive('myDirective', function() {
return {
scope: {
thisAnimal: "="
},
link: function(scope, element, attrs) {
scope.thisAnimal.gestation = 10;
}
};
});
Refer the isolate scope section of this page for more details:
https://docs.angularjs.org/guide/directive

Angularjs 1.2.6: watch directive's parent scope collection

I'm building custom tree directive:
<ul tree="treeOptions">
<li>{{ item.code + ' - ' + item.name }}</li>
</ul>
In javascript:
$scope.myItems = [];
$scope.treeOptions = {
data: 'myItems',
...
}
In directive:
(function (angular) {
'use strict';
angular.module('tree', []).
directive('tree', ['$compile', '$document', function ($compile,
$document) {
return {
restrict: 'A',
scope: { treeOptions: '=tree' }, //Isolated scope
compile: function (elem, attrs) {
//...
return function (scope, elem, attrs) {
//...
scope.$parent.$watchCollection(scope.treeOptions.data,
function (newItems, oldItems) {
var addedItems = _.difference(newItems, oldItems);
var removedItems = _.difference(oldItems, newItems);
//but newItems and oldItems always the same
//...
}
);
}
};
}
};
} ]);
})(angular);
I'm using lodash ( _ ) to find differences between new and old items.
The problem is newItems and oldItems are always the same, even after new items are pushed to parent scope's myItems array. What am I missing?
So, this is definitely an issue in the angular framework. I'm sure they will get around to fixing it sooner or later, but in the mean time if you need to get your code to work I was able to put together a sample that works quite well. The core is to not use the default old/new elements:
var oldWorkingItems = scope.$parent[attrs.testDirective].slice(0);
scope.$parent.$watchCollection(attrs.testDirective,
function (newItems, oldItems) {
console.log('NEW Items:' + newItems);
console.log('Old Items:' + oldWorkingItems);
For the full example as well as my reproduction of the error, see the following Plunkr: http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview. By the way, the reason this is called so many times is because it is inside an ng-repeat, but that was my way to force the use of "$parent". Anyways, hope this helps some!
Edit - It really annoyed me how many times the directive was being run in the ng-repeat so I wrote another plunker (http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview) that uses a single ng-repeat:
<div ng-repeat="element in [1]">
<div test-directive="testCollection"></div>
</div>
This only calls the directive twice (why twice, I'm still not sure).
As Gruff Bunny pointed out in the comments, this is an open issue with AngularJS up to the current version (1.2.13). The workaround for now is to use $watch( , true) or do as drew_w suggested.

Angular JS: How do I set a property on directive local scope that i can use in the template?

I want to access a variable in this case map in the directive without having to predefine it like setting it as an attr of the directrive map="someValue". And also i dont want to use scope.$apply because i actually only want the variable in the isolated scope of the directive. Is this even possible ?
What is the best practice here? Basically my directive needs to do both. Access the parent scope and have its own scope which with i can build the template with.
Thank you everybody.
Here my Js code:
.directive('myFilter', function() {
return {
restrict: 'E',
scope: {
source: '=source',
key: '=key',
},
link: function(scope, element, attrs) {
scope.$on('dataLoaded', function(e) {
scope.map = {};
angular.forEach(scope.source, function(paramObj) {
if (!scope.map[paramObj[scope.key]]) {
var newEntry = {
value: paramObj[scope.key],
isChecked: false
}
scope.map[paramObj[scope.key]] = newEntry;
}
});
});
}
}
});
and my html:
<my-filter source="metaPara.filteredParameters" key="'value'">
<table class="table table-borered">
<tr data-ng-repeat="item in map">
<td>{{item.value}}</td>
</tr>
</table>
</my-filter>
You might want to refer to the Angular documentation for directives, again.
If you want an isolate-scope (a scope which has no access to ancestors), then use
scope : { /* ... */ }
otherwise, if you want a unique scope, which does have access to ancestors, use
scope : true
Then, you can put your HTML-modifying or event-listening (that doesn't rely on ng-click or something else Angular already covers) in
link : function (scope, el, attrs, controller) { }
...and you can put all of your regular implementation inside of
controller : ["$scope", function ($scope) {
var myController = this;
myController.property = "12";
}],
controllerAs : "myController"
So that in your template you can say:
<span>{{ myController.property }}</span>
You can also use a pre-registered controller, which you call by name:
controller : "mySimpleController",
controllerAs : "myController"
Also, rather than using $scope.$apply, I'd recommend using $timeout (has to be injected).
The difference is that $scope.$apply will only work at certain points -- if you're already inside of a digest cycle, it will throw an error, and not update anything.
$timeout( ) sets the updates to happen during the next update-cycle.
Ideally, you should know whether or not you need an $apply or not, and be able to guarantee that you're only using it in one spot, per update/digest, but $timeout will save you from those points where you aren't necessarily sure.

Categories

Resources