I have a simple nav object setup that lists the nav items (and whether they should appear in the primary nav or not). It seems though when I try to mix ng-if with ng-repeat, things fall apart, but when I mix ng-show with ng-repeat it works fine (but I end up with a bunch of hidden elements that I don't want appended to the DOM).
<section class="nav">
<a ng-repeat="(key, item) in route.routes"
ng-href="{{key}}"
ng-show="item.nav"
>
{{item.label}}
</a>
</section>
But the following doesn't work (note the ng-show is now ng-if):
<section class="nav">
<a ng-repeat="(key, item) in route.routes"
ng-href="{{key}}"
ng-if="item.nav"
>
{{item.label}}
</a>
</section>
The routes object looks like
routes: {
'/home': { label: 'Home', nav: true },
'/contact': { label: 'Contact', nav: false},
// etc
}
I receive the following error when trying to use ng-if:
Error: Multiple directives [ngIf, ngRepeat] asking for transclusion on:
I guess it's trying to tell me that I can't state it's declaration for existing twice. I could use ng-if on an inner element, but I think I would still end up with a bunch of empty outer a tags.
There's probably a better solution, but after reading the replies above, you can try making your own custom filter:
angular.module('yourModule').filter('filterNavItems', function() {
return function(input) {
var inputArray = [];
for(var item in input) {
inputArray.push(input[item]);
}
return inputArray.filter(function(v) { return v.nav; });
};
});
Then to use it:
<section class="nav">
<a ng-repeat="(key, item) in routes | filterNavItems"
ng-href="{{key}}">
{{item.label}}
</a>
</section>
Here's the Plunker: http://plnkr.co/edit/srMbxK?p=preview
Instead of ng-if you should use a filter (http://docs.angularjs.org/api/ng.filter:filter) on you ng-repeat to exclude certain items from your list.
I ran into this problem as well, and found a couple ways to solve it.
The first thing I tried was to combine ng-if and ng-repeat into a custom directive. I'll push that up to github sometime soon, but it's kludgy.
The simpler way to do it is to modify your route.routes collection (or create a placeholder collection)
$scope.filteredRoutes = {};
angular.forEach($scope.route.routes, function(item, key) {
if (item.nav) { $scope.filteredRoutes[key] = item; }
};
and in your view
...
<a ng-repeat="(key, item) in filteredRoutes"
...
If you need it to be dynamically updated, just set up watches, etc.
How about this one-liner using $filter:
$scope.filteredRoutes = $filter('filter')($scope.route.routes, function(route){
return route.nav;
});
You should use a filter in your ng-repeat instead of using ng-if.
This should work:
<section class="nav">
<a ng-repeat="(key, item) in route.routes | filter:item.nav"
ng-href="{{key}}">
{{item.label}}
</a>
</section>
Warning: I haven't actually tested this code.
Related
Here is my code that i am struggling with:
.directive('mydirective', function() {
return {
template: '<ul>\
<li priority="500"><i></i>Inbox</li>\
<li priority="500"><i></i>Language</li>\
<li priority="1000"><i></i>Settings</li>\
<li priority="500"><i></i>Contact</li>\
<li priority="1000"><i></i>Help</li>\
</ul>',
link: function(v, e, a){
var elm = e[0].childNodes[0].children;
}
}
});
All i want to do is to add a class to list items that have let say the priority= 1000. I am able to get the element but when i do the loop i don't know how to filter the attribute so then i can add the class. Anyone came around this?
Thanks
If you are going to be sending data and using a ng-repeat then you should be able to use
ng-class="{'className': data.priority === 1000}"
From recursive list of items
<script type="text/ng-template" id="menu_sublevel.html">
id:{{item.id}}
<ul ng-if="item.subs">
<li ng-repeat="item in item.subs" ng-click="openItem(item)" ng-include="'menu_sublevel.html'">
id:{{item.id}}
</li>
</ul>
</script>
<ul>
<li ng-repeat="item in menu.items" ng-click="$event.stopPropagation()" ng-include="'menu_sublevel.html'"></li>
</ul>
and effect
id:0
...id:4
...id:5
...id:16
...id:17
...id:18
...id:6
...id:20
...id:21
...id:22
I want to have selected one at time item.
When i write nested list without recursion i use id and on every level I have method for item selection and i chceck `
levelOneItemSelected.id === item.id
How to select child with id 16 and have his parent with id 5 opened and next parent with id 0 opened while changing selection closes opened items.
If, upon invocation of openItem(item), you also want to select/open its ancestors, then its best to have the reference from item to its parent, for example, item.$$parent. That would enable you to traverse the item's ancestors and modify them. Conceptually speaking, it would look like so:
$scope.openItem(item){
item.isOpen = true;
while (item.$$parent){
item = item.$$parent;
item.isOpen = true;
}
}
So, one way is to pre-process your items and set the .$$parent property accordingly.
If you don't like the idea of changing the item object (could be your domain model), you could always pre-process your domain model and produce a view model that wraps a domain model. It would look like so (in concept):
$scope.menu = [
{ $$parent: null,
item: {id: 0, subs: [
{ $$parent: parentObj, // points to its parent
item: {id: 10, subs: [...]}
}
]}
},
// etc ...
]
But if you don't want to modify either, you could use the fact that ng-repeat creates a child scope and instantiate the $$ancestors property at each scope level. (Notice also, that ng-click should be on the displayed item, not on the <li> for subitems):
<script type="text/ng-template" id="menu_sublevel.html">
<span ng-click="openItem(item, $$ancestors)"
ng-class="{'open': item.isOpen}">id:{{item.id}}</span>
<ul ng-if="item.subs"
ng-init="$$p = $$ancestors.slice(); $$p.push(item)">
<li ng-repeat="item in item.subs"
ng-init="$$ancestors = $$p"
ng-include="'menu_sublevel.html'">
id:{{item.id}}
</li>
</ul>
</script>
<ul>
<li ng-repeat="item in menu.items"
ng-init="$$ancestors = []"
ng-include="'menu_sublevel.html'"></li>
</ul>
Then, in the controller, openItem needs to change:
var currentOpenItem = null,
currentOpenItemAncestors = [];
$scope.openItem = function(item, ancestors){
// closes the currently open item and its ancestors
closeItem(currentOpenItem, currentOpenItemAncestors);
currentOpenItem = item;
currentOpenItemAncestors = ancestors;
openItem(item, ancestors);
}
Demo
The drawback of this approach is that it offloads some of the logic to the View and makes the View more complex and your controller less testable:
In bootstrap, the standard way of indicating that an item is selected is via the .active class. I have a list group that is created via a foreach knockout.js structure.
<div class="list-group" data-bind="foreach: people">
<a href="#" class="list-group-item" data-bind="click: $root.personSelected, css: {active: $root.chosenPerson.name == $data.name}">
<h4 class="list-group-item-heading" data-bind="text: name"></h4>
</a>
<div>
When a person is selected, my call to personSelected sets the selected object to a observable personSelected. I figured that I could use another data binding of the form css: {active: $root.chosenPerson.name == $data.name} to check if the current item was selected, but this does not appear to work. See the jsFiddle
I think that I might not be using the correct comparison statement, or perhaps there is a better way to do this. Any thoughts?
Thanks!
You are on the right track with the css binding and combining it with observables.
But first you need to fix your personSelected function to correctly set your chosenPerson observable (observable are functions you need to call them with the new value as the argument):
self.personSelected = function(person){
self.chosenPerson(person);
}
Then I would create a new helper function (you can shovel all this logic into the binding expression but this is not a good practice) which based on the name returns whether this person is the selected:
self.isSelected = function(name) {
var selectedperson = self.chosenPerson()
if(selectedperson) //handle if no person is selected
{
return selectedperson.name == name;
}
}
Then you just need to use this function in your binding:
<a href="#" class="list-group-item"
data-bind="click: $root.personSelected,
css: { active: $parent.isSelected(name) }">
Demo JSFiddle.
The problem is you weren't setting the value of chosen person observable, you were setting chosen person equal to something new. Also, when your view model is instantiated, you didn't have a value of chosen person yet defined to test against.
http://jsfiddle.net/x52VL/1/
When you set chosenPerson do this -
self.chosenPerson(person);
and test against it like this -
<a href="#" class="list-group-item" data-bind="click: $parent.personSelected, css: {active: $parent.chosenPerson().name == name}">
there's two model I put into Angular js
// full device list
$scope.devices = [{id:1, on:true}, {id:2, on:false}, {id:3, on:true}];
// devices with on == true
$scope.devicesOn = $scope.devices.filter(function(t){return t.on==true});
in page:
<span>the following {{ devicesOn.length }} of {{ devices.length }} are on</span>
<ul>
<li ng-repeat='device in devicesOn'>{{device.id}}</li>
</ul>
so the model devices is dynamic. However devicesOn isn't dynamic because it is not lazy evaluation.
I know you can write something like $scope.countOnDevices = function(){return ...} but it's not ideal. Any more elegant way to solve this?
It's like creating a view (applied with a where filter) from existing table in a RDBMS.
You can use a watcher to watch on the change of the devices list, the logic can be fired when there is any change to the list
$scope.$watch('devices', function () {
$scope.devicesOn = $scope.devices.filter(function (t) {
return t.on == true
});
}, true)
Demo
#sza's answer works great, I also found another solution from here:
https://groups.google.com/forum/#!topic/angular/3LyIjNroT5c
https://groups.google.com/forum/#!msg/angular/7WY_BmFzd3U/Zd_jHnMu58YJ
ng-repeat='item in filtered = (items | filter:filterExpr)
then simply use {{filtered.length}}
demo here
How can I move an element to different places in the DOM with angular js?
I have a list of elements like so
<ul id="list" ng-controller="ListController">
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController"><div>content</div></li>
<li ng-controller="ItemController">
<div>content</div>
<div id="overlay"></div>
</li>
</ul>
What I'm trying to accomplish is moving the #overlay from place to place within the list without having to have a hidden duplicate in every item that I flag hidden/unhidden.
If this was jquery I could just do something like this:
$("#overlay").appendTo("#list li:first-child");
Is there an equivalent way to do this in angular?
Thanks to your clarifications I can understand that you've got a list of items. You would like to be able to select one item in this list (swipe but potentially other events as well) and then display an additional DOM element (div) for a selected item. If the other item was selected it should be un-selected - this way only one item should have an additional div displayed.
If the above understanding is correct, then you could solve this with the simple ng-repeat and ng-show directives like this:
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div ng-click="open(item)">{{item.content}}</div>
<div ng-show="isOpen(item)">overlay: tweet, share, pin</div>
</li>
</ul>
where the code in the controller would be (showing a fragment of it only):
$scope.open = function(item){
if ($scope.isOpen(item)){
$scope.opened = undefined;
} else {
$scope.opened = item;
}
};
$scope.isOpen = function(item){
return $scope.opened === item;
};
Here is the complete jsFiddle: http://jsfiddle.net/pkozlowski_opensource/65Cxv/7/
If you are concerned about having too many DOM elements you could achieve the same using ng-switch directive:
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div ng-click="open(item)">{{item.content}}</div>
<ng-switch on="isOpen(item)">
<div ng-switch-when="true">overlay: tweet, share, pin</div>
</ng-switch>
</li>
</ul>
Here is the jsFiddle: http://jsfiddle.net/pkozlowski_opensource/bBtH3/2/
As an exercise for the reader (me), I wanted to try a custom directive to accomplish this. Here is what I came up with (after many failed attempts):
<ul ng-controller="ListController">
<li ng-repeat="item in items">
<div singleton-overlay>{{item.content}}</div>
</li>
</ul>
A service is required to store the element that currently has the overlay, if any. (I decided against using the controller for this, since I think a 'service + directive' would make for a more reusable component than a 'controller + directive'.)
service('singletonOverlayService', function() {
this.overlayElement = undefined;
})
And the directive:
directive('singletonOverlay', function(singletonOverlayService) {
return {
link: function(scope, element, attrs) {
element.bind('click', moveOrToggleOverlay);
function moveOrToggleOverlay() {
if (singletonOverlayService.overlayElement === element) {
angular.element(element.children()).remove();
singletonOverlayService.overlayElement = undefined;
} else {
if (singletonOverlayService.overlayElement != undefined) {
// this is a bit odd... modifying DOM elsewhere
angular.element(singletonOverlayService.overlayElement.children()).remove();
}
element.append('<div>overlay: tweet, share, pin</div>')
singletonOverlayService.overlayElement = element;
jsFiddle: http://jsfiddle.net/mrajcok/ya4De/
I think the implementation is a bit unconventional, though... the directive not only modifies the DOM associated with its own element, but it may also modify the DOM associated with the element that currently has the overlay.
I tried setting up $watches on scope and having the singleton store and modify scope objects, but I couldn't get the $watches to fire when I changed the scope from inside the moveOrToggleOverlay function.