How directives in Angularjs works? - angularjs

I am struggle with one question and can not figure out why it happens.
I have a module (portfolioModule), service (Menu) and a directive (niMenu). The directive should render a menu item with its subitems.
HTML:
<div ng-app="portfolioModule">
<script id="menuItem" type="text/ng-template">
<li>
<a>{{item.name}}</a>
<ul>
<li ng-repeat="item in menu.getKids(item.id)" ng-include="'menuItem'"></li>
</ul>
</li>
</script>
<div>
<div ni-menu="test">test1</div>
</div>
</div>
JavaScript:
var portfolioModule = angular.module('portfolioModule', []);
portfolioModule.factory('Menu', function() {
var items = [
{
"parent_id": null,
"id": 1,
"name": "foo"
}, {
"parent_id": 1,
"id": 2,
"name": "foo-bar"
}
];
this.getKids = function(parent_id) {
var result = [];
parent_id = parent_id || null;
console.log('getKids', parent_id);
angular.forEach(items, function(value) {
if (value.parent_id === parent_id) {
result.push(value);
}
});
return result;
};
return this;
}
);
portfolioModule.directive('niMenu', function(Menu) {
var niMenu;
return niMenu = {
template: '<ul ng-include="\'menuItem\'" ng-repeat="item in menu.getKids()"></ul>',
restirt: 'A',
link: function(scope, element, attrs) {
console.log('link');
scope.menu = Menu;
}
};
}
);
Working demo: http://jsfiddle.net/VanSanblch/Ng7ef.
In html I call niMenu by ni-menu. The directive has a template and a link function that put Menu-service into a scope of module. In directive's template I use Menu.getKids() and get all top level items. Later, in template that used by ng-include I call Menu.getKids(item.id) and get all children of particular item.
Everything works excellent except one small detail. If you open console then you can observe that there are much more calls of getKids than I am expected. For example, for array of two elements the number of getKids calls is nine.
Could someone explain why on earth that happens?

Ok, so the reason it's executing more than once is because that's how the digest cycle works: it continuously executes all view expressions (like the one you pass to ngRepeat) until it expresses no change. See this answer for more information, but the takeaway is this: it will always execute at least once, but oftentimes more.
When using ngRepeat, you generally want to avoid fetching the data from a function because it negatively impacts performance; why call the same function more than once when the data never changed? A better approach is to have your controller (or in this case directive) execute your function and store the results on the scope, so your ngRepeat looks like ng-repeat="item in items" instead of ng-repeat="item in getItems()".
Unfortunately, that means you have to restructure the way you have your directive working. This turns out to be a good idea anyway, because your directive can be rewritten to be a little bit simpler - you don't need any ngInclude, for example.
What you want to do is create two directives because you have two templates: one to represent the overall menu and one to represent a child item (which, in turn, can have child items). The menu directive should repeat over the top-level menu items, drawing a menuItem for each one. The menuItem directive should check for children and repeat over those, if necessary, with more menuItems. Etc. Here's an example created by Andy Joslin of how you can accomplish a recursive directory: http://plnkr.co/edit/T0BgQR.
To implement something like this moves beyond the scope of this question, but take a stab at it and post a new question if you need help.
Good luck!

Related

how to create personal scope for each compiled element?

When I create new 'wItem' object and want to append compiled DOM to container(not ng-repeat)
container.append($compile(UIService.appendItem())($scope));
var children = container[0].children;
var length = children.length-1;
//take appended element and set new scope Item there
var newEl = children[length];
angular.element(newEl).scope().item = wItem;
UIService.appendItem
function appendWorkItem() {
return '<div layout="column" class=" workItem workItemName" id={{item._id}}child > ' +
'<div>{{item.name}}</div> ' +
' </div>'
}
the result is:
First time
wItem[0] = {_id:1, name: item1}
, it creates wItem and appends to DOM!
second and other times I should be:
wItem[1] = {_id:2, name: item2}
wItem[2] = {_id:3, name: item3}
it creates new wItem and when it appends to DOM it updates previous created elements with the last wItem scope.and I getting this:
I am not so good in it,but I think it passes scope of last element to prev ones. How to fix it?
First, don't manipulate the DOM from your controller. Controllers provide scope and are generally nothing but scoping glue for views and models.
Get familiar with directives because they're practically required. Similarly it is critical to understand scopes. Failing to understand scopes and directives will doom your angular efforts.
Here's a directive implementing your UIService. It can be done other ways.
.directive("workItem", ['UIService', function(uiservice) {
return {
scope: {
item: "="
},
template: function() {
return uiservice.appendWorkItem();
}
}
}])
As has been pointed out you need to familiarize yourself with the ng-repeat directive. An example using the workItem directive and ng-repeat might look like this
<div ng-controller="WorkItems">
<div ng-repeat="item in items">
<work-item item="item"></work-item>
</div>
</div>
Here's a plnkr demonstrating the above directive used with ng-repeat.
I suggest staying away from $compile until you have a good grasp of directives and scopes (and actually have a bona fide reason to use it).

Angular "bind twice"

I'm trying to keep my watches down by using one-time binding (::) in most places.
However, I've run into the situation where I need to wait for one property of an object to arrive from our server.
Is there someway I can make Angular bind twice (first to a placeholder and second to the actual value)?
I tried accomplishing this using bindonce but it did not seem to work (I am guessing this is because bindonce wants to watch an entire object, not a single property).
Another solution would be if I could somehow remove a watch from the templates after the value comes in, if that is possible.
My objects look something like this:
{
name: 'Name',
id: 'Placeholder'
}
And my template:
<div ng-repeat="object in objects">
{{::object.name}}
{{::object.id}}
</div>
Id will change once and only once in the application life time, having a watch forever for a value that will only change once feels wasteful as we'll have many of these objects in the list.
I think this is what you are looking for! Plunkr
I just wrote a bind-twice directive and if I did not completely missed the question it should solve your problem:
directive("bindTwice", function($interpolate) {
return {
restrict: "A",
scope: false,
link: function(scope, iElement, iAttrs) {
var changeCount = 0;
var cancelFn = scope.$watch(iAttrs.bindTwice, function(value) {
iElement.text(value === undefined ? '' : value);
changeCount++;
if (changeCount === 3) {
cancelFn();
}
});
}
}
});
What I do is, I add a watcher on the scope element we need to watch and update the content just like ng-bind does. But when changeCount hit the limit I simply cancel $watch effectively cleaning it from watchlist.
Usage:
<body ng-controller="c1">
<div ng-repeat="t in test">
<p>{{ ::t.binding }}</p>
<p bind-twice="t.binding"></p>
<p>{{ t.binding }}</p>
</div>
</body>
Please see Plunkr for working example.

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

What is the "right" way in Angularjs of doing "master-detail" inter directive communication

I have a directive that displays a list of "master" items and when the user clicks on one of these items I want any "details" directives on the page (there could be more than one) to be updated with the details of the currently selected "master" item.
Currently I'm using id and href attributes as a way for a "details" directive to find its corresponding master directive. But my impression is that this is not the angular way, so if it's not, what would be a better solution?
I appreciate that typically when the issue of inter-communication between directives is raised then the obvious solutions are either to use require: "^master-directive" or to use a service, but in this case the directives are not in the same hierarchy and I don't think using a service is appropriate, as it would make the solution more complicated.
This is some illustrative code showing what I'm doing currently.
<div>
<master-list id="master1"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
In the master-list directive when an item is selected I set an attribute to indicate the currently selected master item:
attrs.$set('masterListItemId',item.id);
In the details-item directive's link function I do:
if (attrs.href) {
var id = attrs.href.split('#')[1];
var masterList = angular.element(document.getElementById(id));
if (masterList) {
var ctrl = masterList.controller('masterList');
ctrl.attrs().$observe('masterListItemId',function(value) {
attrs.$set('detailItemId',value);
});
}
}
attrs.$observe('detailItemId',function(id) {
// detail id changed so refresh
});
One aspect that put me off from using a service for inter-directive communication was that it is possible (in my situation) to have multiple 'masterList' elements on the same page and if these were logically related to the same service, the service would end up managing the selection state of multiple masterList elements. If you then consider each masterList element had an associated detailItem how are the right detailItem elements updated to reflect the state of its associated masterList?
<div>
<master-list id="master1"></master-list>
</div>
<div>
<master-list id="master2"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
<div>
<details-item href="#master2" ></details-item>
</div>
Finally I was trying to use directives, rather than using controller code (as has been sensibly suggested) as I'd really like the relationship between a masterList and its associated detailItems to be 'declared' in the html, rather than javascript, so it is obvious how the elements relate to each other by looking at the html alone.
This is particularly important as I have users that have sufficient knowledge to create a html ui using directives, but understanding javascript is a step too far.
Is there a better way of achieving the same thing that is more aligned with the angular way of doing things?
I think I would use a service for this. The service would hold the details data you care about, so it would look something like this.
In your master-list template, you might have something like a list of items:
<ul>
<li ng-repeat"item in items"><a ng-click="select(item)">{{item.name}}</a></li>
</ul>
...or similar.
Then in your directives, you would have (partial code only)
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick(item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.item;
}
};
})
And then use data in your details template:
<div>Details for {{data.name}}</div>
<ul>
<li ng-repeat="detail in data.details">{{detail.description}}</li>
</ul>
Or something like that.
I would not use id or href, instead use a service to retrieve, save and pass the info.
EDIT:
Here is a jsfiddle that does it between 2 controllers but a directive would be the same idea
http://jsfiddle.net/u3u5kte7/
EDIT:
If you want to have multiple masters and details, leave the templates unchanged, but change your directive controllers and services as follows:
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick($scope.listId,item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.get($scope.listId).item;
}
};
})
.factory('DetailsService',function(){
var data = {};
return {
pick: function(id,item) {
data[id] = data[id] || {item:{}};
// set data[id].item to whatever you want here
},
get: function(id) {
data[id] = data[id] || {item:{}};
return data[id];
}
};
})
I would opt for a different approach altogether without directives. Directives are ideal for DOM manipulation. But in this case I would stick to using just the template and a controller that manages all the data and get rid of the directives. Use ng-repeat to repeat the items
Check out this fiddle for an example of this: http://jsfiddle.net/wbrand/2xrne4k3
template:
<div ng-controller="ItemController as ic">
Masterlist:
<ul><li ng-repeat="item in ic.items" ng-click="ic.selected($index)">{{item.prop1}}</li></ul>
Detaillist:
<ul><li ng-repeat="item in ic.items" >
{{item.prop1}}
<span ng-if="item.selected">SELECTED!</span>
</li></ul>
</div>
controller:
angular.module('app',[]).controller('ItemController',function(){
this.items = [{prop1:'some value'},{prop1:'some other value'}]
this.selectedItemIndex;
this.selected = function(index){
this.items.forEach(function(item){
item.selected = false;
})
this.items[index].selected = true
}
})

How does AngularJS know to check/trigger the 'isToggled' condition in this working example?

I'm learning some basics in angularjs and I've somewhat accidentally stumbled across a pattern that works, but I'm not sure why. Also not sure if I'm doing it in an 'angular best practices' kind of way. This example is greatly simplified from what I'm really working on, but the general concept is the same: setting visibility of children based on a toggle on the parent
It seems to my angular-naive self, that it must somehow be watching functions which use the toggledGroups array and then ???. The conditional ng-show does work, I just don't really get why the ng-show="isToggled(group)" gets re-evaluated since the array I'm modifying isn't on $scope.
Do all functions that exist on $scope get re-evaluated on every $digest? If so, this seems to me like it could create a bottleneck on ng-repeats over large data sets.
As I alluded to above, is this an acceptable pattern or is there a better/'more angular' way I should think about this interaction?
JS:
var app = angular.module('demo', []);
app.service('TransportationService', function(){
var groups = [{
group: "planes",
items: ["Airbus A300", "Extra 300S", "Stearman"]
}, {
group: "trains",
items: ["Flying Scotsman", "The Rocket", "Silver Streak"]
}, {
group: "automobiles",
items: ["Veyron", "Vanquish", "FF", "Continental GT"]
}];
return {
groups: groups
}
});
app.controller('TransportationController', ['$scope', 'TransportationService', function($scope, ts){
var toggledGroups = [];
function isToggled(group) {
return toggledGroups.indexOf(group) > -1;
}
function toggleGroup(group) {
var index = toggledGroups.indexOf(group);
if (index > -1) {
toggledGroups.splice(index, 1);
} else {
toggledGroups.push(group);
}
}
$scope.groups = ts.groups;
$scope.isToggled = isToggled;
$scope.toggleGroup = toggleGroup;
}]);
HTML:
<div ng-app="demo" ng-controller="TransportationController">
<div ng-repeat="group in groups">
<h3>{{group.group}}</h3>
<button ng-click="toggleGroup(group)">Toggle</button>
<ul ng-show="isToggled(group)">
<li ng-repeat="item in group.items">{{item}}</li>
</ul>
</div>
</div>
Fiddle: http://jsfiddle.net/wpHZg/1/
Your assumptions are correct. Every time a digest cycle kicks off, it executes that function. You are also correct in the fact that it's more expensive. How much more expensive? Depends on the size of your data set. From the logic you show, I can't imagine it would be too hard on the CPU, but you should consider the fact that it's going to pile up on all of your other application logic. From the way I see it, you could take 2 courses of action to reduce the load on the client CPU.
1) easy: add a toggled property to your groups:
$scope.toggleGroup = function(group) {
group.toggled = !group.toggled
};
<ul ng-if="group.toggled"></ul>
2) More complex: Allows for some DRYness, make a directive! :D
app.directive('toggler', function() {
return {
restrict: 'A',
link: function(scope, elem) {
elem.find('.button-toggler').on('click', function() {
elem.find('.togglee').toggle();
});
}
};
});
<div toggler="">
<button class="button-toggler">Toggle Me Hard!</button>
<ul class="togglee" style="display:none"></ul>
</div>
The second one allows for you to transport your directive to other parts of your code with ease. It also will not kick off a digest cycle as it is all jQuery based (which in this case is good because you're not changing any scope data on click). Either way, you're going to have a cleaner application.

Resources