Use JsRender to create a row/column layout from simple array data - javascript

I am using jsrender to write a bootstrap based HTML template and render the HTML content using javascript. My data is as follows
var data = [
{
'img': 'img/books/kiterunner.jpg',
'title': 'The Kite Runner',
'authors': 'Khaled Hosseini'
},
{
'img': 'img/books/tokillamockingbird.jpg',
'title': 'To Kill A Mocking Bird',
'authors': 'Harper Lee'
},
{
'img': 'img/books/hungergames.jpg',
'title': 'The Hunger Games',
'authors': 'Suzanne Collins'
}
.....
]
And the corresponding HTML I want to generate is as follows
<div class="row-fluid" id="result">
<script id="template" type="text/x-js-render">
<div class="col-md-3 col-sm-6">
<div class="image-tile outer-title text-center">
<img alt="{{:title}}" src="{{:img}}" style="max-height:350px;">
<div class="title mb16">
<h5 class="uppercase mb0">{{:title}}</h5>
<span>{{:authors}}</span>
</div>
</div>
</div>
</script>
</div>
Currently each object in data gets displayed individually but sometimes, the images are larger in size and disrupt the way the row looks. So I want to repeat the div.row-fluid creation with the content after every 4 objects from data. I currently do the following to register the template and render the data into the required HTML. I can't seem to figure out how to use {{for}} without changing the values in data.
var template = $.templates("#template");
var htmlOutput = template.render(data);
$("#result").html(htmlOutput);
Thank you.
Edit: I have managed to do this by doing explicit HTML string concatenation and then rendering them while using index%4 in the if statements to decide on whether the div.row-fluid needs to be created. However, I would really like a more elegant solution. Adding jquery tag for wider visibility.

You could use a custom {{range}} tag - see this sample.
Here is an example which renders into a table with <td>s grouped by four - for a four column layout:
<script id="myTmpl" type="text/x-jsrender">
<table>
{{for rows ~last=rows.length-1}}
{{if #index%4===0 ~ind=#index}}
<tr>
{{range ~root.rows start=~ind end=~ind+3 max=~last}}
<td>{{:title}}</td>
{{/range}}
</tr>
{{/if}}
{{/for}}
</table>
</script>
using this version of a custom {{range}} tag:
$.views.tags("range", function(array) {
var ret = "",
start = this.tagCtx.props.start,
end = this.tagCtx.props.end,
max = this.tagCtx.props.max;
end = end > max ? max : end;
for (var i = start; i <= end; i++) {
// Render tag content, for this data item
ret += this.tagCtx.render(array[i]);
}
return ret;
});
var data = {rows: [
{
'title': ...
},
...
]};
...

Related

Use angularjs nested ng-repeat to construct complex table

I'm having trouble making proper table with nested ng-repeat.
What I wanted is this https://jsbin.com/razamagabo/1/edit?output
but I'm stuck at here https://plnkr.co/edit/d5voXIpzYL81sSl9BSY2?p=preview
I don't mind my markup is not table but I'm still stuck with div
<div class="row">
<div class="col-xs-6" ng-repeat="obj in data">
{{obj.date}}
<div ng-repeat="user in obj.users">
<br>
{{user.name}}
<br>
{{user.mark}}
</div>
</div>
</div>
In order for you to be able to display your data in the desired way, it will probably be easiest if you restructure your data in the JS before trying to render it.
It will be very complicated to try and match on the user names when they are in separate objects in the data array.
I would suggest processing your scope.data in the controller. (I'm assuming that you don't have much control on how you are receiving the data).
For example after you get your data...
$scope.data = [
{
date:'1-1-2016',
users:[
{
'name':'james',
'mark':18
},
{
'name':'alice',
'mark':20
}
]
},
{
date:'2-1-2016',
users:[
{
'name':'james',
'mark':60
},
{
'name':'alice',
'mark':55
}
]
}
]
var userData = {};
var possibleDates = [];
for (dataObj of Object.entries($scope.data)) {
for (userObj of dataObj) {
if ( !userData[userObj.name] ) {
userData[userObj.name] = {};
}
userData[userObj.name][dataObj.date] = userObj.mark;
if (dates.indexOf(dataObj.date) < 0) {
dates.push(dataObj.date);
}
}
}
$scope.users = userData;
$scope.dates = possibleDates;
this will give you an object like this on your scope
$scope.users = {
'james': {
'1-1-2016': 18,
'2-1-2016': 60
},
'alice': {
'1-1-2016': 20,
'2-1-2016': 55
}
};
$scope.dates = ['1-1-2016', '2-1-2016'];
This to me seems easier to structure for your template. Though this assumes each user has an entry for each date.
<div>
<div id='header-row'>
<div id='empty-corner></div>
<div class='date-header' ng-repeat='date in $scope.dates></div>
</div>
<div class='table-row' ng-repeat='{key, value} in $scope.users'>
<div class='user-name'>{{ key }}</div>
<div class='user-data' ng-repeat='date in $scope.dates>
{{ value[date] }}
</div>
</div>
</div>
As long as you apply inline-block styles to the rows/elements this should give you what you are looking for.
Though you can also think of ways to simplify your data even further. You could instead of having each user have an object where the dates are keys, you could just push the values into an array.
With your current data structure it is not possible to display it like you want. You are trying to loop over date-users objects in data array but then you want to display user from inside users array in separate rows. With ng-repeat you can loop through rows tr but not through columns. First you would need to map your data array to group elements that are supposed to be visible in 1 row into 1 object in array. Currently you have them in 2 separate objects:
James mark: 18 and James mark: 60.

How to sort JSON data in ng-repeat?

I am getting a server response and binding these data to view using ng-repeat. Now I want to sort these data by priceList and name. I am able to sort name using orderBy, but not with priceList. I want to sort the products based on priceList. Sorting with name will change the order of list while sorting by priceList will effect only the order of products of each category. It will effect the order of displayed category. Please help me resolve this.
My code:
<div ng-controller="Ctrl">
<pre>Sorting predicate = {{predicate}};</pre>
<hr/>
<table class="friend">
<tr>
<th>Name
</th>
<th><a href="" ng-click="predicate = 'priceList'>price</a></th>
</tr>
</table>
<div ng-repeat="data in _JSON[0].categories | orderBy:predicate">
<div ng-repeat="vals in data.itemTypeResults |orderBy:'partTerminologyName'" id="{{vals.partTerminologyName}}">
`<h4 style="background-color: gray">{{vals.partTerminologyName}} : Position :{{vals.position}}</h4>`<br>
<div ng-repeat="val in vals.products">
<b> Quantity:{{val[0].perCarQty}}</b><br>
<b> part:{{val[0].partNo}}</b><br>
<b>sku:{{val[0].sku}}</b><br>
<b> qtyInStock:{{val[0].qtyInStock}}</b><br>
<b> priceList:{{val[0].priceList}}</b><br>
<b>priceSave:{{val[0].priceSave}}</b><br>
<b> qtyDC:{{val[0].qtyDC}}</b><br>
<b> qtyNetwork:{{val[0].qtyNetwork}}</b><br>
<b> priceCore:{{val[0].priceCore}}</b><br>
</div>
</div>
</div>
JS:
$scope._JSON = [
{"categories":
[
{"id":14061,"name":"Drive Belts",
"itemTypeResults":[
{"partTerminologyName":"Serp. Belt",
"position":"Main Drive",
"products":{
"5060635":[
{"perCarQty":2,"partNo":"5060635",
"sku":"20060904","qtyInStock":2,"qtyNetwork":4,
"qtyDC":6,"priceList":19.15,"priceSave":3.29,
"priceCore":10.0}
],
"635K6":[
{"perCarQty":9,"partNo":"635K6",
"sku":"10062449","qtyInStock":2,"qtyNetwork":4,
"qtyDC":6,"priceList":18.15,"priceSave":3.21,"priceCore":10.0}
]
}
}
]
},
{"id":2610,"name":"Drive Belt Tensioners, Idlers, Pulleys & Components",
"itemTypeResults":[
{"partTerminologyName":"Drive Belt Tensioner Assembly",
"position":"N/A",
"products":{
"950489A":[
{"perCarQty":4,"partNo":"950489A",
"sku":"10150833","qtyInStock":2,"qtyNetwork":4,
"qtyDC":6,"priceList":18.15,"priceSave":3.21,"priceCore":10.0
}
]
}},
{"partTerminologyName":"Drive Belt Idler Pulley","position":"N/A",
"products":{
"89161":[
{"perCarQty":1,"partNo":"89161",
"sku":"99995959","qtyInStock":2,"qtyNetwork":4,
"qtyDC":6,"priceList":17.15,"priceSave":3.21,"priceCore":10.0}
],
"951373A":[
{"perCarQty":2,"partNo":"951373A","pla":"LTN",
"plaName":"Litens",
"sku":"10150926","qtyInStock":2,"qtyNetwork":4,
"qtyDC":6,"priceList":18.15,"priceSave":3.21,"priceCore":10.0}
]
}
}
]
}
]
}
];
$scope.predicate = '';
Fiddle: Fiddle
You might need to define a very good sorter function, or sort your products before they are interpreted by ng-repeat. I've created sorter function using underscore.js (or lodash).
You can checkout the demo (or the updated demo). Products are first sorted by category and then sorted by price in every category.
<!-- in html -->
<button ng-click="sortFn=sortByPrice">Sort By Price</button>
<button ng-click="sortFn=doNotSort">Do not Sort</button>
...
<div ng-repeat="val in sortFn(vals.products)">
...
// in js
$scope.sortByPrice = function(products) {
return _.sortBy(products, function(product) {
return product.length > 0 ? product[0].priceList : 0;
});
};
$scope.doNotSort = function(products) {
return products;
};
$scope.sortFn = $scope.doNotSort; // do not sort by default
BTW: You are directly calling val[0], which is very dangerous, if the product does not contain any elements, your code will break. My code won't ;-)
Update 1
The author asks me for a more pure Angular way solution.
Here is my answer: I think my solution is exactly in Angular way. Usually you can implement a filter (similar to orderBy) which wraps my sortByPrice. Why I don't do that, because you have ng-click to switch your order filter. I'd rather put control logic into a controller, not as pieces into view. This will be harder to maintain, when your project keeps growing.
Update 2
Okay, to make the +50 worthy, here is the filter version you want, (typed with my brain compiler) Please check in fiddle
You need to organize the products in other estructure. For example:
$.each($scope._JSON[0].categories , function( i , e) {
$.each(e.itemTypeResults, function(sub_i,sub_e) {
$.each(sub_e.products, function(itemTypeResults_i,product) {
console.log(product);
var aProduct = new Object();
aProduct.priceList = product[0].priceList;
aProduct.name = e.name;
$scope.products.push(aProduct);
});
} )
});
The code is not very friendly but what i do is putt all the products in one array so they can be ordered by the price. You have the products inside categories so that's why angular is ordering by the price in each category.
Fiddle:
http://jsfiddle.net/7rL8fof6/1/
Hope it helps.
Your fiddle updated: http://jsfiddle.net/k5fkocby/2/
Basically:
1. Digested the complex json object into a flat list of objects:
var productsToShow = [];
for (var i=0; i < json[0].categories.length; i++){
var category = json[0].categories[i];
for (var j=0; j<category.itemTypeResults.length;j++){
var item = category.itemTypeResults[j];
var products = item.products;
for (var productIndex in products) {
var productItems = products[productIndex];
for (var k=0; k<productItems.length;k++){
var productItem = productItems[k];
// Additions:
productItem.categoryName = category.name;
productItem.partTerminologyName = item.partTerminologyName;
productItem.position = item.position;
productsToShow.push(productItem);
}
}
}
}
Show category title only when needed by:
ng-repeat="product in (sortedProducts = (productsToShow | orderBy:predicate))"
and
ng-show="sortedProducts[$index - 1].partTerminologyName != product.partTerminologyName"
you can sort from your database and get final JSON data..
db.categories.aggregate([{$group : {category : {your condition} }, price: {$sort : { price: 1 } },}}])

angular performance: critical rendering path?

Im trying to optimice the page loading speed when rendering tables with many rows(columns min. 25x).
I am not experienced debugging/improving performance on angular apps so quite lost on what could be involved in this lack of speed.
Here is Chrome timeline report for 5 row query:
Here is Chrome timeline report for 100 row query:
The XHR load(api/list/json/Chemical...) increases in time as more rows are rendered on the table.
The server response with the data is returned fast(not the bottle neck):
Here is the template for the table:
<tbody ng-if="compressed">
<tr ng-if="dbos && (rows.length == 0)">
<td class="tableColumnsDocs"><div class="tableButtons"> </div></td>
<td class="tableColumnsDocs"><div>No results</div></td>
<td class="tableColumnsDocs" ng-repeat="attobj in columns track by $index" ng-if="$index > 0">
<p> </p>
</td>
</tr>
<tr class="tableRowsDocs" ng-repeat="dbo in rows track by $index">
<td class="tableColumnsDocs"><div ng-include="'link_as_eye_template'"></div></td>
<td class="tableColumnsDocs" ng-repeat="attobj in columns track by $index">
<div ng-init="values = dbo.get4(attobj.key); key = attobj.key; template = attobj.template || getAttributeTemplate(dbo.clazz + attobj.key);">
<div class="content" ng-include="template"></div>
<div class="contentFiller" ng-include="template"></div>
</div>
</td>
</tr>
</tbody>
And here templates the table will call:
<script type="text/ng-template" id="plain_values_template">
<p ng-repeat="v in values track by $index">{{ v }}</p>
</script>
<script type="text/ng-template" id="links_as_dns_template">
<div ng-repeat="dbo in values track by $index" ng-include="'link_as_dn_template'"></div>
</script>
<script type="text/ng-template" id="json_doc_template">
<textarea class="form-control" rows="{{values.length + 2}}" ng-trim="false" ng-readonly="true">{{ values | json }}</textarea>
</script>
<script type="text/ng-template" id="link_as_dn_template">
<p>{{ dbo.displayName() }}</p>
Relevant controller part:
$scope.getAttributeTemplate = function(str) {
//console.log("getAttributeTemplate"); console.log(str);
if ($templateCache.get(str + ".template")) {
return str + ".template";
}
var a = str.split(/(>|<)/);
//console.log(a);
if ((a.length - 1) % 4 == 0) {
return "links_as_dns_template";
}
var clsname = a[a.length - 3];
if (clsname == "*") {
return "plain_values_template";
}
var attname = a[a.length - 1];
var cls = datamodel.classes[clsname];
var att = cls.attribute[attname];
if (!att) {
return "plain_values_template";
}
if (att.type == "ref") {
return "links_as_dns_template";
}
return "plain_values_template";
};
I am new to angular and performance opt. so any tips on how to improove or bad practice highlight will be very helpful!
Long tables are angular's biggest evil, because of the hell-as-slow base directives such as ng-repeat
Some easy and obvious stuffs :
I see a lot of bindings in the row/cell templates without one-time binding (::). I dont think your row data is mutating. switching to one-time bindings will reduce the watchers count -> perf.
Some harder stuff :
Quick answer :
dont let angular handle the performance bottleneck
Long answer :
ng-repeat is supposed to compile it's transcluded content once. But using ng-include is killing this effet, causing every row to call compile on their ng-included contents. The key for good performance in big table is to be able to generates (yea, manually, which $compile, $interpolate and stuff) a unique compiled row linking function, with less as possible angular directives - ideally only one-time expression bindings, and to handle row addiction/removal manually (no ng-repeat, you own directive, your own logic)
You should AT LEAST find a way to avoid the second nested ng-repeat on' ng-repeat="attobj in columns track by $index"'. This is a dual repeated on each row, killing compilation &linking (rendering perf) and watcher count (lifecycle perf)
EDIT : as asked, a "naive" example of what can be done to handle the table rendering as manually (and fast) as possible. Note that the example does not handle generating the table header, but it's usually not the hardest thing.
function myCustomRowCompiler(columns) {
var getCellTemplate = function(attribute) {
// this is tricky as i dont know what your "getAttributeTemplate" method does, but it should be able to return
// the cell template AS HTML -> you maybe would need to load them before, as getting them from your server is async.
// but for example, the naive example to display given attribute would be
return $('<span>').text("{{::model."+ attribute +"}}"); // this is NOT interpolated yet
};
var myRowTemplate = $('<tr class="tableRowsDocs">');
// we construct, column per column, the cells of the template row
_.each(columns, function(colAttribute, cellIdx) {
var cell = $("<td>");
cell.html(getCellTemplate());
cell.appendTo(myRowTemplate);
})
return $compile(myRowTemplate); // this returns the linking function
}
and the naive usage :
function renderTableRows(dbos, columns) {
var $scope; // this would be the scope of your TABLE directive
var tableElement = $el; // this would be your table CONTENT
var rowLinker = myCustomRowCompiler(columns); // note : in real life, you would compile this ONCE, but every time you add rows.
for(var i=0; i<dbos; i++) {
var rowScope = $scope.$new(); // creating a scope for each row
rowScope.model = dbos[0]; // injecting the data model to the row scope
rowLinker(rowScope, function(rowClone) { // note : you HAVE to use the linking function second parameter, else it will not clone the element and always use the template
rowClone.appendTo(tableElement);
});
}
};
This is the approach i've been using to my own projects's table framework (well, more advanced, but this is really the global idea), allowing to use angular power to render the cell content ( 'getCellTemplate' implementation can return html with directive, which will be compiled), using filter even including directives in the cell, but keeping the table rendering logic to myself, to avoid useless ng-repeat watch, and minimizing the compilation overheat to it's minimum.

Combine divs within a for loop

Is it possible to use a JavaScript for loop to combine a number of div's? I have 16 sets of these I am wanting to put into a for loop. The problem I am having is that its HTML not JavaScript I am trying to do this with. I haven't seen anything so far on how to go about this. Thanks for any help or suggestions.
What the following code does is catch the data pre-defined from a 16X17 table and inserts it into one cell in my document. I then have have other code using the div id's that makes visible just the one I need.
<div id="101" class="hidden"><script>document.write(tab1a1)</script></div>
<div id="102" class="hidden"><script>document.write(tab1a2)</script></div>
<div id="103" class="hidden"><script>document.write(tab1a3)</script></div>
<div id="104" class="hidden"><script>document.write(tab1a4)</script></div>
<div id="105" class="hidden"><script>document.write(tab1a5)</script></div>
<div id="106" class="hidden"><script>document.write(tab1a6)</script></div>
<div id="107" class="hidden"><script>document.write(tab1a7)</script></div>
<div id="108" class="hidden"><script>document.write(tab1a8)</script></div>
<div id="109" class="hidden"><script>document.write(tab1a9)</script></div>
<div id="110" class="hidden"><script>document.write(tab1a10)</script></div>
<div id="111" class="hidden"><script>document.write(tab1a11)</script></div>
<div id="112" class="hidden"><script>document.write(tab1a12)</script></div>
<div id="113" class="hidden"><script>document.write(tab1a13)</script></div>
<div id="114" class="hidden"><script>document.write(tab1a14)</script></div>
<div id="115" class="hidden"><script>document.write(tab1a15)</script></div>
<div id="116" class="hidden"><script>document.write(tab1a16)</script></div>
<div id="117" class="hidden"><script>document.write(tab1a17)</script></div>
Update: Pulling data from table
<!--Start- Takes Assembly number from Data Table--> <!--Change "<Col1;" equala columns-->
for (var x = 1; x<Col1; x++){window["aa"+x] = document.getElementById("part1Table").rows[0].cells[x+1].innerHTML;}
<!--End--- Takes Assembly number from Data Table-->
<!--Start- Takes Assembly Rows from Data Table--> <!--Change "<Row1;" equals rows-->
for (var y = 1; y<Row1+1; y++){window["rows"+y] = document.getElementById("part1Table").rows[y].cells[1].innerHTML;}
<!--End- Takes Assembly Rows from Data Table-->
<!--Start- Takes Part number from Data Table--> <!-- "<Col1;" equals columns----> <!-- If a Column is added to main table add a new line below---->
for (var z1 = 1; z1 <Col1; z1++) {window["tab1a"+z1] = document.getElementById("part1Table").rows[1].cells[z1 +1].innerHTML;}
for (var z2 = 1; z2 <Col1; z2++) {window["tab2a"+z2] = document.getElementById("part1Table").rows[2].cells[z2 +1].innerHTML;}
for (var z3 = 1; z3 <Col1; z3++) {window["tab3a"+z3] = document.getElementById("part1Table").rows[3].cells[z3 +1].innerHTML;}
Try using AngularJS (which is a javascript framework that you can easily add to your html page (see AngularJS.com for a quick intro))
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js"></script>
</head>
<body ng-app='myApp'>
<div ng-repeat='entry in entries'>
<div id={{entry.id}} class="hidden"><script>document.write({{entry.table}})</script></div>
</div>
<script src='app.js'></script>
</body>
</html>
Then within a controller in a file called app.js you would make a array of objects 1...100 or whatever number you want and pass it to a scope like this
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', function($scope) {
$scope.entries = [{
id: '101', table: 'tab1a1'}, {id: '102', table: 'tab1a2'}]
// extend to your own range
});
having a bunch of sequential variables like tab1a1 is difficult to work with. Can you put those in an array? Then...
var b=document.getElementsByTagName('body')[0];
for(var i=1; i<18; i++){
var d=document.createElement('div'); //make a div element
d.id='10'+i; //assign sequential id
d.innerHTML=tab1a[i]; //put the the array element content corresponding to that number into the new div element
b.appendChild(d); //place new element into the DOM
}
Another option would be to use excel and basically make a column with on the rows 101,102,103... And a similar same column for the tables. Then just add the code lines in string format and bind the colums together with &. Copy everything and paste this in notepad and then in your code!
Ex:
="<div id='" & A1 & "' etc etc
Thank you to everyone who gave suggestions. I spent a few more hours with the code and came up with the following. It works great in testing. I can now add and remove rows and columns from my table without having to change the code.
for (var w = 1; w <Row1; w++)
for (var z1 = 1; z1 <Col1; z1++) {window["tab" + w + "a" + z1] = document.getElementById("part1Table").rows[w].cells[z1 +1].innerHTML;}
This replaces my 17 lines of the following:
for (var z17 = 1; z17 <Col1; z17++) {window["tab17a"+z17] = document.getElementById("part1Table").rows[17].cells[z17 +1].innerHTML;}
This is very good because my new table has 50+ rows.

angularjs data binding with dynamically created elements

I have this code in my application:
$scope.appendBets = function()
{
$.each($scope.phaseBets, function(i, bet)
{
var betElement = angular.element('<div ng-model="phaseBets[i]">Bet id: {{phaseBets[i].id}}</div>');
angular.element(document.querySelector('#betsHolder')).append(betElement);
$compile(betElement)($scope);
});
}
the $scope.phaseBets is loaded from $http.get.
Now the problem is that the {{phaseBets[i].id}} content not seen on the html page, I am getting this Bet id:.
I have seen this OS but it's not working for me, maybe because of the array.
Is there anything wrong with my code?
Note
The thing is that by the I'd I will create an element (different for each id) so ng-repeat not helping that mutch.
Here's how I'd do it using ng-repeat and ng-include:
$scope.items = [
{id: 1, title: 'foo', data: {body: 'baz1'}},
{id: 2, title: 'bar', data: {body: 'baz2'}}
];
<div ng-repeat="item in items">
<h2>{{item.title}}</h2>
<div ng-include src="getTemplateById(item.id)"></div>
</div>
Where the templates are defined inline like this:
<script type="text/ng-template" id="template-1.html">
Content of template-1.html
<div>{{item.data.body}}</div>
</script>
<script type="text/ng-template" id="template-2.html">
<p>Content of template-2.html</p>
</script>
and getTemplateById is:
$scope.getTemplateById = function(id) {
return 'template-' + id + '.html';
};
You can see it in action here.
I think you got it from wrong side, in angularjs controllers/data drives the view, here you are creating elements (and even worse adding them to page) in loop (expensive DOM operations)
I'd replace your code with following
<div id="betsHolder">
<div ng-repeat="bet in phaseBets track by bet.id">Bet id: {{bet.id}}</div>
</div>
as soon as you assign your array/object to $scope.phaseBets the DOM will be created
but using ng-repeat is better option,
angular.forEach($scope.phaseBets, function(bet, i)
{
var betElement = angular.element('<div ng-model="bet">Bet id: {{bet.id}}</div>');
angular.element(document.querySelector('#betsHolder')).append(betElement);
$compile(betElement)($scope);
});

Categories

Resources