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:
Related
I have an array of objects which contains a name and an id.
I'm using a v-for directive to iterate through those objects and display their name.
When I click on one of the <li>, it calls a method which stores in a data variable the id of the object selected.
I'm trying to apply a specific CSS class for this selected item (Applying a grey background in order to tell the user it's currently selected)
Since it's considered an anti-pattern to mix v-for and v-if, I'm currently looking for another alternative.
I've tried to call a method with the current item as an argument, and return a class like "CommonItemClass SelectedClass" if the current item == the selected item (stored with #click). Unfortunately, it doesn't work.
<ol>
<li #click="selectedCategorie(categorie)" v-for="categorie in categories" :key="categorie.id" :class="methodClass(categorie)">
{{ categorie.nom }}
</li>
</ol>
Try this:
In your template:
<ol>
<li
:key="categorie.id"
v-for="categorie in categories"
:class="{ 'selected': selectedId === categorie.id }"
#click="selectedId = categorie.id"
>{{ categorie.nom }}</li>
</ol>
In your javascript
data() {
return {
categories: [...],
selectedId: null
};
}
In your styles
li.selected {
background-color: grey;
}
I have an ng-repeat code with expandable list items. I would like to expand one item at a time.
The way I am trying to do it is
in the html file
<div data-ng-repeat="parts in data track by $index">
<li id="titleHelp" ng-click='setItem($index);">
and in the directive in the setItem function I want to collapse the previously expanded item and expand the new one. Is it possible to access one repeat element in the directive using index?
thanks!
How do you currently expand the list item?
What I would do is set a variable as soon as an item is clicked and in your repeated list do a
<div data-ng-repeat="parts in data track by $index">
<li id="titleHelp" ng-click='setItem($index);">
<div ng-show="$index = selected_item"> <!-- contents --></div>
In your setItem function:
$scope.setItem = function(i) {
$scope.selected_item = i;
}
Declare a object
$scope.obj={selected:null};
After that add a method in the ng repeat,
$scope.isHide = function (id) {
if (id == $scope.obj.selected)
return $scope.obj.selected = "all";
return $scope.obj.selected = id;
}
If you want to hide div, call this method with the id. Do the same thing for the li if you need.
I am using angular-drag-and-drop-lists (https://github.com/marceljuenemann/angular-drag-and-drop-lists) for my AngularJS project to create two lists that allow me to do the following:
Drag items from list A to list B
Drag items from list B to list A
Reorder items in list A
Reorder items in list B
Using a simple example on the library's site (http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/simple) I can see that these four conditions are easily achievable. However, things start to get hairy when I want to introduce slightly more complex behaviour:
When dragging items from list A to list B, I want something to occur (easily achieved with the dnd-drop callback. This currently works for me
When dragging items from list B to list A, I don't want anything to happen other than a regular drop (i.e. the item winds up in list A and nothing fancy happens). This currently works for me
When reordering items in list A, nothing should happen when an item is re-dropped into it's new position in the list (even if it is being dropped back into it's original position) This is where I am having issues - see below for further explanation
When reordering items in list B, nothing should happen when an item is re-dropped into it's new position in the list (even if it is being dropped back into it's original position). This currently works for me
I am largely adapting the sample code used in the example link provided above. The problem I am having is that the action I want to take place when moving items from list A to list B is also occurring when I reorder things in list A.
The current setup/pseudocode I have is the following:
My lists:
$scope.models.lists = {
"A": [],
"B": []
}
I populate these lists with information pulled from a database. Each item in the list also has a property that tracks how many children the item has.
My markup looks like the following:
<ul dnd-list="list"
dnd-allowed-types="['itemType']"
dnd-drop="myCallback(event, index, item, external, type, 'itemType')">
<li ng-repeat="item in list | filter:searchText"
dnd-draggable="item"
dnd-moved="list.splice($index, 1)"
dnd-effect-allowed="move"
dnd-selected="models.selected = item"
dnd-type="'itemType'"
ng-class="{'selected': models.selected === item}"
ng-dblclick="editProperties();">
{{item.label + ' - ' + item.description}}
</li>
</ul>
My callback looks like the following:
$scope.myCallback= function(event, index, item, external, type, allowedType) {
// If the item in question has no children then we don't need to do anything special other than move the item.
if (item.children.length == 0) {
return item;
}
// If moving items around list B then just move them.
for (var i = 0; i < $scope.models.lists.B.length; i++) {
if (item.label === $scope.models.lists.B.[i].label) {
return item;
}
}
// I only want this code to execute if we know we are moving from list A to list B and the item in question has > 0 children.
if (item.children.length > 0) {
// Code that I want to execute only when moving from list A to list B goes here
}
If anyone is able to assist me with this I will be very grateful.
Thanks!
If you're not totally committed to that library (which, btw, doesn't work with a touchscreen) RubaXa's Sortable is the truth: Sortable
The documentation is strong, it's quite performant, and I wish I hadn't wasted my time on other DnD libraries before this one.
Looking at the README, the dnd-drop event fires for any drop - regardless of whether it was within the same list or not.
Going by the script as you have it now, you need to check the event (or pass some additional information into your callback) to determine what list the event is firing on.
hi i have add a dragstartCallback that set variable in event.dataTransfer
for the information of the origin/source of my dragged obj:
$scope.dragstartCallback = function(event){
var id = event.target.id;
var parent = $('#' + event.target.id).closest('ul')[0].id;
var group = $('#' + event.target.id).closest('div')[0].id;
event.dataTransfer.setData("id", id);
event.dataTransfer.setData("parent", parent);
event.dataTransfer.setData("group", group);
return true;
}
And in the dropCallback i just check if is List A or List B
$scope.dropCallback = function(event, index, item, external, type, allowedType) {
$scope.logListEvent('dropped at', event, index, external, type);
var idDivSource = event.dataTransfer.getData("parent");
var idDivDestination = $(event.target).closest('ul')[0].id;
var groupSource = event.dataTransfer.getData("group");
var groupDestination = $('#' + event.target.id).closest('div')[0].id;
if(groupSource == groupDestination){
if(idDivSource != idDivDestination){
if(idDivDestination == 'IDLISTB'){
return item?
}
if(idDivDestination == 'IDLISTA'){
DO Something else
}
DO Something else
}
}
}
Html code:
<div class="associated-drag-and-drop DnD" id="GROUP-MyD&D1" ng-repeat="(listName, list) in models.lists" flex="">
<ul dnd-list="list" id="{{listName}}" dnd-drop="dropCallback(event, index, item, external, type, 'containerType')" flex="">
<md-list-item class="associated-list__item" ng-repeat="item in list | filter:searchFilter" id="{{item.id}}"
dnd-dragover="dragoverCallback(event, index, external, type)"
dnd-dragstart="dragstartCallback(event)"
dnd-draggable="item"
dnd-moved="list.splice($index, 1)"
dnd-effect-allowed="move"
dnd-selected="models.selected = item"
ng-class="{'selected': models.selected === item}"
class="noright associated-list__item"
>
CONTENT
</md-list-item>
</ul>
</div>
Do not use this type of filter "| filter:searchFilter"
But use ng-show="And put here the condition"
Alternative change the reference to $index in dnd-moved="list.splice($index, 1)"
If you dont make this change you will have a problem with the filtered list when drag and drop
Exemple
NOT FILTERED LIST
you will have 2 array in 2 ng-repeat
ListA ListB
index1 of value index2 of value
ng-repeat of list ng-repeat of list
0 aaa 0 ppp
1 bbb 1 qqq
2 ccc 2 www
3 dddaaa 3 eeerrr
4 eeeccc 4 mmmwww
ecc... ecc...
The drag and drop lists work on the index of ng-repeat
FILTERED LIST
Now if we make a filter for 'a' with "| filter:searchFilter" code
angular will make this list
List A List B
0 aaa
1 dddaaa
the index when you drag "dddaaa" will be 1 and not 3
so it will not be removed from listA when dropped in listB
becouse the index is not the same as the non filtered list
instead if you use the ng-show="condition"
it will keep the original index of list not filtered
<md-list-item ng-repeat="item in list"
ng-show="condition">
</md-list-item>
My item list:
$scope.models = {
lists: {
"LISTA": [{1},{2},{3}],
"LISTB": [{1},{2},{3}]
}
};
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.
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.