I have a weird problem where for some reason splice always deletes last element from array, even though the alert gives correct index.
The onRemove() method is what does the removing.
<button (click)="onAdd()">Add</button>
<ul>
<li *ngFor="let course of courses; index as i; even as isEven">
{{ i }} - {{ course.name }} <span *ngIf="isEven">(EVEN)</span>
<button (click)="onRemove(course)">Remove</button>
</li>
</ul>
export class AppComponent {
courses = [
{ id: 1, name: 'course1' },
{ id: 2, name: 'course2' },
{ id: 3, name: 'course3' },
];
onAdd() {
this.courses.push({ id: 4, name: 'course4' });
}
onRemove(course) {
let index = this.courses.indexOf(course);
alert(index); // I get correct index here
this.courses.splice(index, 1);
}
}
The logic is correct. I think the angle you are looking at it could be the problem. It is removing the correct element when you click on it.
For example, consider that you have 3 elements in the array with the following contexts index starting from 0.
0 - course1
1 - course2
2 - course3
When You remove element with index 1, the total number of elements becomes two and as a result the index changes as well.
0 - course1
1 - course3
So this can get you to think that it is always deleting the last element whereas in reality it is just shifting the position of the array.
The following code could be what you are trying to achieve. Just change the first string interpolation to {{course.id}} instead of {{i}}
<li *ngFor="let course of courses; index as i; even as isEven">
{{ course.id }} - {{ course.name }} <span *ngIf="isEven">(EVEN)</span>
<button (click)="onRemove(course)">Remove</button>
</li>
Hope this helps
Related
I have the following data structure, and I'm trying to render each object individually on click whiteout overwriting the previous value with the current value.
boardCollection =
[
{
id: 1,
dashboardType: "Simple",
fields: [
"Board naspa",
"Cea mai mare mica descriere"
]
},
{
id: 2,
dashboardType: "Simple",
fields: ["Titlu fara idei", "Descriere in speranta ca se va afisa"]
},
{
id: 3,
dashboardType: "Complex",
fields: ["Primu board complex", "descriere dorel", "Hai ca merge cu chiu cu vai"]
},
{
id: 4,
dashboardType: "Complex",
fields: ["Kaufland", " merge si asta ", "aaaaaaaaaaaaaaaaaaaaaaaaaa"]
}
]
in which I am accessing the elements in the following manner ->value/index are defined globally.
display() {
let currentElement = this.boardCollection[this.index]
this.value = currentElement;
if (this.index < this.boardCollection.length - 1) {
this.index++;
} else {
this.index = 0;
}
}
Here is the HTML and the way that i`m trying to render each object.
<div *ngIf="show">
<h1>{{value.dashboardType}}</h1>
<ol *ngFor="let prop of value.fields |keyvalue">
<li>{{prop.value }}</li>
</ol>
</div>
<button (click)="display()">Show</button>
show is set to true in the display method.
What I have achieved so far is to display each object or the properties from them, but each time the button is pressed, the current value will overwrite the previous value, therefore I'm looking for some help into saving the previous value in order to display each object so in the end to have all the objects from the Array rendered to the UI
I would have another array in the TypeScript and keep adding to this array as display is clicked.
boards = [];
...
display(index: number) {
let currentElement = this.boardCollection[this.index]
this.value = currentElement; // might not be needed
this.boards = [...this.boards, ...currentElement]; // append to this.boards immutably so change detection takes effect (this.boards.push won't force change detection)
if (this.index < this.boardCollection.length - 1) {
this.index++;
} else {
this.index = 0;
}
}
...
<div *ngFor="let board of boards>"
<h1>{{board.dashboardType}}</h1>
<ol *ngFor="let prop of board.fields |keyvalue">
<li>{{prop.value }}</li>
</ol>
</div>
<button (click)="display()">Show</button>
Clicking on Show each time should keep on displaying each one one by one.
I am trying to build a custom element to manage simple lists, renaming the items and changing their order. Unfortunately I'm noticing some weird behavior that's actually really hard to pin down.
Typing into the inputs does not appear to be recognized as changes for Aurelia to update the item
When typing/changing one item after page load and then changing its position in array via those methods, the index of the item seems lost (turns into -1). If the item isn't changed via input field, the index in the array is recognized correctly and sorting works.
Are there any known issues with arrays, binding and maybe even child elements? What are the battle tested approached to get the desired behavior? Thanks a lot!
Parent Element
...
<list items.bind="list"></list>
...
List Element
<template>
<div class="input-group" repeat.for="item of items">
<input value.bind="item" class="input" type="text" placeholder="Item" autofocus>
<a click.delegate="deleteItem(item)">X</a>
<a click.delegate="moveItemUp(item)">^</a>
<a click.delegate="moveItemDown(item)">v</a>
</div>
<a click.delegate="addItem()">Add Item</a>
List JS
export class List {
#bindable({defaultBindingMode: bindingMode.twoWay}) items;
constructor() {}
addItem() {
this.items.push('new')
}
deleteItem(item) {
let i = this.items.indexOf(item)
this.items.splice(i, 1)
}
moveItemUp(item) {
let i = this.items.indexOf(item)
if (i === 0) return
let temp = item
this.items.splice(i, 1)
this.items.splice(i - 1, 0, temp)
}
moveItemDown(item) {
let i = this.items.indexOf(item)
if (i === this.items.length) return
let temp = item
this.items.splice(i, 1)
this.items.splice(i, 0, temp)
}
}
repeat.for has several contextual variables you can leverage of. [Documentation]
Gist demo: https://gist.run/?id=1c8f78d8a774cc859c9ee2b1ee2c97f3
Current item's correct position can be determined by using $index contextual variable instead of items.indexOf(item).
Databound values of inputs will be preserved by passing item to items.slice(newIndex, item).
If you need to observe array changes, CollectionObserver could be a great fit for that. More details here: Observing Objects and Arrays in Aurelia.
list.js
import { bindable, bindingMode } from 'aurelia-framework';
export class List {
#bindable({defaultBindingMode: bindingMode.twoWay}) items;
constructor() {}
addItem() {
this.items.push(`New Item ${this.items.length + 1}`);
}
deleteItem(i) {
this.items.splice(i, 1);
}
moveItemUp(i, item) {
if (i === 0)
return;
this.moveItem(i, i - 1, item);
}
moveItemDown(i, item) {
if (i === this.items.length - 1)
return;
this.moveItem(i, i + 1, item);
}
moveItem(oldIndex, newIndex, item) {
this.items.splice(oldIndex, 1);
this.items.splice(newIndex, 0, item);
}
}
list.html
<template>
<div class="input-group" repeat.for="item of items">
<input value.bind="item" class="input" type="text" placeholder="Item" autofocus> |
<a click.delegate="deleteItem($index)"><i class="fa fa-close"></i></a> |
<a click.delegate="moveItemUp($index, item)"><i class="fa fa-arrow-up"></i></a> |
<a click.delegate="moveItemDown($index, item)"><i class="fa fa-arrow-down"></i></a>
</div>
<a click.delegate="addItem()">Add Item</a>
</template>
I believe this has to do with the immutability of strings. That is, strings can't be modified, so when you modify a value in the textbox, the array element is actually replaced instead of modified. That's why you're losing the binding.
Here's a gist that demonstrates it working correctly when binding to a list of objects.
https://gist.run/?id=22d186d866ac08bd4a198131cc5b4913
I'm comparing 2 integers in a loop which loops through an array which contains JSON objects. The JSON objects contain an ID and a Name. When I click on the close button of my window that window should disappear and the others should remain but what happens is that if I close the window with the highest ID all of them get close and if for example I close the fifth one the #1-5 gets closed but the higher IDs remain.
ctrl.items = [{
id: 1,
name: "Number one"
},
{
id: 2,
name: "Number two"
},
{
id: 3,
name: "Number three"
}];
ctrl.removeWindow = function(toBeRemovedId) {
console.log("Called: " + toBeRemovedId);
for (var i = ctrl.items.length - 1; i >= 0; i--) {
console.log(ctrl.items[i].id + " " + toBeRemovedId);
if(ctrl.items[i].id == toBeRemovedId) {
ctrl.items.splice(ctrl.items[i], 1);
}
};
}
<div class="window show" id="item_window" ng-repeat="item in itemWindowCtrl.items">
<hgroup>
<h1>{{ item.name}}</h1>
<button class="close" ><span class="fa fa-times fa-lg" ng-click="itemWindowCtrl.removeWindow(item.id)"></span></button>
</hgroup>
<section>
<p>{{ item.id + " " + item.name }}</p>
</section>
</div>
Thanks in advance for any clues you might be able to bring me.
The array splice function takes the index, rather than the item to remove, so you should just be doing:
ctrl.items.splice(i, 1);
The looping is a bit unnecessary though. Why don't you just pass through the item to be removed rather than looping through in descending id order to find matching, id? For example:
ng-click="itemWindowCtrl.removeWindow(item)"
Then in your controller / directive:
ctrl.items.splice(ctrl.items.indexOf(item), 1);
change the for loop to get the index and then just remove that index
var index = -1;
for (var i = 0; i < ctrl.items.length; i++) {
if(ctrl.items[i].id === toBeRemovedId) {
index = i;
}
};
ctrl.items.splice(index, 1);
I have a products page that I want to show 3 items in each row and then if it has more, create a new row and show more. So 3 cols per row with unlimited rows. Below is the code that I have that contains my loop which I assume the code will need to go into.
$(data).find('products').each(function() {
itemName = $(this).find('itemName').text();
itemDesc = $(this).find('itemDesc').text();
itemID = $(this).find('id').text();
items +='<div class="row-fluid">\
<div class="span3">Col 1</div>\
<div class="span3">Col 2</div>\
<div class="span3">Col 3</div>\
</div>';
count++;
});
Here is where I need to do it but I am a little stuck on how to approach this. If the count is dividable by 3 I assume it will then need to create a new row.
Thanks for any help or input you can provide.
First of all, no need to handle a count variable on your own, the .each() function already supplies an index element (as an optional argument).
With the modulus operator you can get the remainder from dividing the index by 3. Then you can tell when do you need to print the opening of the row and the ending of it.
$(data).find('products').each(function(index) {
itemName = $(this).find('itemName').text();
itemDesc = $(this).find('itemDesc').text();
itemID = $(this).find('id').text();
if ((index % 3) == 0) items += '<div class="row-fluid">';
items += '<div class="span3">Col 1</div>';
if ((index % 3) == 2) items += '</div>';
});
if (items.substr(-12) != '</div></div>') items += '</div>';
Going left field, don't! Use CSS instead.
Style up your span3 class to have a with of 30ish % with a display of inline block. That way when you decide to display 2, 4 or 60 per row you only need to change the CSS. This also opens you up to change the number of items per row with CSS media queries for diferent viewports e.g. mobile.
Further more this way you don't need to worry about closing off the row when your items returned aren't divisible by 3
On a side note, if you decide to go the CSS route, consider using <ul> and <li> instead, as semanticaly you have a list.
http://jsfiddle.net/UKQef/1/
Update Fiddle updated to demonstrate use of li and the flexibility of this approach.
You should use modulus: http://msdn.microsoft.com/en-us/library/ie/9f59bza0(v=vs.94).aspx
It gives you a remainder back from dividing two numbers so you could probably do this with something like (inside your .each):
if(!($(this).index() % 2)){
// Add your row
}
$(this).index() returns the index of your .each() and the % 2 returns the remainder of that index divided by 2 so the first 3 times this runs it'd be like this:
0 / 2 = 0 = add a row
1 / 2 = .5 = don't add a row
2 / 2 = 1 = don't add a row
Hopefully this is what you meant.
Here's what I think is a cleaner approach:
// Map each product to a cell.
var cells = $(data).find('products').map(function() {
var itemName = $(this).find('itemName').text();
var itemDesc = $(this).find('itemDesc').text();
var itemID = $(this).find('id').text();
return $('<div></div>').addClass('span3').text(itemName+' '+itemDesc+' '+itemID);
});
// Collect the cells into rows.
var rows = [];
for (var i=0, j=cells.length; i<j; i+=3) {
rows.push(
$('<div></div>')
.addClass('row-fluid')
.append(cells.slice(i,i+3))
);
}
The best approach to your issue is using jquery template. you can fetch your data in Json format by ajax request and create rows dynamically by jquery template:
<script src="jquery.tmpl.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
var data = [
{ name: "Astor", product: "astor", stocklevel: "10", price: 2.99},
{ name: "Daffodil", product: "daffodil", stocklevel: "12", price: 1.99},
{ name: "Rose", product: "rose", stocklevel: "2", price: 4.99},
{ name: "Peony", product: "peony", stocklevel: "0", price: 1.50},
{ name: "Primula", product: "primula", stocklevel: "1", price: 3.12},
{ name: "Snowdrop", product: "snowdrop", stocklevel: "15", price: 0.99},
];
$('#flowerTmpl').tmpl(data).appendTo('#row1');
});
</script>
<script id="flowerTmpl" type="text/x-jquery-tmpl">
<div class="dcell">
<img src="${product}.png"/>
<label for="${product}">${name}:</label>
<input name="${product}" data-price="${price}" data-stock="${stocklevel}"
value="0" required />
</div>
</script>
Html:
<div id="row1" class="drow"></div>
var $products = $(data).find('products');
var num_of_rows = Math.ceil($products.length/3)
//Create your rows here each row should have an id = "row" + it's index
$products.each(function(i,val){
var row_index = Math.ceil(i/3)
$('#row' + row_index).append("<div>Col"+i%3+"</div>")
});
Something like that should work
I'll preface this by saying I am very new to AngularJS so forgive me if my mindset is far off base. I am writing a very simple single page reporting app using AngularJS, the meat and potatoes is of course using the angular templating system to generate the reports themselves. I have many many reports that I am converting over from a Jinja-like syntax and I'm having a hard time replicating any kind of counter or running tabulation functionality.
Ex.
{% set count = 1 %}
{% for i in p %}
{{ count }}
{% set count = count + 1 %}
{% endfor %}
In my controller I have defined a variable like $scope.total = 0; which I am then able to access inside of the template without issue. What I can't quite figure out is how to increment this total from within an ng-repeat element. I would imagine this would look something like -
<ul>
<li ng-repeat="foo in bar">
{{ foo.baz }} - {{ total = total + foo.baz }}
</li>
</ul>
<div> {{ total }} </div>
This obviously doesn't work, nor does something like {{ total + foo.baz}}, thanks in advance for any advice.
If all you want is a counter (as per your first code example), take a look at $index which contains the current (0 based) index within the containing ngRepeat. And then just display the array length for the total.
<ul>
<li ng-repeat="item in items">
Item number: {{$index + 1}}
</li>
</ul>
<div>{{items.length}} Items</div>
If you want a total of a particular field in your repeated items, say price, you could do this with a filter, as follows.
<ul>
<li ng-repeat="item in items">
Price: {{item.price}}
</li>
</ul>
<div>Total Price: {{items | totalPrice}}</div>
And the filter function:
app.filter("totalPrice", function() {
return function(items) {
var total = 0, i = 0;
for (i = 0; i < items.length; i++) total += items[i].price;
return total;
}
});
Or, for improved reusability, a generic total filter function:
app.filter("total", function() {
return function(items, field) {
var total = 0, i = 0;
for (i = 0; i < items.length; i++) total += items[i][field];
return total;
}
});
Which would be used like:
<div>Total price: {{items | total:'price'}}</div>
I needed running total rather that plain total, so I've added upon what #TimStewart left. Here the code:
app.filter("runningTotal", function () {
return function(items, field, index) {
var total = 0, i = 0;
for (i = 0; i < index+1; i++) {
total += items[i][field];
}
return total;
};
});
To use it in column you just do:
<div>Total price: {{items | runningTotal:'price':$index}}</div>
I'm not sure I totally understand the question, but are just needing to display the total number in the object you're iterating over? Just set $scope.total to the length of your array (bar in your example above). So, $scope.total = $scope.bar.length;
If you're wanting the total of all the foo.baz properties, you just need to calculate that in your controller.
$scope.total = 0;
angular.forEach($scope.bar, function(foo) {
$scope.total += foo.baz;
});