Loops in Handlebars - javascript

I have an array of tracks coming from a database that I want to display in a div.
More specifically I need to put every two of them in a bootstrap row. I can easily do it in the controller JS file by first collecting all in a fragment element and then use a loop to put them into the rows and then in the target div but I am wondering if it would be possible to do it directly while producing them in handlebars?
Here is the handlebars template:
{{#if result}}
{{#each result}}
<div class="playlist-item col-xs-4">
<a href="#/user/{{username}}/playlist/{{id}}" class="no-style">
<h3 class="result-title">{{title}}</h3>
<p class="result-description">{{description}}</p>
<img class="result-image img-circle" src="{{img}}">
</a>
<br>
<a type="button" id="{{id.videoId}}" class="btn btn-default btn-remove"
href="#/user/{{username}}/playlist/{{id}}/remove-from-playlist">Remove from
playlist</a>
</div>
{{/each}}
{{else}}
<h4>You currently have no tracks in your playlist</h4>
{{/if}}
Here is the JS:
showPlaylist() {
return Promise.all([
userController.loadPlaylist(),
templates.loadTemplate('playlist'),
])
.then(([tracks, template]) => {
let fragment = document.createDocumentFragment()
let div = document.createElement('DIV');
div.innerHTML = template(tracks);
div = [...div.children];
let len = div.length
while(div.length > 0) {
let row = document.createElement('div')
row.className = 'row'
let col = div.splice(0,2)
row.append(col[0])
if(col[1]) {
row.append(col[1])
}
len -= 2;
fragment.append(row)
}
$('#container').html(fragment)
})
}

It is possible to group your items into rows, but you would need to use a custom helper function to do it.
We will need to create a block helper that takes an array of items, breaks the array into rows of specified number of columns, and then applies the block "row" template to each row. If we were to call our block helper "eachRow", the resulting template might look like the following:
{{#eachRow result 2}}
<div class="row">
{{#each columns}}
<div class="playlist-item col-xs-4">
{{!-- TODO: Rest of item template goes here. --}}
</div>
{{/each}}
</div>
{{/eachRow}}
Notice that we still use the item template within a regular Handlebars #each block. Except now the #each is wrapped within a "row" template block. The 2 is a parameter that will be passed to our helper that is to be the number of columns in each row.
Next, we will write our helper:
Handlebars.registerHelper('eachRow', function (items, numColumns, options) {
var result = '';
for (var i = 0; i < items.length; i += numColumns) {
result += options.fn({
columns: items.slice(i, i + numColumns)
});
}
return result;
});
This helper simply iterates over our source array in increments of numColumns and for each iteration applies our "row" block template, passing the array of items (columns) that are to render in that row. The helper concatenates the rows and returns the result.
I have created a fiddle for reference.

Related

How do I use For Loop in JavaScript to show the list?

I am a beginner in JavaScript and I can't figure out the following problem: I am trying to create a simple JavaScript Movie List. I have 10 lists on the Movie List. I tried to show all of the lists with for loop, but it doesn't work.
Here's the code:
function renderModal() {
for (let i = 0; i < listMovies.length; i++) {
let movieData = listMovies[i];
document.getElementById("poster").src = movieData.img;
document.getElementById("title").innerHTML = movieData.name;
document.getElementById("genre").innerHTML = movieData.genre;
document.getElementById("rating-num").innerHTML = "Rating: "+ movieData.rating + "/10";
document.getElementById("movie-desc").innerHTML = movieData.desc;
document.getElementById("imdb-page").href = movieData.link;
return movieData;
}
}
What do I have to do?
Help me to fix it!.
You can use template tag for list and render it into target element.I am showing an example.
Movie list
<div id="movieList"></div>
template for list
<template id="movieListTemplate">
<div class="movie">
<img src="" class="poster" alt="">
<div class="title"></div>
<div class="genre"></div>
<div class="rating-num"></div>
<div class="movie-desc"></div>
<div class="imdb-page"></div>
</div>
</template>
Javascript code:
if (listMovies.length > 0) {
const movileListTemplate = document.getElementById('movieListTemplate')
const movieRenederElement = document.getElementById('movieList')
for(const movie of listMovies) {
const movieEl = document.importNode(movileListTemplate.content, true)
movieEl.querySelector('.poster').src = movie.img
movieEl.querySelector('.title').textContent = movie.name
//use all queryselector like above
}
}
Your return movieData; will stop the loop dead. Not that running it more than once will change anything since you change the same elements over and over. IDs must be unique.
Here is a useful way to render an array
document.getElementById("container").innerHTML = listMovies.map(movieData => `<img src="${movieData.img}" />
<h3>${movieData.name}</h3>
<p>${movieData.genre}</p>
<p>Rating: ${movieData.rating}/10</p>
<p>${movieData.desc}
IMDB
</p>`).join("<hr/>");
With return movieData, the for loop will ends in advance.You should put it outside the for loop.

Creating a template out of HTML Elements

lets say i have a parent-div. And in this div-container, i want to display 5 elements which have all the same structure. For example:
<div class="element">
<p class="name">
</p>
<div class="logo">
</div>
</div>
Is there a way to make an object or prototype out of it, so i dont have to generate every single HTML Element with their classes and src values with the appendChild-function and Dot-Notations in a for-loop?
Im thinking of something like:
for(let i = 0; i<=5;i++){
var element = new element(class,src1,src2 ...);
}
And the "element" is defined in a external class file or something familiar.
Im a beginner, so please show mercy :)
You'll need to clone the node from the template's content. For example:
const templateElement = document.querySelector("#someTemplate")
.content
.querySelector(".element");
// create an Array of nodes (so in memory)
const fiveNodes = [];
for (let i = 0; i < 5; i += 1) {
const nwNode = templateElement.cloneNode(true);
// ^ clone the whole tree
nwNode.querySelector("p.name").textContent += ` #${i + 1}`;
fiveNodes.push(nwNode);
}
// append the nodes to document.body
// this is faster than appending every element in the loop
fiveNodes.forEach(el => document.body.append(el));
<template id="someTemplate">
<div class="element">
<p class="name">I am node</p>
<div class="logo"></div>
</div>
</template>

AngularJS ng-repeat is slow

It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.
For example:
<div class="list" ng-repeat="entry in data">
<h3>{{entry.title}}</h3>
</div>
This script is updating the $scope.data:
$scope.load = function() {
$scope.data = getDataFromDB();
}
Lets say I have 5 entries inside $scope.data. The entries are:
[
{
id: 1,
title: 1
},
{
id: 2,
title: 2
},
......
]
When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.
So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.
How can I make the rendering instant?
EDITED
I will paste all my code here:
The controller:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = [];
$scope.loadViewModels($scope.orderBy, table);
}
$scope.loadViewModels = function (orderBy, table, cb) {
if (!$scope.endOfPage) {
let searchKey = $scope.page.searchString;
let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
let searchClause = '';
if (searchKey && searchKey.length > 0) {
let searchArr = [];
$($scope.vmKeys).each((i, key) => {
searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
});
searchClause = `WHERE ` + searchArr.join(' OR ');
}
let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
sqlite.query(sc, rows => {
$scope.$apply(function () {
var data = [];
let loadedCount = 0;
if (rows != null) {
$scope.currentPage += 1;
loadedCount = rows.length;
if (rows.length < $scope.itemsPerPage)
$scope.endOfPage = true
for (var i = 0; i < rows.length; i++) {
let item = rows.item(i);
let returnObject = {};
$($scope.vmKeys).each((i, key) => {
returnObject[key] = item[key];
});
data.push(returnObject);
}
$scope.viewModels = $scope.viewModels.concat(data);
}
else
$scope.endOfPage = true;
if (cb)
cb(loadedCount);
})
});
}
}
The view:
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
<div class="row note-list" ng-if="showList">
<h3>Notes</h3>
<input ng-model="page.searchString" id="search"
ng-keyup="search('notes')" type="text" class="form-control"
placeholder="Search Notes" style="margin-bottom:10px">
<div class="col-12 note-list-item"
ng-repeat="data in viewModels track by data.id"
ng-click="edit(data.id)"
ontouchstart="touchStart()" ontouchend="touchEnd()"
ontouchmove="touchMove()">
<p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
ng-bind-html="data.title"></p>
<p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
<div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
<span class="btn fa fa-trash"></span>
</div>
</div>
<div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
<span class="fa fa-plus"></span>
</div>
</div>
<div ng-if="!showList" class="ease-in">
<div>
<div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
<div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
<span class="fa fa-check"></span>
</div>
</div>
<div contenteditable="true" class="note-title"
ng-bind-html="selected.title" id="title">
</div>
<div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
</div>
</div>
<script src="../js/pages/note.js"></script>
Calling it from:
$scope.loadViewModels($scope.orderBy, 'notes');
The sqlite query:
query: function (query, cb) {
db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, res) {
return cb(res.rows, null);
});
}, function (error) {
return cb(null, error.message);
}, function () {
//console.log('query ok');
});
},
It is apache cordova framework, so it uses webview in Android emulator.
My Code Structure
<html ng-app="app" ng-controller="pageController">
<head>....</head>
<body>
....
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
....
</div>
</body>
</html>
So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?
Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.
NOTE
After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?
One approach to improve performance is to use the track by clause in the ng-repeat expression:
<div class="list" ng-repeat="entry in data track by entry.id">
<h3>{{entry.title}}</h3>
</div>
From the Docs:
Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
For more information, see
AngularJS ngRepeat API Reference -- Tracking and Duplicates
In your html, try this:
<div class="list" ng-repeat="entry in data">
<h3 ng-bind="entry.title"></h3>
</div>
After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.
Example:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = []; <------ THIS
$scope.loadViewModels($scope.orderBy, table);
}
So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.

Scrape text from a complex DOM structure

Consider the following hierarchy in DOM
<div class="bodyCells">
<div style="foo">
<div style="foo">
<div style="foo1"> 'contains the list of text elements I want to scrape' </div>
<div style="foo2"> 'contains the list of text elements I want to scrape' </div>
</div>
<div style="foo">
<div style="foo3"> 'contains the list of text elements I want to scrape' </div>
<div style="foo4"> 'contains the list of text elements I want to scrape' </div>
</div>
By using class name bodyCells, I need to scrape out the data from each of the divs one at a time (i.e) Initially from 1st div, then from the next div and so on and store it in separate arrays. How can I possibly achieve this? (using puppeteer)
NOTE: I have tried using class name directly to achieve this but, it gives all the texts in a single array. I need to get data from each tag separately in different arrays.
Expected output:
array1=["text present within style="foo1" div tag"]
array2=["text present within style="foo2" div tag"]
array3=["text present within style="foo3" div tag"]
array4=["text present within style="foo4" div tag"]
As you noted, you can fetch each of the texts in a single array using the class name. Next, if you iterate over each of those, you can create a separate array for each subsection.
I created a fiddle here - https://jsfiddle.net/32bnoey6/ - with this example code:
const cells = document.getElementsByClassName('bodyCells');
const scrapedElements = [];
for (var i = 0; i < cells.length; i++) {
const item = cells[i];
for (var j = 0; j < item.children.length; j++) {
const outerDiv = item.children[j];
const innerDivs = outerDiv.children;
for (var k = 0; k < innerDivs.length; k++) {
const targetDiv = innerDivs[k];
scrapedElements.push([targetDiv.innerHTML]);
}
}
}
console.log(scrapedElements);

Adding get() to line causes "is not a function" error

This is the start of an inventory system I am working on. Basically it takes an array with items and quantities in a compressed form and outputs the items into an item div.
Running the below produces no error:
$('.item_amount').html(collection[itemName].amo);
Adding the get() method after the selector like so:
$('.item_amount').get(i).html(collection[itemName].amo);
produces "$(".item_amount").get(i).html is not a function".
This is what the line is altering:
<div class="item">
<img src="" class="item_image"/>
<div class="item_amount"></div>
</div>
The line that is causing the error is located in a for loop that loops through all the keys in an array. Then outputs the item quantity from the array in the item_amount div based on the index stored in the variable "i". The for loop also creates an object for each item in the array and puts in the a collection object.
Full code below:
<body>
<div class="item">
<img src="" class="item_image"/>
<div class="item_amount"></div>
</div>
<div class="item">
<img src="" class="item_image"/>
<div class="item_amount"></div>
</div>
<div class="item">
<img src="" class="item_image"/>
<div class="item_amount"></div>
</div>
<script type="text/javascript">
var collection = new Object();
function makeItem(itemName, id, amo) {
collection[itemName] = new item(id, amo);
}
function item(id, amo) {
this.id = id;
this.amo = amo;
}
var inventoryCom = "368.9,366.15,384.32"; //compressed inventory
var inventoryArr = inventoryCom.split(',');
for(var i=0; i < inventoryArr.length; i++) {
var itemName = 'item' + (i + 1); //unique name for each item
var itemArr = inventoryArr[i].split('.');
makeItem(itemName, itemArr[0], itemArr[1]);
$('.item_amount').get(i).html(collection[itemName].amo);
}
</script>
</body>
.get(i) returns DOM element, which doesn't have .html() method - that's what js engine wants to say to you.
You need to use .eq(i) instead. Like
$('.item_amount').eq(i).html(collection[itemName].amo);
or
$('.item_amount:eq(' + i + ')').html(collection[itemName].amo);
This line may be a problem
var itemName = 'item' + (i + 1); //
This may increment the array count out of the upper bound. check the itemName value.
Also try to add an alert for this
collection[itemName].amo

Categories