Performance very slow, javascript, mustache rendering templates in a loop - javascript

I'm having terrible performance problems using .append and mustache, as you can see i'm looping over events and rendering a template that i am appending in to the DOM - can anyone suggest how make this a lot more efficient, currently the app grinds to a halt in Chrome..
Anyhelp will be be very much welcome, this is a major problem for me at the moment.
_.each(currentEvents.events, function(list, index) {
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}"
var eventOutput = Mustache.render(template, list);
$('[data-event-location-id=' + list.event_location_id + ']').append(eventOutput);
});

The var template should be init outside the loop (only once!):
var i, eventLength=currentEvents.events;
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}";
This mustache parse method helps speeds up future uses of render method (documentation):
Mustache.parse(template);
using _.each or forEach it more expensive than the native for loop (pay attention that I didn't use the currentEvents.events.length expression in the for condition - cause we prefer it to be calculated only once):
for (i = 0 ; i < eventLength ; i++ ) {
outPutArray.push(Mustache.render(template, list));
});
For this line to work you should change your flow design, from this jquery selector i can see your ids are already has been appended inside the DOM, and now you only go and append to each one o them the relevent HTML, this is wrong, you should have do the appending process to the ids which we can not see in your code and make it work togheder with the appending of the additional markup. like:
$('#parent-div').append(outPutArray.join(" "));
All code together:
var i, eventLength=currentEvents.events;
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}";
Mustache.parse(template);
for (i = 0 ; i < eventLength ; i++ ) {
outPutArray.push(Mustache.render(template, list));
});
$('#parent-div').append(outPutArray.join(" "));

Related

jQuery .size() function doesn´t work with a variable

At many points in my code I need to know how many .page classes are used.
To know this I use $(".page").size()
I want to save this information into a variable.
So I wrote this:
var vari = {
*more variables*
totalPageCount : $(".page").size()
};
The Problem is that vari.totalPageCount always gives 0 back.
With console.log() I get this:
console.log($(".page").size()); // Return 8
console.log(vari.totalPageCount); // Return 0
Edit:
Here is a example how i use it.
JS:
var vari = {
currentPage : 0,
pageAnimations : 0,
animationList : ".fade-in, .fade-out",
totalPageCount : $(".page").size(),
};
var footer = {
html : function(){
var html;
var date = this.date();
for(var i=0; i<vari.totalPageCount; i++){
html = '<span class="pageNumber" id="pageNumber">Folie:'+i+' • '+vari.custom["companyName"]+' • '+date+'</span>';
$("#normalPage"+i).append(html);
}
return;
}
};
HTML:
<body class="presWrapper">
<div class="pageWrapper">
<div class="page startPage" id="startPage">
<h2 class="mainTitle">Lorem</h2>
<h4 class="subTitle">Ipsum</h4>
</div>
</div>
<div class="pageWrapper">
<div class="page normalPage" id="normalPage1">
<div class="content">
<p class="fade-in">HELLO WORLD</p>
</div>
</div>
</div>
<div class="pageWrapper">
<div class="page endPage" id="endPage">
<div class="content">
<p class="fade-out">HELLO SATURN</p>
<p class="fade-out">HELLO WORLD</p>
<p class="fade-in">HELLO WORLD</p>
<p>pTag</p>
</div>
</div>
</div>
</body>
Any suggestions to solve this problem?
vari.totalPageCount Gets evaluated only when it is declared.
As a result it will only have the value of $(".page").size() when it is first run.
Unless you are waiting on document ready the children of .page have not yet been added and it has length 0.
When you later call the console and execute the selector again - you get the true count in the console message - but the stored value has already been calculated as 0 within vari.
length() and size() are equivalent functions in jquery but size has been deprecated so length is the appropriate function to call. But in either case - its likely you are just evaluating the length too early when the vari object is constructed for it to have a meaningful value.
Does the following give you the wrong value for the property:
$(document).ready(function () {
var vari = {totalPageCount: $('.page').length};
console.log(vari.totalPageCount);
});
Relevant documentation
The .size() method is deprecated as of jQuery 1.8.
Use length property instead:
var vari = {
*more variables*
totalPageCount : $(".page").length;
};
Also, make sure you are using this code at the bottom of the script or inside a document ready handler. You won't get accurate information if you try to get it before DOM has been fully setup.
This will increment count for every element with having the page class
var count = 0;
$('.page').each(function() {
count++;
});
var vari = {
totalPageCount: count
};
Working jsFiddle

What's the best way to append an element using angular?

My objective is to show a grid of products and ads between them.
warehouse.query({limit: limit, skip: skip}).$promise
.then(function(data) {
for (var i = 0; i < data.length; i++) {
var auxDate = new Date(data[i].date);
data[i].date = auxDate.toISOString();
}
Array.prototype.push.apply($scope.products, data);
//add an img ad
var warehouseElem = angular.element(document.getElementsByClassName('warehouse')[0]);
var newAd = $sce.trustAsHtml('<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>');
warehouseElem.append(newAd);
skip += 9
});
Doesn't work.
I already tried simply using pure javascript like,
var warehouseElem = document.getElementsByClassName('warehouse')[0];
var newAd = document.createElement('img');
warehouseElem.appendChild(newAd);
Also doesn't work.
I suppose I need to do something with angular, can't find out what. I think it's sanitize but maybe I just don't know how to use it.
Remember I need to inject an img every once in a while between products.
This is a job for ng-repeat!
<div ng-repeat="data in datas">
<div>[show data here]</div>
<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
If you have bind your "datas" in scope and Math too like this in your controller like this it should works
$scope.datas // this is your list of products
$scope.Math = Math;
If you don't want to spam add for each line you can use ng-if with $index like this :
<div ng-if="$index%2==0">
<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
This will make it display add every 2 lines.
Since you seemed to come from a jQuery-like (or native DOM manipulation) background, I suggest you to read that post : "Thinking in AngularJS" if I have a jQuery background?.
This will explain you why in angular you almost don't manipulate DOM and quite some other things (only in directives).
EDIT : to fix the grid problem, just merging my two html block build your array of datas like this :
$scope.myArray = [product[0], ad[0] or just an empty string it will work still, product[1], ad[1]]
And the html
<div ng-repeat="data in datas">
<div ng-if="$index%2==0">[show data here]</div>
<img ng-if="$index%2==1 src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
In AngularJS you should generally avoid doing DOM manipulation directly and rather rely on angular directives like ng-show/ng-hide and ng-if to dynamically hide sections of a template according to the specific case.
Now back to the problem at hand.
Assuming that you are trying to render a list of products loaded with the code displayed above and display an ad for some of them, you can try the following.
<!-- place the img element in your template instead of appending -->
<div ng-repeat="product in products">
<!-- complex product template-->
<!-- use ng-if to control which products should have an ad -->
<img ng-src="product.adUrl" ng-if="product.adUrl" />
</div>
Then in your controller set adUrl for products that should have an ad displayed.
warehouse.query({limit: limit, skip: skip}).$promise
.then(function(data) {
for (var i = 0; i < data.length; i++) {
var hasAd = // set to true if this product should have an add or not
var auxDate = new Date(data[i].date);
data[i].date = auxDate.toISOString();
if(hasAd){
data.adUrl = "/ad/?r=" + Math.floor(Math.random()*1000);
}
}
Array.prototype.push.apply($scope.products, data);
skip += 9
});
I am most probably assuming too much. If that is the case please provide more details for your specific case.
If you declare a scope variable,
$scope.newAd = $sce.trustAsHtml('<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>');
and in your HTML template, have a binding like
<div ng-bind-html="newAd"></div>,
it should work.

Get child elements within casper.each

Using CasperJS 1.1 with the following codes, I'm able to fetch useful DOM html from web page.
casper.each(c.getElementsInfo(xpath), function(casper, element, j) {
var html = element["html"].trim();
if(html.indexOf('Phone') > -1) {
// what should I put here?
}
});
However, I want to access & obtain the child elements of the element. How can I achieve this? Element's HTML source (a.k.a the value of html) is as follow:
Loop 1
<div class="fields">
Phone
</div>
<div class="values">
12345678 (Mr. Lee) </div>
Loop 2
<div class="fields">
Emergency Phone
</div>
<div class="values">
23456789 (Emergency)
</div>
Loop 3
<div class="fields">
Opening Hours
</div>
<div class="values">
9:00am-6:30pm(Weekday) /
Close on Sundays and Public Holidays(Can be booked)(Holiday)
</div>
Loop 4
<div class="fields">
Last Update
</div>
<div class="values">
11/06/14 </div>
The above HTML is badly formatted, and contains a lot of whitespaces.
The data I wanted to fetch is:
Phone: 12345678
Emergency Phone: 23456789 (Emergency)
Opening Hours: 9:00am-6:30pm(Weekday) / Close on Sundays and Public Holidays(Can be booked)(Holiday)
Last Update: 11/06/14
Tried RegEx, but the RegEx is too complicated.
I don't recommend doing this with regular expressions. It can be easily done with some selectors, but it has to be done in the page context (inside of the evaluate() callback), because DOM nodes cannot be passed to the outside.
CasperJS provides a helper function for matching DOM nodes by XPath with __utils__.getElementsByXPath() through the ClientUtils module that is always automatically inserted. The result of that function is an array, so the normal forEach() pattern applies. DOM nodes can be used as context nodes for selecting child elements with el.querySelector(".class").
var info = casper.evaluate(function(xpath){
var obj = {};
__utils__.getElementsByXPath(xpath).forEach(function(el){
obj[el.querySelector(".fields").textContent.trim()] =
el.querySelector(".values").textContent.trim();
});
return obj;
}, yourXPathString);
If you want to select elements based on a CSS selector use the following:
var info = casper.evaluate(function(cssSelector){
var obj = {};
__utils__.findAll(cssSelector).forEach(function(el){
obj[el.querySelector(".fields").textContent.trim()] =
el.querySelector(".values").textContent.trim();
});
return obj;
}, yourCssSelector);

Update class on one progress bar where there are multiple progress bars

I'm having trouble updating the class on bootstrap progress bar.
I have multiple progress bars on a single page and I want to only update the class on one at the time if it goes over for example 20%. From "progress progress-success" to "progress progress-danger"
I have tried many variations, my code looks now like this where I'm trying to set unique id on
every bar by ng-repeat (AngularJS).
Javascript :
$scope.result = ProjectServices.projects().get(function(d) {
$scope.tasktime = []
console.log($scope.result.Tasks);
var tasks = $scope.result.Tasks;
for(var i = 0; i < tasks.length; i++) {
var prosent = ( tasks[i].EstimatedTimeLeft / tasks[i].EstimatedTime ) * 100;
console.log(prosent);
var extra = '#bar' + i;
if(prosent > 20 ) {
$(extra).removeClass("progress progress-success");
$(extra).addClass("progress progress-danger");
}
$scope.tasktime.push(prosent);
}
HTML : note $index is angularjs syntax to generate numbers from 0...?
<div id="bar{{$index}}" class="progress progress-success">
<div class="bar" style="width:{{tasktime[$index] | number:2}}%">{{tasktime[$index] | number:1}}% </div>
</div>
But it dosent seem to pick up the element, because I know it goes into the if statement.
My extra variable var extra = '#bar' + i; dosen't seem to be legit either any idead how to update single progress bar and generate id for the jQuery selector command?
I think in this example you would be better served to use the ng-class attribute and let angular make the class changes instead of forcing jquery into your controller which generally you wouldn't want to do:
http://docs.angularjs.org/api/ng.directive:ngClass
<div id="bar{{$index}}" class="progress " ng-class='{"progress-success":tasktime[$index].prosent<=20,"progress-danger":tasktime[$index].prosent>20}'>
<div class="bar" style="width:{{tasktime[$index] | number:2}}%">{{tasktime[$index] | number:1}}% </div>
</div>
A quick note I realized the prosent isn't defined, however you could just as easily define a function in your controller:
$scope.prosent = function(item) {
return ( item.EstimatedTimeLeft / item.EstimatedTime ) * 100
}
And the html would be: ng-class='{"progress-success":prosent(item) or prosent(tasktime[$index] depending on your repeater/etc.
Lastly, it seems like perhaps you have not shared all of your code.. I would set up an ng-repeat and do it like this:
<div ng-repeat='task in tasktime' class='progress' ng-class='{"progress-success":task.prosent<=20,"progress-danger: task.prosent>20'>
<div class="bar" style="width:{{task | number:2}}%">{{task | number:1}}% </div>
etc

Move Through Object List

<div id="team-name">{{teams[0].name}}</div>
<button id="next">Next</button>
When the "next" button is hit I would like the team-name to be the next team name in the list, i.e. 0 becomes 1?
I have a feeling I need JS to do this - but I am not sure how I would use JS to do this.
Also, the list is generated from the server.
UPDATE
{{ }} is part of a templating system - Jinja2 to be precise.
The teams list is passed into the webpage through Jinja2 - so the webpage has access to the entire teams list - I hope that makes sense.
class Team(db.Model):
__tablename__ = 'Team'
name = db.Column(db.String(21))
matches_total = db.Column(db.Integer())
matches_won = db.Column(db.Integer())
matches_lost = db.Column(db.Integer())
Make a list containing the names available as team_names and update your template like this:
<div id="team-name" data-index="0" data-entries="{{ team_names|tojson }}">{{teams[0].name}}</div>
<button id="next">Next</button>
In case you are using flask which seems to be the case, pass this to your render_template() call:
team_names=[t.name for t in Team.query]
Then you can use the following jQuery snippet to do what you want:
$('#next').on('click', function(e) {
e.preventDefault();
var nameElem = $('#team-name');
var entries = nameElem.data('entries');
var index = (nameElem.data('index') + 1) % entries.length;
nameElem.text(entries[index]).data('index', index);
})
Note: This answer assumes the list is not too big.

Categories