How to update $index when list changes in angular - angularjs

I'm trying to implement a keyboard navigation in a multi-level List.
Therefor i try to give every Item in the List a unique ID, like 5.2.1(category.item.subitem).
I already tried a lot of stuff, like ng-init the index to a variable on ng-repeat, an other approaches using directives, but all had the problem so far that when i change the list (delete items for example) my Custom Index doesn't update!
I made a simple Plnkr here:
You can delete an item or subitem and see that the "real" index and the customindex don't relate.
http://plnkr.co/edit/BsRCNAqM7jWkeAJ9rkLN?p=preview
at the moment i have a custom directive called customIndex that gets the Index as attribute.
<li ng-repeat="subitem in item.subitem" custom-Index="$parent.$index+'.'+$index">
and inside the directive i simply $eval the attribute:
.directive('customIndex', function(){
return{
restrict:'A',
link: function(scope, el, attrs){
scope.myIndex = scope.$eval(attrs.customIndex);
}
}
})
But this, like all other solutions i tried, doesn't work.
I think this must be a common kind of problem.
Does anyone have any suggestions for me?
THANKS
Markus

I think you overcomplicated yourself. Try only this piece of markup and no need for custom directive or something else:
<ul>
<li ng-repeat="item in data">
"Real"-index:{{$index}} | myIndex:{{myIndex = $index}}} | Item:{{item.itemName}} <button ng-click="data.splice(data.indexOf(item),1)">Del</button>
<ul>
<li ng-repeat="subitem in item.subitem">
"Real"-index:{{$index}} | myIndex:{{myIndex = $parent.$index+'.'+$index}} | Subitem:{{subitem}} <button ng-click="item.subitem.splice(item.subitem.indexOf(subitem),1); ">Del</button>
</li>
</ul>
</li>
</ul>
ng-init will never work for you because it is executed only when directive is compiled. With custom directive it will never work because the scope item will be of the item that you delete and you dont' have access to the entire list.
Here is the demo:
http://plnkr.co/edit/Nr4cJ2m3JqI8NyLFnQhC?p=preview

so, in the meantime i found a solution. Maybe not the best, but it works.
i could attach the needed parameters to a directive attribute and set a $watch on this attribute to stay synchronised with any changes caused by deleting/filtering etc.
for example:
<div item-Index="[$parent.$index,$index,-1,$first, $last]"</div>
and the directive hast the following function in the link function:
scope.$watch(attrs.itemIndex, function(value) {
scope.itemIndex = value;
});
now every item has a unique index defined.
The multilevel keyboard navigation also works now, but this is beyond the scope of this question.

Related

Closing modal in Angular UI Bootstrap while erasing array of 1000 items, takes a significant time

I've an Angular application where I pass an empty array to Angular-UI Bootstrap Modal. Depends on user choice it may be filled or not with items, up to 1000 objects. All them I display in a list with ng-repeat. All items in a list are pre-generated, so I can't retrieve them asynchronously for example by using plugin like smart-table.
When user press ok button all generated values returned to controller from which it was called. Technically all them already there because I pass $scope.items by reference:
resolve: {
items: function () {
return $scope.items;
}
}
But when I press cancel button I'm erasing all items in an array by invoking $scope.items.length=0; My problem that it takes a while. In my plunker example below it's barely noticeable but noticeable but in my actual application it's much more noticeable and unacceptable.
My guess that It's due to Angular's two-way binding, it takes a time to clean all watchers but I don't know how to solve this issue, if it can be solved.
My MCVE at plunker here: http://plnkr.co/edit/JwanDxBzh3a7ilEX58z8?p=preview
UPDATE: Tried to use one-time binging, plunker here: http://plnkr.co/edit/PjzHRYiuXFHE1M1Pap6U?p=preview
<li ng-repeat="item in items">
{{ ::item | date:'yyyy-MM-dd' }}
</li>
as described here: https://stackoverflow.com/a/18791503/947111 it didn't help.
Ok, I was right and I was almost there with Angular's one-time binding I just used it in a wrong place, and should be used in a ng-repeat in a such way:
<li ng-repeat="item in ::items">
{{::item | date:'yyyy-MM-dd' }}
</li>
Thanks to this answer: https://stackoverflow.com/a/23903690/947111
Working plunker located here: http://plnkr.co/edit/XWi6Z0eCXveV58WJfpo6?p=preview

NG-Repeat doesn't update cleanly when scope is updated

So I have a list of workspaces:
<li class="workspace-object" ng-repeat="w in workspaces | filter:searchQuery" ng-click="selectWorkspace(w)">
{{w.name | titleCase }}
</li>
That List gets updated in the database by some function, and then I call the loadWorkspaces function below, to re-populate the $scope.workspaces object
$scope.loadWorkspaces = function() {
APIService.getWorkspaces().then(function(data){
$scope.workspaces = data;
});
}
When this happens, the $scope.workspaces, when logged out, reads the right updated information immediately, however, the ng-repeat DUPLICATES, before it updates to the proper list. I have no idea why this happens. Any ideas?
In this example, I am updating the workspace title, and that update runs the loadworkspaces function.
Try this ...
w in workspaces track by $index
When the contents of the collection change, ngRepeat makes the corresponding > changes to the DOM:
When an item is added, a new instance of the template is added to the DOM.
When an item is removed, its template instance is removed from the DOM.
When items are reordered, their respective templates are reordered in the DOM.
By default, ngRepeat does not allow duplicate items in arrays. This is because when there are duplicates, it is not possible to maintain a one-to-one mapping between collection items and DOM elements.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
For example, you may track items by the index of each item in the collection, using the special scope property $index
Accepted answer did not help me (Using Angular 1.1.10), my repeat was still updating delayed. I struggled for ages before I got it working.
Instead of repeating my custom directive like so;
<my-directive ng-repeat="item in myarray track by item.id" some-binding="item"></my-directive>
I (magically) got it working after moving the ng-repeat to a (non-directive) parent element, like so:
<div ng-repeat="item in myarray track by item.id">
<my-directive some-binding="item"></my-directive>
</div>

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

AngularJS - parent scope is not updating

I'll try to explain the problem with the example. Example is simplified, so it's not very logical, but anyway..
Let's say I have such view
<div ng-app="TestApp" ng-controller="MyController">
<button ng-add-item="items">Add Item</button>
<ul>
<li ng-repeat="item in items">{{item.name}} <button ng-change-item="item">Change item</button></li>
</ul>
</div>
And I have two directives:
1) ngAddItem - which adds dummy item (after click)
2) ngUpdateItem - which changes item (after click)
The problem is that 1) works great, but second - not..
http://jsfiddle.net/pbmnL/2/
P.S. I know I can use ng-click and many other things in this example. This is just simplified example.
Thanks for a help!..
UPDATE: If I change just a name property everything works fine!
http://jsfiddle.net/pbmnL/4/
But in real example I still need to change entire object..
Your code does not work because with this line:
_item = _changed_item;
you just point local variable _item to another object but do not update the item in the list.
As a solution you may use angular.copy - it replaces all properties of one object with properties from another one.
angular.copy(_changed_item, _item);
Here is jsfiddle that works.
Do this:
$element.bind('click', function () {
$scope.$parent.item.name = 'changed name';
$scope.$apply();
});

Shouldn't there be an AngularJS ngWith directive?

Maybe I'm crazy, or too used to KnockoutJS, but I keep looking for an ngWith directive in the docs to define the scope on an element, in a controller, or for an included (ngInclude) partial.
For example:
I'd like to write a controller that augments MyItem like:
MyModule.controller('MyItemCtrl', function($scope) {
$scope.doSomethingToItem = function() {
$scope.name = "bar";
};
});
Or a view/template for MyItem like:
<div ng-controller="MyItemCtrl">
{{name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
But in both of these cases I'm imagining my $scope to be prototypically inherit from my model, MyItem.
But the scope doesn't inherit from the model!!
Which baffles me.
Instead, my model is a property on the scope.
In the case of a repeater:
<div ng-repeat="item in list">
<div ng-controller="MyItemCtrl">
{{item.name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
</div>
which means everywhere I have to use item.this or item.that instead of just this and that. I have to remember which functions are native to the model, and which were applied directly to the scope by a controller.
If I want to have a partial to display names (stupid example, I know, but you get the idea):
<h3>{{name}}</h3>
I have to write it
<h3>{{item.name}}</h3>
and then ensure the model is always item. Usually by wrapping it in a directive simply to defines a scope with an item property.
What I often feel like I want to do is simply:
<div ng-include="'my/partial.html'" ng-with="item"></div>
or
<div ng-repeat="list" ng-controller="MyItemCtrl">
{{name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
Is there some magical directive out there that I haven't found? Or am I completely wrong and just looking for trouble?
Thanks.
EDIT:
Many thanks to Brandon Tilley for explaining the dangers of using scopes as models. But I still often find the need for some quick declarative scope manipulation and wish for an ng-with directive.
Take, for example, you have a list of items which, when clicked, shows an expanded view of the selected item. You might write it something like:
<ul>
<li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div ng-controller="ItemController">
{{selection.maxView}}
</div>
now you have to get properties of the selected item using selection.property rather than what I'd want: item.property. I'd also have to use selection in ItemController! Making it wholly coupled with this view.
I know, in this simple example I could have a wrapping controller to make it work, but it illustrates the point.
I've written a very basic with directive:
myApp.directive('with', ['$parse', '$log', function(parse, log) {
return {
scope: true,
link: function(scope, el, attr) {
var expression = attr.with;
var parts = expression.split(' as ');
if(parts.length != 2) {
log.error("`with` directive expects expression in the form `String as String`");
return;
}
scope.$watch(parts[0], function(value) {
scope[parts[1]] = value;
}, true);
}
}
}]);
that simply creates a new scope parsing one expression onto another value, allowing:
<ul>
<li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div with="selection as item" ng-controller="ItemController">
{{item.maxView}}
</div>
This seems infinitely useful to me.
Am I missing something here? Just making trouble for myself down the line somehow?
I've found I can just put an array around the source of ng-repeat and it becomes functionally an ng-with. :)
<div ng-repeat="name in [vm.dto.Person.Name]" >
<input type="text" ng-model="name.First" />
<input type="text" ng-model="name.Last" />
</div>
Seems like some purests may not like this... Also doesn't seem like there is a good alternative.
This is a great question. I can see how this may be confusing coming from another front-end framework, but in Angular, the scope has a reference to the model, and the syntax you're describing is normal. I personally like to describe the scope as more like a view model.
Miško Hevery, the author of AngularJS, does a good job of explaining this concept in this video, starting at about the 30 minute mark and lasting about 3 minutes:
People oftentimes think that the scope is the model, and that's not the case. Scope has references to the model. [...] So in the view, you say model dot whatever property you want to access.
While it may be possible to write an ngWith directive that does kind-of what you're looking for, since Angular uses prototypal inheritance for scopes, you will likely run into the same issues that Miško describes in the aforementioned video at 31:10 (where you think you're updating a value on the parent scope but you're actually not). For more details on prototypal inheritance in AngularJS, check out the excellent article The Nuances of Scope Prototypal Inheritance on the AngularJS wiki.
Another approach would be to set a new variable via ng-init:
<div ng-init="name = vm.dto.Person.Name" >
<input type="text" ng-model="name.First" />
<input type="text" ng-model="name.Last" />
</div>
I think one side effect of this when coming from knockout is that is encourages you to create more components with less responsibility, and of course within a component the 'this' is the viewmodel for that component. If you get to a point where it is really annoying you then it might be a sign you need a new component.
And it sure is nice being able to just refer to something in the component view model without thinking about the scope you're in. And you can't tell me you miss $parents[2] and $root ;-)
PS. Yes I know you can use let in knockout, which means fewer $ stuff.
I definitely miss knockout when doing Angular stuff (I still use both) but it sure is nice to do certain things things like title="Customer #{{ row.customerId }} - {{ row.fullName }}" and not have everything under data-bind.

Resources