Javascript splice removing wrong items - javascript

I have following array of objects.
[{"name":"Rain"},{"name":"Storm"},{"name":"Forest"}]
Which has indexes [0, 1, 2].
I'm trying to delete the item on the given position using code:
$scope.selectedSounds.splice(index, 1);
But it is removing items wrong way, for example the last item cannot be deleted. If I'm trying to remove item with index 1, it removes item with index 2..
What can be wrong please?
I tried both ways:
$scope.removeSoundFromSelection = function(index) {
try {
// First
$scope.selectedSounds.splice(index, 1);
var indexNew = $scope.selectedSounds.indexOf(index);
console.log(indexNew);
if (indexNew > -1) {
$scope.selectedSounds.splice(indexNew, 1);
}
// Second
if ($scope.selectedSounds.hasOwnProperty(index)){
delete $scope.selectedSounds[index];
}
//delete $scope.selectedSounds[index];
} catch(e) {
$scope.showAlert();
}
};
ADDED TEMPLATE:
<div class="list">
<a class="item item-thumbnail-left" ng-repeat="sound in selectedSounds">
<img src="cover.jpg">
<h2>{{sound.name}}</h2>
<p>TEST</p>
<div class="customDeleteBtnInList">
<button ng-click="removeSoundFromSelection({{$index}})" class="button button-icon icon ion-close-circled"></button>
</div>
</a>
</div>

You are using interpolation for {{$index}} inside the ng-repeat expression removeSoundFromSelection({{$index}}). Just remove the interpolation and use only $index it will automatically be evaluated against the scope. And you just need $scope.selectedSounds.splice(index, 1).
Ideally using the interpolation there should cause parse error instead of this behavior though (Unless very old angular version, i.e < 1.2.0, is used).
Working Demo
angular.module('app', []).controller('ctrl', function($scope) {
$scope.selectedSounds = [{
"name": "Rain"
}, {
"name": "Storm"
}, {
"name": "Forest"
}];
$scope.removeSoundFromSelection = function(index) {
$scope.selectedSounds.splice(index, 1);
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div class="list">
<a class="item item-thumbnail-left" ng-repeat="sound in selectedSounds">
<img src="cover.jpg">
<h2>{{sound.name}}</h2>
<p>TEST</p>
<div class="customDeleteBtnInList">
<button ng-click="removeSoundFromSelection($index)" class="button button-icon icon ion-close-circled">Remove</button>
</div>
</a>
</div>
</div>
Even though this specific scenario in the question does not use ng-init the issue of wrong item removed can happen if you are using ng-init initialized index alias as well. Just adding that scenario as well to the answer for any future visitations on this question. i.e example:-
<a class="item item-thumbnail-left"
ng-repeat="sound in selectedSounds" ng-init="idx=$index">
....
<button ng-click="removeSoundFromSelection(idx)"
This will end up removing wrong items because ng-init'ed scope properties are not watched and updated during the digest cycle. So even if the item gets removed from DOM after splicing the array ng-inited idx will still have the old index of the item where as $index special property would have got updated to reflect the actual index. So in such cases as well use $index to pass the index instead of using cached ng-inited idx.

You are removing the item at that index twice.
Once here:
$scope.selectedSounds.splice(index, 1);
And once here:
// Second
if($scope.selectedSounds.hasOwnProperty(index)){
delete $scope.selectedSounds[index];
}
Just remove that second part and you should be fine, I can't see what you could be trying to do after that first splice line.

The following code works as expected for me, and seems to be what you are trying to achieve:
var sounds = [{"name":"Rain"},{"name":"Storm"},{"name":"Forest"}];
sounds.splice(1, 1);
console.log(sounds);
My guess is that you are (at some point) not using the correct index. Take a look at the code that creates that variable per #Alex J's answer

If you want the middle item to be deleted, index should equal 1. It's possible that whatever logic you are doing is giving you the wrong value for index
*Edit: After seeing your updated code, it looks like you are splicing twice. You are doing it the first time in the try statement, and then it goes to the if statement where that will also be true. If you are trying to write a function to just splice out an object at a given index, you could do:
$scope.removeSoundFromSelection = function(index) {
if($scope.selectedSounds[index]){
$scope.selectedSounds.splice(index, 1);
}
}

var season = [{"name":"Rain"},{"name":"Storm"},{"name":"Forest"}];
var seasoned= season.slice(0, 2);
console.log(seasoned); //it sliced rain and storm...

Related

Relevant item not removing Angular 12

Tried to remove an item from the array but it's removing the last index of that array, not the relevant one, below is my code. Ex: if I remove the first item from the list then it's removing the last item from the list.
component.ts code
this.items.splice(this.items.indexOf(id));
html code
<span class="close" style="cursor: pointer;" (click)="removeItems(item.Id)">
<i class="far fa-trash-alt fa-2x" style="color: red;"></i>
</span>
List items
I'm using *ngFor loop to get my items. I didn't put that code here.
If you pass negative value to splice the function will remove element(s) from the end.
You need to pass number of items to be removed. So you are looking for this : this.items.splice(this.items.indexOf(id), 1);
If items is a collection of objects then using indexOf with just the id is likely to return -1 every time, which would explain why the last item is being removed.
You would probably need something like
const itemToRemove = this.items.findIndex(i => i.id === id);
if(itemToRemove >= 0) {
this.items.splice(itemToRemove, 1);
}

Angular $watch with array splice

I've got a problem regarding watcher in Angular and array splice. Combined both of them have a really strange behaviour. So i created a small demo with logs showing the problem that's happening. Here is the code I'm having or you can see it working in this Plunker.
html:
<div class="logs">
<div><b>LOGS:</b></div>
<ul ng-repeat="watchEntry in watchersLogs track by $index">
<li>{{watchEntry}}</li>
</ul>
<div><b>Watcher Count:</b> {{watchers.length}}</div>
</div>
<div class="item" ng-repeat="item in list" ng-init="InitItem()">
<div class="item-title-wrapper">
<span class="item-title">Item {{ $index + 1 }}</span>
<button ng-click="AddNewItem()">Add New</button>
<button ng-click="RemoveItem()">Remove</button>
</div>
<div class="field">
<div>
CREDIT:
<input type="number" name="credit" ng-model="item.credit" />
</div>
<div>
DEBIT:
<input type="number" name="debit" ng-model="item.debit" />
</div>
</div>
</div>
js:
var app = angular.module("listApp", []);
app.controller("listController", function($scope) {
// list with all data:
$scope.list = [{
credit: 2000,
debit: 0
}, {
credit: 100000,
debit: 1000
}];
// list containing all watchers:
$scope.watchers = [];
// logs containing all watcher event:
$scope.watchersLogs = [];
function SetWatcher(itemIndex) {
$scope.watchers.splice(itemIndex, 0, $scope.$watch("list[" + itemIndex + "]", function(newValues, oldValues, scope) {
$scope.watchersLogs.push("Item " + itemIndex + " watcher fired!");
}, true));
}
$scope.InitItem = function() {
// set a watcher for newly create item:
SetWatcher(this.$index);
}
$scope.AddNewItem = function() {
var newItem = {
credit: 0,
debit: 0
};
// put the item into the array:
$scope.list.splice((this.$index + 1), 0, newItem);
};
$scope.RemoveItem = function() {
// destroy the watcher:
$scope.watchers[this.$index]();
$scope.watchers.splice(this.$index, 1);
// remove the item from the list:
$scope.list.splice(this.$index, 1);
};
});
css:
.logs {
margin-bottom: 50px;
}
.item {
margin-bottom: 25px;
}
So, as you can see there, when I'm initializing or adding a new item into the $scope.list array, I'm assigning to it a new watcher. From the logs you can see that each watcher is fired at start-up only once which is fine. However, my application needs the item to be added after the one you selected or currently reviewing /e.g. if you have 3 items and click the Add New button for the 2nd one, it should add a new entry on 3rd place/. That's why I'm using splice.
However, this is causing $watch expression to be executed multiple times sometimes. So, if you add a new entry in the last place it will fire the $watch expression only once for it. That's okay. But, if you add it somewhere in the middle /e.g. 2nd, 3rd, etc./, the watcher expression is going to be fired not only for it, but for all other items that are after it as well. I'm guessing that splice somehow changes the reference of the items after the newly created one, that's why the $watch is executed when that happens.
I need the $watch expressions as I'm doing some calculations when values are being changed, so I think I can't get rid of them. I need the splice functionality too, as I mentioned before... So, any ideas how to solve this?
Thanks in advance! :)
...UPDATE...
I resolved the problem! Big thanks to #Deblaton Jean-Philippe for the help. SO, first of all I created a new function for removing the watchers:
function RemoveWatcher(itemIndex) {
if ($scope.watchers[itemIndex]) {
// destroy the watcher:
$scope.watchers[itemIndex]();
// remove it from the array as well:
$scope.watchers.splice(itemIndex, 1);
}
}
I'm calling it before setting up a new watcher in order to be sure that it's cleared. I;m calling it inside the RemoveItem method as well, but the tricky thing here is that I'm always removing the last watcher entry from the array, as I'm splicing the $scope.list array and it changes the order of the fields. SO what I'm doing there is just calling the RemoveWatcher method like so:
RemoveWatcher($scope.watchers.length - 1);
This seems to be working perfectly! You can check the updated Plunker as well. :)
The code is doing what you've asked him to do. He is watching the value at the index you've asked.
If you add a new watch on and index already registered, it will be triggered twice.
If you look at the documentation here, you will see that $watch() returns a function. Executing this function will unregister your watch.
What you need to do is, when you add a new item, is unregistering the watch that is there.
Maybe something like this could help you :
function SetWatcher(itemIndex) {
if($scope.watchers[itemIndex]) $scope.watchers[itemIndex]();
$scope.watchers.splice(itemIndex, 0, $scope.$watch("list[" + itemIndex + "]", function(newValues, oldValues, scope) {
$scope.watchersLogs.push("Item " + itemIndex + " watcher fired!");
}, true));
}

What is the most direct way to determine that the inner element should be displayed in AngularJS?

I have a JSON structure which represents as hierarchical elements.
It looks like the following:
{
"url":"http://docsetups.json",
"partnerId":1,
"fieldDefs":
[
{"roleName":"Make","roleId":1,
"children":[{"roleName":"Invoice Number","roleId":11}]
},
{"roleName":"Model","roleId":2,
"children":[
{"roleName":"Manufacturer","roleId":21},
{"roleName":"EquipmentCode","roleId":22},
{"roleName":"EquipmentSSN","roleId":23}
]
}
]
}
Plunker
I've have created a plunker at: http://plnkr.co/edit/betBR2xLmcmuQR1dznUK?p=preview
I am using ng-repeat to display this in elements as a hierarchy of elements like the following:
When I click on either element the entire structure expands and looks like the following:
The code which renders the DOM is nice and easy and looks like the following:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.isVisible"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}
</div>
</div>
vm.isVisible
The thing to focus on here is the subitem which has the ng-show="vm.isVisible" so that it only displays if that value is true.
Show Only The Subitem of the Clicked Parent
However, I'd like to only display the subitem when its parent item is clicked -- instead of showing all subitems like it does now. Can someone offer a good way to do this? I'm hoping to do it without a directive, because I am interested in whether or not this is possible without a directive or if the code is terribly convoluted in that case.
If you have a solution which includes creating a directive, please keep it as simple as possible. Thanks.
I think you should define a flag for every item which determine if the item is open.
Then you pass the item itself into handler:
data-ng-click="vm.onClick(item)
after that - you simply need to invert isOpen flag:
function onClick(item)
{
item.isOpen = !item.isOpen;
}
The whole view snippet:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item)" data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="item.isOpen" data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
The plunker: http://plnkr.co/edit/N8mUZaVfmLpnlW4kxzSr?p=preview
#Oleksii You're answer is very close and it did inspire me to develop the following answer so I appreciate your input and I did upvote you. However, there's a bit more to it than what you gave me.
View Solution at Plunker
I forked the previous plunker and you can see the final solution at:
http://plnkr.co/edit/QvyHlLh83bEyvlNkskYJ?p=preview
No Directive Required
Now I can click either or both element and they expand independently. Here's the sample output:
It took a bit of thinking, but what I did first was create a new type which holds a roleName (consider it unique) and a isVisible boolean. I call that type visibleItem and it looks like this:
var visibleItem = function (roleName){
this.isVisible = false;
this.roleName = roleName;
};
After that I created an array to hold all the visibleItems (1 for each node):
var visibleItems = [];
Now when I load the json I go ahead and create 1 visibleItem object for each node and push it into the visibleItems array.
$http.get('items.json')
.success(function(data, status, header, config) {
vm.documentSetups=data;
for (var x = 0; x < vm.documentSetups.fieldDefs.length; x++)
{
visibleItems.push(new visibleItem(vm.documentSetups.fieldDefs[x].roleName));
}
})
They are "keyed" by their roleName (consider it unique).
Next, I had to write two helper methods (setVisibleItem and getVisibleItem)
function setVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
visibleItems[x].isVisible = !visibleItems[x].isVisible;
}
}
}
function getVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
return visibleItems[x].isVisible;
}
}
return false;
}
Wire Up The Helper Methods
Finally, I wire up the setVisibleItem to the ng-click of the element and I wire up the getVisibleItem to the ng-show directive.
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.getVisibleItem(item.roleName)"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
Summary Of How It Works
Basically each of those just iterates through the list and checks to insure if the roleName sent in matches the roleName of the item. If it does it sets or gets the value.
Solved Without a Directive and Not Bad
It's a lot more work than you think it'll be, but I didn't have to implement a directive and the code is still fairly basic.

How to use ng-if with ng-repeat?

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.

Find row index using jQuery

<div class="parent">
<div class="formRow js-TextBox"><a></div> <------ index 0
<div class="formRow js-Image"><a></div> <------ index 1
<div class="formRow js-TextBox"><a></div><------ index 2
<div class="formRow js-ImageList"><------ index 3
<div class="formRow js-Image"><a></div> <-- **This should return 3 but is returning 4 because I search index on the basis of formRow**
</div>
Code to find the formRow index
var parent = $(this).parents("div.formRow");
var rowIndex = parent.parent().find("div.formRow").index(parent);
Please refer to above code and advise how I can find the index of formRow for which I have clicked on anchor element a.
The problem is that the last form row contains another formRow, but I want the index of parent form row.
I'd remove ambiguity and provide the row number to the element at render time, i.e.
<div class="formRow js-TextBox" id="row<%= rownumber %>"><a></div>
Or as the ID to the anchor:
<div class="formRow js-TextBox"><a id="row<%= rownumber %>"></div>
There are a multitude of ways to approach this. I always find that if something seems to hard then I'm approaching it from the wrong direction (or just plain wrong).
Can you modify the above - I'd add another css class - primaryRow to the first three and use that instead of formRow in the jQuery expression
OK - The run this code:
$('.parent > div').addClass('primaryRow');
to add the class.
Then
var rowIndex = $('.primaryRow').index($(this).parents().find('.primaryRow'));
This works.
$('a').click(function(){
var i = $(this).parents('.parent > .formRow').index('.parent > .formRow');
alert(i);
return false;
});
Check it out: jsfiddle

Categories

Resources