Jquery looping and binding 10 records at a time - javascript

I have a scenario where I get thousands of records from the server as JSON and bind all records to the page. For each record, I am doing some calculations in jquery and binding data to UI. As record count is in 1000's the time take for calculation and binding data is more. Data on the page is bound at the end when all records calculations are done. Is there any option to bind data one by one or 10 by 10 and show binding on UI for that set. What I am trying to find is to execute $.each for 10 records at a time and append next set of 10 records to it and so on. any idea to make the page load faster? (Paging is not required for my requirement). Any clue can help.
<div id="keepFinalDataHere"></div>
$.each(data, function (i, record) {
content += "<div>" + record.id + "</div><div>" + record.fromId + "</div><div>" + record.subject + "</div>";
});
$(content).appendTo('#keepFinalDataHere');
In above code, content is built by getting several thousands of records and once the content is built, then it is being bound to the div. I am looking for an option to get first 10 items bind the data to make sure that users feel like the page is loaded, and then APPEND remaining items in sets of 100 or so to existing list.

In simple way you can do in chunks.
<div id="keepFinalDataHere"></div>
<script>
//.../
var chunkSize = 50;//what ever you want or could be dynamic based on data size
var $keepFinalDataHere = $('#keepFinalDataHere');
$.each(data, function (i, record) {
content += "<div>" + record.id + "</div><div>" + record.fromId + "</div><div>" + record.subject + "</div>";
if(i % chunkSize === 0){ // content chunk is ready
$keepFinalDataHere.append(content); // show records
content = '';//reset the content
}
});
if(!(content === '')){//any leftOver records
$keepFinalDataHere.append(content);
}

If you want to keep the UI responsive and want to be able to execute code in between rendering a large amount of DOM elements, you'll have to use a timeout mechanism. You can do so by passing your render method to setTimeout.
Instead of adding the method to the stack and executing it immediately, setTimeout pushes the method to a task queue and only executes it once the current js stack has cleared.
The main steps of the method I propose:
Copy your data set to a temporary array
Use splice to remove the first n items from the array
Render the first n items to the DOM
if there are still items left, go to (2)
Here's the main part of the code, with comments, assuming:
testData holds an array of data points
createRow holds the logic to transform a data point to a rendered DOM element
INITIAL_CHUNK_SIZE holds the number of rows you want to render without a timeout.
DEFAULT_CHUNK_SIZE holds the number of rows each following loop has to render
The time out renderer (toRenderer):
var toRenderer = function(s) {
// We need a copy because `splice` mutates an array
var dataBuffer = [].concat(testData);
var nextRender = function(s) {
// Default value that can be overridden
var chunkSize = s || DEFAULT_CHUNK_SIZE;
dataBuffer
.splice(0, chunkSize)
.forEach(createRow);
if (dataBuffer.length) {
setTimeout(nextRender);
}
};
// Triggers the initial (not timed out) render
nextRender(INITIAL_CHUNK_SIZE);
};
In the example below I've included a moving spinner to show how the render loop is able to hold a decent frame rate.
Note that the larger the DEFAULT_CHUNK_SIZE, the faster you'll have all your items rendered. The tradeoff: once one render chunk takes more than 1/60s, you'll loose your smooth frame rate.
// SETTINGS
var DATA_LENGTH = 10000;
var DEFAULT_CHUNK_SIZE = 100;
var INITIAL_CHUNK_SIZE = 10;
var list = document.querySelector("ul");
var createRow = function(data) {
var div = document.createElement("div");
div.innerHTML = data;
list.appendChild(div);
};
// Blocking until all rows are rendered
var bruteRenderer = function() {
console.time("Brute renderer total time:");
testData.forEach(createRow);
console.timeEnd("Brute renderer total time:");
}
// Pushes "render assignments" to the "task que"
var toRenderer = function(s) {
console.time("Timeout renderer total time:");
var dataBuffer = [].concat(testData);
var nextRender = function(s) {
var chunkSize = s || DEFAULT_CHUNK_SIZE;
dataBuffer
.splice(0, chunkSize)
.forEach(createRow);
if (dataBuffer.length) {
setTimeout(nextRender);
} else {
console.timeEnd("Timeout renderer total time:");
}
};
nextRender(INITIAL_CHUNK_SIZE);
};
// EXAMPLE DATA, EVENT LISTENERS:
// Generate test data
var testData = (function() {
var result = [];
for (var i = 0; i < DATA_LENGTH; i += 1) {
result.push("Item " + i);
}
return result;
}());
var clearList = function() {
list.innerHTML = "";
};
// Attach buttons
document.querySelector(".js-brute").addEventListener("click", bruteRenderer);
document.querySelector(".js-to").addEventListener("click", toRenderer);
document.querySelector(".js-clear").addEventListener("click", clearList);
button {
display: inline-block;
margin-right: .5rem;
}
.spinner {
background: red;
border-radius: 50%;
width: 20px;
height: 20px;
animation-duration: 1s;
animation-timing-function: linear;
animation-direction: alternate;
animation-name: move;
animation-iteration-count: infinite;
}
#keyframes move {
from {
transform: translate3d(800%, 0, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
ul {
height: 200px;
overflow-y: scroll;
background: #efefef;
border: 1px solid #ccc;
}
<button class="js-brute">
Inject rows brute force
</button>
<button class="js-to">
Inject rows timeout
</button>
<button class="js-clear">
clear list
</button>
<pre></pre>
<div class="spinner"></div>
<ul>
</ul>

If you have problems with the amount of data you had fetched from the server, you should found a way to limit here the array.
So your client code could handle just the proper amount of elements, just those you could show to the user.
If this is not possible, and you want to do all on client side, you should have a more complicated approach.
You have to save the pointer to the processed elements, and a variable with the amount of element to process (page num?).
And then use a for loop.
// Globally but not global
var cursor = 0
...
for(var i = cursor; i < (cursor+pageNum); i++) {
var element = myDataAsJsonFromApi[i];
// ... do something here.
}
// check if pageNum elements is added..
cursor += pageNum
if (myDataAsJsonFromApi.length == cursor) {
// load from server...
}

One option is to split your data buffer into chunks, so you can operate on some of the data at a time.
var data = [1,2,3,4,5,6,7,7,8,9,9,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,89];
(function () {
var lastSliceStart = 0;
function writeNext() {
var length = 10;
var chunk = $(data).slice(lastSliceStart, lastSliceStart+length);
$(chunk).each((key, item) => {
console.log(item);
});
lastSliceStart += length;
if (lastSliceStart < data.length) {
setTimeout(writeNext, 500); // Wait .5 seconds between runs
}
}
writeNext();
})();
https://jsfiddle.net/bogfdmfb/1/

Build a queue, process queue for few items at a time, show progress, process next items in queue an so on.
//your app
App = {
data: [] //set your JSON dataSource here
}
//define Task
Task = function () {
this.buildQueue(10);
};
Task.prototype = {
buildQueue: function (size) {
var data_count = App.data.length; //length of your datasource
this.queue = [];
// fill the queue
var lastIndex = 0;
var current_index = size;
var c = true;
while (c) {
if (current_index >= data_count - 1) {
current_index = data_count;
c = false;
}
this.queue.push([lastIndex, current_index - 1]);
lastIndex = current_index;
current_index += size;
}
/* If size is 10, array would be [[0,9], [10,19], [20,29]....and so on], The smaller the size, better progress / percentage variation / loading on ui display */
},
doNext: function () {
if (this.queue.length == 0) {
this.end();
return;
}
var row = this.queue.shift(); //stack is LIFO, queue is FIFO, this.queue.pop()
try {
this.processQueue(App.data, row[0], row[1]); //pass dataSource, and indexes of array, loop in processQueue function for indexes passed
} catch (e) {
return;
}
this.incrementProgress(row[1] / App.data.length); //progress on ui
// do next
var _self = this;
setTimeout(function () {
_self.doNext();
}, 1);
},
incrementProgress: function (percent) {
var $progress = $('#percent');
percent = Math.ceil(percent * 100);
percent = percent + '%';
$progress.text(percent);
},
start: function () {
$('#percent').show();
this.doNext(); //initiate loop
},
end: function () {
$('#percent').hide();
},
processQueue: function (data, start, end) {
for (var i = start; i <= end; i++) {
var dataObj = data[i];
//use the data here, update UI so user sees something on screen
}
}
};
//initialize an instance of Task
var _task = new Task(task);
_task.start();

Related

Clar Ajax data after each OnClick and then append new data

I'm doing a project for school. A memory game that uses the pokéAPI to get images.
I have three buttons depending on the game level (easy, medium, hard) and it generates the amount of cards.
When I click a button it generates the pictures but when I click it again it append the new data into the same selector.
I have tried with: $('#output').html(''), $('#output').append(''), $('#output').remove(), $('#output').empty()
// Settings for game level. Each integer represents number of cards * 2.
let easy = 4;
let medium = 6;
let hard = 8;
// Arrays for PokemonImgUrl.
let originalPokemonImgUrl = [];
let duplicateAllPokemonImgUrl = [];
let allPokemonImgUrl = [];
// PokéAPI URL.
const pokemonDataUrl = 'https://pokeapi.co/api/v2/pokemon/';
// This function creates a random number depending on the settings below.
function randomNumber() {
// Settings for max randomnumbers starting from index 1.
let randomNumberMax = 500;
let fromIndex = 1;
// Math random function with values from randomnumbers.
return Math.floor(Math.random() * randomNumberMax) + fromIndex;
}
// Function for getting data from PokéAPI.
function getData() {
$.ajax ({
type: 'GET',
url: pokemonDataUrl + randomNumber(), // Calling randomnnumber to get a random pokémon.
success: function(pokemonData) {
var pokemonImgUrl = pokemonData.sprites.front_default; // Store and extract pokemon images.
originalPokemonImgUrl.push(pokemonImgUrl); // store ImagesURL to a global array called allPokemonImgUrl.
}
})
}
// Shuffle code from css-tricks.com.
function Shuffle(cards) {
for(var j, x, i = cards.length; i; j = parseInt(Math.random() * i), x = cards[--i], cards[i] = cards[j], cards[j] = x);
return cards;
};
// function iterates through allPokemonImgUrl array and outputs it into the DOM.
function output() {
allPokemonImgUrl.forEach(function (i) {
$('#output').append('<img src="'+ [i] +'">');
}
)}
/* This function copies the array so that we always have two of the same cards.
Then concat into a new array and shuffles it. After that it outputs the result.*/
function startGame(){
setTimeout( function(){
duplicateAllPokemonImgUrl = originalPokemonImgUrl.slice();
}, 1000 );
setTimeout( function(){
allPokemonImgUrl = originalPokemonImgUrl.concat(duplicateAllPokemonImgUrl);
}, 1500 );
setTimeout( function(){
Shuffle(allPokemonImgUrl)
}, 2000 );
setTimeout( function(){
output();
}, 2500 );
}
/* Events for clicking on game levels. It iterates to check how many cards it needs
and calls the function getData accordingly. */
$(document).on('click', '#easy', function() {
for (var cards = 0; cards < easy; cards++) {
getData();
}
startGame();
})
$(document).on('click', '#medium', function() {
for (var cards = 0; cards < medium; cards++) {
getData();
}
startGame();
})
$(document).on('click', '#hard', function() {
for (var cards = 0; cards < hard; cards++) {
getData();
}
startGame();
})
When I click a button I get the images as expected.
But when I click it again it appends the new data after the old data. I want to remove the old ajax data first and then append the new data.
I found the solution:
Adding a clear function:
function clear() {
originalPokemonImgUrl = [];
duplicateAllPokemonImgUrl = [];
allPokemonImgUrl = [];
$('#output').empty();
}
Then just call it:
$(document).on('click', '#easy', function() {
for (var cards = 0; cards < easy; cards++) {
getData();
}
clear();
startGame();
})

Showing Progress while Child row loads

Coming again with another question :)
This time I had a requirement to show some progress while Child rows are being loaded. Since there is an Api call which relatively takes little time to return data, I do want to show the some progress unless the user who clicks the parent row is totally unaware whether there is a call done to see its child rows.
What I have done:
I wrote a style sheet class which has a
loader-small.gif
image as this:
tr.loading td.details-control {
background: url('/Images/loader-small.gif') no-repeat center center;
}
and applied like this:
$('#accountManagerEarningsDataTable tbody').on('click', 'td.details-control', function () {
var tr = $(this).closest('tr');
var row = table.row(tr);
try {
if (row.child.isShown()) {
// This row is already open - close it
row.child.hide();
tr.removeClass('shown');
}
else {
//Calling the loading Class ------>
tr.addClass('loading');
// Open this row
var arrForTable1 = [];
var arrForTable2 = [];
totalBrokerage = 0;
totalRetailBrokerage = 0;
totalSelfServiceBrokerage = 0;
console.log('You selected: ' + row.data().AccountManagerID);
var settings = {
"columnDefs": [
{ targets: 1, align: "right", decimals: 0 },
{ targets: 2, align: "right", decimals: 0 },
{ targets: 3, align: "right", decimals: 0 },
{ targets: 4, align: "right", decimals: 2 },
{ targets: 5, align: "right", decimals: 2 }
]
};
//problems with asynchoronus call back
var response = organization_GetAccountManagerDetailEarningsAccountData(row.data(), purl2, pcontext);
if (response.success === "true") {
for (var i = 0; i < response.value.length; i++) {
for (var j = 0; j < response.value[i].Securities.length; j++) {
var itemRow2 = {};
itemRow2["Security ID"] = response.value[i].Securities[j].SecurityId;
itemRow2["Trades"] = response.value[i].Securities[j].Trades;
itemRow2["Buy Qty"] = response.value[i].Securities[j].BuyQuantity;
itemRow2["Sell Qty"] = response.value[i].Securities[j].SellQuantity;
itemRow2["Total Brkg"] = response.value[i].Securities[j].Effective_Brokerage;
itemRow2["Online Brkg"] = response.value[i].Securities[j].Online_Brokerage;
arrForTable2.push(itemRow2);
totalBrokerage = totalBrokerage + parseFloat(response.value[i].Securities[j].Effective_Brokerage);
totalSelfServiceBrokerage = totalSelfServiceBrokerage + parseFloat(response.value[i].Securities[j].Online_Brokerage);
}
totalBrokerage = Math.round(totalBrokerage * 100) / 100;
totalSelfServiceBrokerage = Math.round(totalSelfServiceBrokerage * 100) / 100;
totalRetailBrokerage = Math.round(totalRetailBrokerage * 100) / 100;
var itemRow1 = {};
itemRow1["Account ID"] = response.value[i].AccountId;
itemRow1["Account Name"] = response.value[i].AccountName;
itemRow1["..."] = '<div class="alert alert-info" role="alert">' + buildHtmlTable(arrForTable2, 'table2x' + j, settings) + '<p>Total Brokerage ' + numberWithCommas(totalBrokerage) + '</p></div>';
arrForTable1.push(itemRow1);
arrForTable2 = [];
totalBrokerage = 0;
totalRetailBrokerage = 0;
totalSelfServiceBrokerage = 0;
}
tr.removeClass('loading');
htmlTable1 = buildHtmlTable(arrForTable1, 'table1x' + i);
row.child(htmlTable1).show();
tr.addClass('shown');
}
else {
row.child('<table><tr><td>' + response.value[0].AccountId + '</td></tr></table>').show();
tr.addClass('shown');
};
}
} catch (e) {
console.log(e.message);
}
});
The Problem:
Firefox nicely shows the Progress image after the user clicks it, but Edge and Chrome does not show. Both browsers crossed this piece of code when I was debugging from developer tools of the respective browser.
Its browser compatible problem? Is there a solution for it? Help me please.
In case of chrome there is such an issue while showing the loading bar while making a server call. Please make the following changes where you are making the service call. First add the class loading to the table
tr.addClass('loading');
After that make the service call by giving a timeout function
setTimeout(function(){
var response = organization_GetAccountManagerDetailEarningsAccountData(row.data(), purl2, pcontext);
......
//Your service calls and response call backs
},1);
On providing a timeout (say 1ms), Chrome will get the time to bind the loading bar to DOM, In other case the DOM Object is not available to show the spinner.

How to Set the value of an angular variable to another variable dynamically

I have a slideshow on my website with left and right buttons.
Like this (http://i.prntscr.com/863ad10cfd4e4f1ea9b90721cc6582e8.png).
I am using angular to change the image on left and right.
As you can see in the function I increase the value
/*SlideShow Pictures*/
$scope.picture_1 = "./images/photos/watch.jpg";
$scope.picture_2 = "./images/photos/watch.jpg";
$scope.picture_3 = "./images/photos/watch.jpg";
$scope.picture_4 = "./images/photos/watch.jpg";
$scope.picture = $scope.picture_1;
$scope.picture_value = 1;
$scope.image_change_right = function () {
if ($scope.picture_value < 4)
{
$scope.picture_value = $scope.picture_value + 1;
$scope.picture = ('$scope.picture_' + $scope.picture_value);
console.log($scope.picture_value);
}
else{
$scope.picture_value = 1;
$scope.picture = ('$scope.picture_' + $scope.picture_value);
console.log($scope.picture_value);
}
}
Above is the function called for button right press.
The function increases the variable by 1 and adds it to the string to call the new variable. In the console log it looks great! However I think it is only showing as a string --- it is not actually setting the value of scope.picture to the variable.
How can I set this to not be a string but as a valid variable?
Thanks everyone!
A better way would be like this:
The Controller:
// The array of picture links.
$scope.pictures = [
"./images/photos/watch.jpg",
"./images/photos/watch.jpg",
"./images/photos/watch.jpg",
"./images/photos/watch.jpg"
];
$scope.current = 0; // Initialize the current pictures place in the array.
$scope.picture = $scope.pictures[$scope.current]; // Set the current picture.
// The direction is either 1 or -1;
$scope.changePicture = function (direction) {
$scope.current += direction; // add or remove one depending on direction.
$scope.current %= $scope.pictures.length; // Normalize the number based on the length of the pictures array.
console.log($scope.picture);
}
The Html:
<img src="{{picture}}">
<button ng-click="changePicture(1)">Next</button>
<button ng-click="changePicture(-1)">Previous</button>
Why don't you use an array with image links like this?
/*SlideShow Pictures*/
$scope.pictures = ["./images/photos/watch.jpg", "./images/photos/watch.jpg", "./images/photos/watch.jpg", "./images/photos/watch.jpg"];
$scope.picture = $scope.pictures[0];
$scope.picture_value = 0;
$scope.image_change_right = function () {
if ($scope.picture_value < 4)
{
$scope.picture_value = $scope.picture_value + 1;
$scope.picture = $scope.pictures[$scope.picture_value];
console.log($scope.picture_value);
}
else{
$scope.picture_value = 0;
$scope.picture = $scope.pictures[$scope.picture_value];
console.log($scope.picture_value);
}
}

Is there any way to track Knockout Observable Array item is displayed in UI?

I am using foreach with pagination for displaying the notification list to the user. Now I want to know whether the item is displayed for the user in screen enough time to update the notification status from New to Viewed. I cannot update it to all the rendered item since some of the items might not be displayed since I am using pagination also I want to mark it as updated after displaying it enough time(Eg 5 sec)
Knockout has a very handy rateLimit extension you can use to perform delayed updates. By creating a computed copy of your current page observable, and extending it to only notify you 5 seconds after it has stopped changing, you can update the items on that page to a read status. For example:
var delayedPage = ko.computed(function() {
// Loop through the items that are rendered (a computed of `page`)
// Note: this makes the computed impure
itemsOnDisplay().forEach(function(item) {
// Set an observable status property of their viewmodel
item.read(true);
});
// This creates the subscription to page changes
return page();
}, this).extend({
rateLimit: {
timeout: 5000,
method: "notifyWhenChangesStop"
}
});
In a working example:
A collection of items with a boolean read observable
An observable page property that tells us what page we're on
A computed set of itemsOnDisplay that holds the items that are currently rendered
A rate limited reflection of the current page that updates 5 seconds after the last page change
var ViewModel = function(data) {
this.itemsPerPage = 6;
this.page = ko.observable();
this.items = ko.observableArray(data);
this.displayItems = ko.pureComputed(function() {
var start = this.page() * this.itemsPerPage;
var end = start + this.itemsPerPage;
return this.items().slice(start, end);
}, this);
this.canGoBack = ko.pureComputed(function() {
return this.page() > 0;
}, this);
this.canGoForward = ko.pureComputed(function() {
return (this.page() + 1) * this.itemsPerPage < this.items().length;
}, this);
// The important part:
this.delayedPage = ko.computed(function() {
var currentPage = this.page();
if (typeof currentPage === "undefined") return null;
this.displayItems().forEach(function(item) {
item.read(true);
});
console.log("Read items on page " + currentPage);
return currentPage;
}, this).extend({ rateLimit: { timeout: 5000, method: "notifyWhenChangesStop" } });
this.page(0);
}
ViewModel.prototype.prev = function() {
this.page(Math.max(this.page() - 1, 0));
};
ViewModel.prototype.next = function() {
this.page(Math.min(this.page() + 1, Math.ceil(this.items().length / this.itemsPerPage)));
};
var myData = [];
for (var i = 0; i < 50; i += 1) {
myData.push({
label: "Item " + i,
read: ko.observable(false)
});
}
var vm = new ViewModel(myData);
ko.applyBindings(vm);
li::after {
content: "new";
font-style: italic;
margin-left: 1rem;
padding: .25rem;
background: orange;
}
.is-read {
background: #efefef;
}
.is-read::after {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: displayItems">
<li data-bind="text: label, css: {'is-read': read }"></li>
</ul>
<button data-bind="click: prev, enable: canGoBack">prev</button>
<button data-bind="click: next, enable: canGoForward">next</button>

How to setInterval() and removeInterval() in this situation?

I understand that in order to remove an interval you need a stored reference to it, so I figured I'll store function returns in a global array.
But why when I click on a cell the intervals keep going and only one stops (I saw first and last cell to stop flashing)?
What I wanted it to do is append a continuous fadeTo on all cells of a table row (excluding first one), and a listener that on clicking any of these cells would stop animations on all of them.
Here's my best effort so far (jsfiddle):
var intervals;
var target = $('#table');
var qty = target.find('td:contains(Price)');
var row = qty.closest('tr');
arr = new Array();
row.find('td').each( function() {
if ($(this).text() !== "Price" ) {
intervals = new Array();
addAnimation($(this));
}
});
function addAnimation(cell) {
var intv = setInterval(function() {
cell.fadeTo("slow", 0.3);
cell.fadeTo("slow", 1);
}, 1000);
intervals.push(intv);
cell.click(function() {
for (var i = 0; i < intervals.length; intervals++) {
window.clearInterval(intervals[i]);
}
});
}
You are instantiating the intervals array multiple times and incrementing the wrong parameter in the for loop:
var intervals = [],
target = $('#table'),
qty = target.find('td:contains(Price)'),
row = qty.closest('tr');
row.find('td').each( function() {
if ($(this).text() !== "Price" ) {
addAnimation($(this));
}
});
function addAnimation(cell) {
var intv = setInterval(function() {
cell.fadeTo("slow", 0.3);
cell.fadeTo("slow", 1);
}, 1000);
intervals.push(intv);
cell.click(function() {
for (var i = 0; i < intervals.length; i++) {
window.clearInterval(intervals[i]);
}
$(this).stop();
});
}
See: fiddle
Your other problem is here:
var intervals;
...
if ($(this).text() !== "Price" ) {
intervals = new Array();
addAnimation($(this));
That creates a new array each time. You should be initialising intervals when you declare it and delete the line creating a new array in the if block:
var intervals = [];
...
if ($(this).text() !== "Price" ) {
addAnimation($(this));
}
However, you may wish to run this more than once, so you should clear out the array when you clear the intervals, something like:
function addAnimation(cell) {
var intv = setInterval(function() {
cell.fadeTo("slow", 0.3);
cell.fadeTo("slow", 1);
}, 1000);
intervals.push(intv);
cell.click(function() {
for (var i = 0; i < intervals.length; intervals++) {
window.clearInterval(intervals[i]);
}
// reset the array
intervals = [];
});
}
or replace the for loop with something like:
while (intervals.length) {
window.clearInterval(intervals.pop());
}
which stops the intervals and clears the array in one go. :-)

Categories

Resources