Ng-Repeat, how to test for last ng-show element - javascript

I am not trying to add a different class for the last element of an ng-repeat.
I have data to display:
$scope.sizes = [{name:XS,available:true},{name:S,available:true},{name:M,available:false},{name:L,available:true},{name:XL,available:false}];
I want to display the range of data that contains true, i.e. display the above as "XS → L" (note missing out the M option).
Best I have come up with is using ng-repeat:
<span ng-repeat="size in sizes track by $index"
ng-init="sizeIndex = $index"
ng-show="size.available == true && (sizeIndex == 0 || sizeIndex == (sizes.length -1))">
{{size.short}}<span ng-show="sizeIndex ==0"> → </span>
</span>
As expected this fails because size XL available == false, and it won't work if size XS is unavailable.
Is there a permutation of the display logic that I haven't thought of? Or is there a 'proper' way to test if an element is the last (or first) shown in a list?

If you filter by available then a simple $first n $last will work
<li class="animate-repeat" ng-repeat="size in sizes | filter: {available:true}">
<div ng-show="$first">{{size.name}}</div>
<div ng-show="$last">{{size.name}}</div>
</li>
demo https://plnkr.co/edit/QHIKrUOudSYpczWuVdCe?p=preview this a fork of the angular doc example

The best way to do this is to calculate the result in your controller before trying to display it:
$scope.sizes = [
{name: "XS", available: true},
{name: "S", available: true},
{name: "M", available: false},
{name: "L", available: true},
{name: "XL", available: false}
];
DisplayRange();
function DisplayRange () {
var BeginSize = null, EndSize = null;
for (var i=0; i < $scope.sizes.length; i++) {
if ($scope.sizes[i].available === true && BeginSize == null) {
BeginSize = $scope.sizes[i].name;
}
if ($scope.sizes[i].available === true) {
EndSize = $scope.sizes[i].name;
}
}
$scope.FormattedSizes = BeginSize + " → " + EndSize;
}
What this is doing is scanning through each item in the $scope.sizes array. When it finds the first available item, it assigns that name to BeginSize. And each time it sees an available item, it will assign that name to EndSize. We can be sloppy with EndSize because eventually the loop will reach the last available item and assign it to EndSize which will make it correct.
In your HTML, reference it like this:
<span>{{ FormattedSizes }}</span>

Related

How can I render objects to HTML with saving the previous value?

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.

Angular dynamic selects have items that affect one another

I'm attempting to make a dynamic form in Angular 1.4.7 in which:
There are multiple reports (vm.reports = [];)
Each report can be assigned ONE report object via vm.reportOptions.
Each vm.reportOptions can only be selected ONCE across multiple reports, which is filtered via exclude.
Each report supports MANY dimension objects via vm.dimensionOptions.
Each dimension can only be selected ONCE per report, which is filtered via excludeDimensions (subsequent reports have access to all the dimensionOptions and filter on their own).
These requirements are all working (roughly) with the exception of:
If I add two reports, and add the exact same dimensions (ie: Report One > Dimension One > Enable Dimension Filter and Report Two > Dimension One > Enable Dimension Filter) for each of the reports, changing the select inside of Enable Dimensions Filter changes it in both the reports.
I assume that this is happening due to the fact that I'm pushing the actual dimension objects in to each reports dimensions: [] array and that they are still pointing to the same object.
-- EDITS --
I realize angular.clone() is a good way to break this reference, but the <select> code I wrote is automatically piping in the object to the model. I was tempted to give each report their own controller and giving each report their own copy() of the options.
Would this work? Or is there a better way?
I have a working JSBin here.
Pertinent Code:
HTML:
<body ng-app="app">
<div ng-controller="AlertsController as alerts">
<pre>{{alerts.output(alerts.reports)}}</pre>
<div class="container">
<div
ng-repeat="report in alerts.reports"
class="report"
>
<button
ng-if="$index !== 0"
ng-click="alerts.removeItem(alerts.reports,report)"
>Delete Report</button>
<label>Select Report</label>
<select
ng-model="alerts.reports[$index].report"
ng-init="report"
ng-options="reportSelect.niceName for reportSelect in alerts.reportOptions | exclude:'report':alerts.reports:report"
></select>
<div
ng-repeat="dimension in report.dimensions"
class="condition"
>
<div class="select">
<h1 ng-if="$index === 0">IF</h1>
<h1 ng-if="$index !== 0">AND</h1>
<select
ng-model="report.dimensions[$index]"
ng-change="alerts.checkThing(report.dimensions,dimension)"
ng-init="dimension"
ng-options="dimensionOption.niceName for dimensionOption in alerts.dimensionOptions | excludeDimensions:report.dimensions:dimension"
>
<option value="123">Select Option</option>
</select>
<button
class="delete"
ng-if="$index !== 0"
ng-click="alerts.removeItem(report.dimensions,dimension)"
>Delete</button>
</div>
<input type="checkbox" ng-model="dimension.filtered" id="filter-{{$index}}">
<label class="filter-label" for="filter-{{$index}}">Enable Dimension Filter</label>
<div ng-if="dimension.filtered">
<select
ng-model="dimension.operator"
ng-options="operator for operator in alerts.operatorOptions">
</select>
<input
ng-model="dimension.filterValue"
placeholder="Text"
></input>
</div>
</div>
<button
ng-click="alerts.addDimension(report)"
ng-if="report.dimensions.length < alerts.dimensionOptions.length"
>Add dimension</button>
</div>
<button
ng-if="alerts.reports.length < alerts.reportOptions.length"
ng-click="alerts.addReport()"
>Add report</button>
<!--
<div ng-repeat="sel in alerts.select">
<select ng-model="alerts.select[$index]" ng-init="sel"
ng-options="thing.name for thing in alerts.things | exclude:alerts.select:sel"></select>
</div>
-->
</div><!-- container -->
</div>
</body>
JS:
var app = angular.module('app', []);
app.controller('AlertsController', function(){
var vm = this;
vm.reportOptions = [
{id: 1, niceName: 'Report One'},
{id: 2, niceName: 'Report Two'},
{id: 3, niceName: 'Report Three'},
];
vm.dimensionOptions = [
{id: 1, niceName: 'Dimension One'},
{id: 2, niceName: 'Dimension Two'},
{id: 3, niceName: 'Dimension Three'},
];
vm.operatorOptions = [
'>',
'>=',
'<',
'<=',
'=',
'!='
];
////// DEBUG STUFF //////
vm.output = function(value) {
return JSON.stringify(value, undefined, 4);
}
////////////////////////
vm.reports = [];
vm.addReport = function() {
vm.reports.push({report: {id: null}, dimensions: []});
}
vm.removeItem = function(array,item) {
if(array && item) {
var index = array.indexOf(item);
if(index > -1) {
array.splice(index,1);
}
}
}
vm.addDimension = function(report) {
console.log('addDimension',report);
if(report) {
report.dimensions.push({})
}
};
// init
if(vm.reports.length === 0) {
vm.reports.push({report: {}, dimensions: [{}]});
// vm.reports.push({report: vm.reportOptions[0], dimensions: [vm.dimensionOptions[0]]});
}
});
app.filter('excludeDimensions', [function() {
return function(input,select,selection) {
// console.log('ed',input,select,selection);
var newInput = [];
for(var i = 0; i < input.length; i++){
var addToArray=true;
for(var j=0;j<select.length;j++){
if(select[j].id===input[i].id){
addToArray=false;
}
}
if(addToArray || input[i].id === selection.id){
newInput.push(input[i]);
}
}
return newInput;
}
}]);
app.filter('exclude', [function () {
return function(input,type,select,selection){
var newInput = [];
for(var i = 0; i < input.length; i++){
var addToArray=true;
for(var j=0;j<select.length;j++){
if(select[j][type].id===input[i].id){
addToArray=false;
}
}
if(addToArray || input[i].id === selection[type].id){
newInput.push(input[i]);
}
}
return newInput;
};
}]);
How do I get around pushing same object reference to array
Use angular.copy()
array.push(angular.copy(vm.formObject));
// clear object to use again in form
vm.formObject={};
I ended up using select as so that it just set an id on the object instead of pointing to the original object. This solved the problem.

Angular js comparison

I have a condition that needs to be checked in my view: If any user in the user list has the same name as another user, I want to display their age.
Something like
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check" ng-if="user.isSelected"></span>{{user.firstName}} <small ng-if="true">{{'AGE' | translate}} {{user.age}}</small>
</div>
except I'm missing the correct conditional
You should probably run some code in your controller that adds a flag to the user object to indicate whether or not he/she has a name that is shared by another user.
You want to minimize the amount of logic there is inside of an ng-repeat because that logic will run for every item in the ng-repeat each $digest.
I would do something like this:
controller
var currUser, tempUser;
for (var i = 0; i < $scope.userList.length; i++) {
currUser = $scope.userList[i];
for (var j = 0; j < $scope.userList.length; j++) {
if (i === j) continue;
var tempUser = $scope.userList[j];
if (currUser.firstName === tempUser.firstName) {
currUser.showAge = true;
}
}
}
html
ng-if='user.showAge'
Edit: actually, you probably won't want to do this in the controller. If you do, it'll run every time your controller loads. You only need this to happen once. To know where this should happen, I'd have to see more code, but I'd think that it should happen when a user is added.
You can simulate a hashmap key/value, and check if your map already get the property name. Moreover, you can add a show property for each objects in your $scope.userList
Controller
(function(){
function Controller($scope) {
var map = {};
$scope.userList = [{
name:'toto',
age: 20,
show: false
}, {
name:'titi',
age: 22,
show: false
}, {
name: 'toto',
age: 22,
show: false
}];
$scope.userList.forEach(function(elm, index){
//if the key elm.name exist in my map
if (map.hasOwnProperty(elm.name)){
//Push the curent index of the userList array at the key elm.name of my map
map[elm.name].push(index);
//For all index at the key elm.name
map[elm.name].forEach(function(value){
//Access to object into userList array with the index
//And set property show to true
$scope.userList[value].show = true;
});
} else {
//create a key elm.name with an array of index as value
map[elm.name] = [index];
}
});
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
HTML
<body ng-app="app" ng-controller="ctrl">
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check"></span>{{user.name}} <small ng-if="user.show">{{'AGE'}} {{user.age}}</small>
</div>
</body>

linking data from different json objects in jsrender

I am trying to link the data from foos and selectedFoos. I wish to list the selectedFoos and show the name from the foos object. The fooid in the selectedFoos would be linked to the foos id.
EDIT: I dont want to alter the structure of foos or selectedFoos.
fiddle is here
Html, Template
<div id="content"></div>
<script id="content_gen" type="x-jsrender">
<ul> {^{for sf}}
<li > {{: fooid}} - {{: code}} {{foo.name}} </li>
{{/for}}
</ul>
</script>
JS
var foos = [{
"id": 1,
"name": "a"
}, {
"id": 2,
"name": "b"
}, {
"id": 3,
"name": "c"
}];
var selectedFoos = [{
"fooid": 1,
"code": "z"
}, {
"fooid": 3,
"code": "w"
}];
var app = {
sf: selectedFoos,
f: foos
};
var templ = $.templates("#content_gen");
templ.link("#content", app);
You could add a view converter to lookup the name by id.
Like this - http://jsfiddle.net/Fz4Kd/11/
<div id="content"></div>
<script id="content_gen" type="x-jsrender">
<ul> {^{for sf}}
<li>{{id2name:fooid ~root.f }} - {{: code}} </li>
{{/for}}
</ul>
</script>
js
var app = {
sf: selectedFoos,
f: foos
};
$.views.converters("id2name", function (id, foos) {
var r = $.grep(foos, function (o) {
return o.id == id;
})
return (r.length > 0) ? r[0].name : '';
});
var templ = $.templates("#content_gen");
templ.link("#content", app);
Scott's answer is nice. But since you are using JsViews - you may want to data-link so you bind to the name and code values. Interesting case here, where you want to bind while in effect traversing a lookup...
So there are several possible approaches. Here is a jsfiddle: http://jsfiddle.net/BorisMoore/7Jwrd/2/ that takes a modified version of Scott's fiddle, with a slightly simplified converter approach, but in addition shows using nested {{for}} loops, as well as two different examples of using helper functions.
You can modify the name or the code, and see how the update works. You'll see that code updates in all cases, but to get the name to update is more tricky given the lookup.
You'll see that in the following two approaches, even the data-binding to the name works too.
Nested for loops
Template:
{^{for sf }}
{^{for ~root.f ~fooid=fooid ~sf=#data}}
{{if id === ~fooid}}
<li>{^{:name}} - {^{:~sf.code}} </li>
{{/if}}
{{/for}}
{{/for}}
Helper returning the lookup object
Helper:
function getFoo(fooid) {
var r = $.grep(foos, function (o) {
return o.id == fooid;
})
return r[0] || {name: ""};
}
Template:
{^{for sf}}
<li>{^{:~getFoo(fooid).name}} - {^{:code}} </li>
{{/for}}
See the many topics and samples here
http://www.jsviews.com
such as the following:
http://www.jsviews.com/#converters
http://www.jsviews.com/#helpers
http://www.jsviews.com/#fortag
http://www.jsviews.com/#iftag
http://www.jsviews.com/#samples/data-link/for-and-if
You should iterate over selectedFoos and lookup the name with fooid by iterating over foos. Then combine that data before rendering.
function getNameById(id) {
for (var i = 0; i < foos.length; i++)
if (foos[i].id == id)
return foos[i].name;
return '';
}
This function will return the name when given the id.
Usage:
alert(getNameById(2)); // alerts "b"

Javascript every nth, create new row

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

Categories