Browser freezes while appending data to DOM - javascript

Here's the scenario. I've built a grid with min. 1 to max. 132 columns with more than 10k rows. I am using knockout for data binding. The data is fetched by $.ajax function of jquery. On first call it fetches 100 rows and 100 rows on each scroll. If less than 100 it returns all the rows.
The browser does not freeze when ajax is call made but the browser freezes after ajax call. So once we've data at client side, it binds to the table. The browser freezes for a second or two while appending rows with data to the table.
function demoData(args){
var self = this;
self.dataList = ko.observableArray();
self.fillData = function () {
self.tableListPager = new ScrollPagerForAccordian($("#tblGrid"), 40, self.fillDetailData);
$.ajax({ //api call });
self.loadDetailData(data);
}
self.fillDetailData = function (pageNo) {
$.ajax({ //api call });
}
self.loadDetailData = function (res) {
self.dataList.push();
}
function ScrollPagerForAccordian(el, recHeight, callback) {
}
Even I tried to put self.loadDetailData(data) in web worker but still the browser freezes.
Update:
Is there any way to load next 100 rows in background and append it without freezing a browser on a scroll ???
Update-2
Please find below <tbody> structure:
<tbody data-bind="foreach: dataList">
<tr data-bind="foreach: rowValues()">
<td data-bind="style :{ width :colwidth + 'em' , 'min-width' :colwidth + 'em' , 'max-width' :colwidth + 'em' }">
<!-- ko if: displayFlag == true -->
<!-- ko if: flagData == false -->
rendering div
<!-- /ko -->
<!-- ko if: flagData == true -->
rendering another div
<!-- /ko -->
<!-- /ko -->
<!-- ko if: displayFlag == false -->
rendering another div
<!-- /ko -->
</td>
</tr>
</tbody>
How do I prevent browser freezing in such a case ???
I am using Chrome Version 38.0.2125.111 m and have to resolve this issue for chrome only as the view will be displayed in awsomium control.

Related

ko.computed property to determine visibility not working

In my KncokoutJS ViewModel, I have the follow computed property:
self.SelectedUserHasRoles = ko.computed(function () {
if (self.isLoaded()) {
return self.selectedUser().roles().length > 0;
}
return false;
});
And in my HTML, I have the following:
<!-- ko if: isLoaded() -->
<!-- ko if: !SelectedUserHasRoles -->
<div>
<p>User has no Roles.</p>
</div>
<!-- /ko -->
<!-- ko if: SelectedUserHasRoles -->
<div class="roles-wrapper" data-bind="foreach: $root.selectedUser().roles()">
<div class="role-token" data-bind="text: Name"></div>
</div>
<!-- /ko -->
<!-- /ko -->
In my code, I was to say this:
If data from AJAX call has finished loading (isLoaded is true), then for the currently selected user, check and see if he/she has any roles. If yes, then loop through them and show them, if not, show a bit of text saying 'User has no Roles.'
All seems to work, except for the showing User has no Roles text snippet. I've no idea why that isn't showing! I'm putting breakpoints into my computed property and can see that when I select a user with no roles, the expression (in console window) is false, and I'm negating that, so I should see that text snippet!
What am I doing wrong? I've created a screencast to make things easier to understand.
When you want to negate an observable or computed value in a binding, you have to call it explicitly:
<!-- ko if: !SelectedUserHasRoles() -->
In the case of the if binding, there's also the ifnot counterpart:
<!-- ko ifnot: SelectedUserHasRoles -->
I think it's useful to understand why this is needed, since I see it happening a lot.
You could see the data-bind attribute as a comma separated string of key value pairs. Knockout wraps each of the values in a function, which it calls the valueAccessor.
Essentially, you'll go from:
data-bind="if: SelectedUserHasRoles"
to
{
"if": function() { return SelectedUserHasRoles }
}
SelectedUserHasRoles is an observable instance, which evaluates as truthy. When you negate this value using an !, it will always be false.
var myObs = ko.observable("anything");
var valueAccessor = function() { return myObs; };
var valueAccessorNeg = function() { return !myObs; };
console.log(valueAccessor()); // Returns the observable
console.log(valueAccessorNeg()); // Always prints false
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
The valueAccessor function is passed to the init method of a binding. Usually, it is retrieved by calling it, and then unwrapped. Because the unwrap utility doesn't care about whether you pass it an observable or a plain value, you'll not see any errors when you make this mistake.
var myObs = ko.observable(false);
var va1 = function() { return myObs; };
var va2 = function() { return !myObs; };
var va3 = function() { return !myObs(); };
console.log(ko.unwrap(va1())); // false
console.log(ko.unwrap(va2())); // always false
console.log(ko.unwrap(va3())); // true
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
I hope this small peek under the hood might help you (and others that have made this mistake) to be able to determine when the () are needed in the future.
Because you are not binding to a variable but to an expression, you need to add parenthesis here:
<!-- ko if: !SelectedUserHasRoles() -->
//^^ here
See the following snippet
function CreateVM() {
var self = this;
this.isTrue = ko.observable(false);
this.selectedUser = ko.observable();
this.isLoaded = ko.observable();
self.SelectedUserHasRoles = ko.computed(function () {
if (self.isLoaded()) {
return self.selectedUser().roles().length > 0;
}
return false;
});
}
var vm = new CreateVM();
ko.applyBindings(vm);
var userWithRoles = { roles: ko.observableArray([1,2]) };
var userWithoutRoles = { roles: ko.observableArray([]) };
vm.selectedUser(userWithoutRoles);
vm.isLoaded(true);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko if: isLoaded() -->
<!-- ko if: !SelectedUserHasRoles() -->
<div>
<p>User has no Roles.</p>
</div>
<!-- /ko -->
<!-- ko if: SelectedUserHasRoles -->
<div class="roles-wrapper" data-bind="foreach: $root.selectedUser().roles()">
<div class="role-token" data-bind="text: $data"></div>
</div>
<!-- /ko -->
SelectedUserHasRoles: <span class="role-token" data-bind="text: SelectedUserHasRoles"></span>
<!-- /ko -->
See user3297291's answer for more details.
You have the same check for isLoaded() twice, actually
<!-- ko if: isLoaded() -->
<!-- ko if: !SelectedUserHasRoles -->
if isLoaded() evalutes to false, your SelectedUserHasRoles() won't even be evaluated.

Removing metadata-generated table rows in knockout.js

For my first stackoverflow question. Please pardon my general lack of understanding. I'm working on that.
I am auto-generating a table based on user identified dates that have different record values for each day provided. I am referencing a .JSON metagen/metadata file that defines the rows and columns, yet I cannot seem to find a way to filter out the results that are being passed as empty objects. I want to delete the rows without results. I believe I need to create a or reference a viewmodel function inside a binding, but everything I have tried so far has failed.
I have a bindings file that reads in the data and calculates the fields as they are queried based on the record names. The records are then created with knockout foreach statements and the values are taken from a Subgrid() function found in my bindings file. The data is read in as number format.
The View
<!-- ko foreach: $data.cols -->
<th data-bind="text:$data.label === undefined ? $data.value:$data.label"></th>
<!-- /ko -->
</tr>
</thead>
<tbody>
<!-- ko foreach: $data.rows -->
<tr>
<td data-bind="text:$data.label === undefined ? $data.value:$data.label"></td>
<!-- ko foreach: $parent.cols -->
<td data-class="subgridRow"></td>
<!-- /ko -->
</tr>
<!-- /ko -->
The Viewmodel
/*global define,sandbox */
define([
'scalejs.sandbox!subgrid',
], function (
sandbox
) {
'use strict';
return function (node) {
var observable = sandbox.mvvm.observable,
merge = sandbox.object.merge,
text = observable(node.text);
sandbox.flag.invoke('loadUwf');
return merge(node, {
text: text }
The return portion of the bindings file.
//remove values for missing LoB records and return the value
loop1: for (var obj in exdata){
for (var key in exdata[obj]){
if (exdata[obj][key]=== parentValue || ctx.$parent.value === "TOTAL"){
if( !text ){text = 0;}
if (ctx.$parent.value === "TOTAL"){
return {text: recalc.total.toFixed(round) + ' ' + unit,}}
else{return {text: recalc.val.toFixed(round) + ' ' + unit,}
break loop1;}

Update viewmodel when value changes in dynamically added input in knockout

I have been working on KnockoutJS since two weeks and I am trying to add inline editing in a grid using KnockOutJS and jQuery. My html:
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Excerpts</th>
<th>Content</th>
</tr>
</thead>
<tbody data-bind="foreach: Articles">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: Excerpts, event: { dblclick: $root.editField }"></td>
<td data-bind="text: Excerpts, event: { dblclick: $root.editField }"></td>
<td data-bind="text: Content, event: { dblclick: $root.editField }"></td>
</tr>
</tbody>
My JS:
function Articles(Articles) {
this.id = ko.observable(Articles.id);
this.Title = ko.observable(Articles.Title);
this.Excerpts = ko.observable(Articles.Excerpts);
this.Content = ko.observable(Articles.Content);
}
var ViewModel = {
Articles: ko.observableArray
([new Articles(id = 1, Title = "Title1", Excerpts = "Excerpts1", Content = "Content1")]),
loadArticles: function () {
var self = this;
self.Articles(Articles);
},
editField: function (d, e) {
var currentEle = $(e.target);
var value = $(e.target).html();
$(currentEle).html('<input class="thVal" type="text" value="' + value + '" />');
$(currentEle).find('input').focus();
$(currentEle).find('input').keyup(function (event) {
if (event.keyCode == 13) {
$(currentEle).html($(currentEle).find('input').val().trim());
//CallAjaxWithData('/MTB_Articles/EditArticle', 'POST', ko.toJSON(d), null, null); // To update data in server
}
});
$(document).click(function () {
if ($(currentEle).find('input').val() != null) {
$(currentEle).html($(currentEle).find('input').val().trim());
//CallAjaxWithData('/MTB_Articles/EditArticle', 'POST', ko.toJSON(d), null, null); // To update data in server
}
});
}
}
ko.applyBindings(ViewModel);
ViewModel.loadArticles();
Whenever the user double clicks on any td in the grid, I am adding an input field dynamically using the editField function and binding the updated value to the td again when user presses enter key or clicks somewhere else on the page. The parameter d in the editField function gives the current viewmodel object. I have to update the corresponding value in the parameter d when user edits the value in a particular column, convert d to json format and send it to server via ajax call to be updated in the database. The changes made by the user should be reflected in the view model( the parameter d). So how can we update the view model using dynamically added controls?
JSFiddle for this
You can do it in a more 'ko-ish' way that will make it easier for you.
KO is mostly declarative, and you're mixing declarative and procedural (jQuery) code.
To make it declarative, and much easier to implement, do the following:
add an editing observable property to your Articles. Initialize it to false
inside the <td>'s show either the text, or a data-bound input, depending on the value of the editing observable property
use the double click event, to set editing to true
use the enter key press to do what you need (ajax) with the values in your model, and set the editing to false again
You can do it like this:
<td>
<!-- ko ifnot: editing, text: Excerpts --><!-- /ko -->
<!-- ko if: editing -->
<input class="thVal" type="text" data-bind="value: Excerpts" />
<!--- /ko -->
</td>
Or even shorter:
<td>
<!-- ko ifnot: editing, text: Excerpts --><!-- /ko -->
<input class="thVal" type="text" data-bind="value: Excerpts, if: editing" />
</td>

Knockout computed is triggered too many times

I have a viewModel and a template which renders data based on an if condition:
<!-- ko template: { data: selectedFolder, if: isTemplateVisible, name: 'selectedFoldersProperties-template' } --><!-- /ko -->
I think that the template is being rendered 4 times consecutively, and one of the reasons is that isTemplateVisible is a ko.computed.
If i change if: isTemplateVisible to if: selectedFolder, then the template gets rendered 2 times consecutively.
I have a jsfiddle demo.
You will see that "hit" is output-ed for 4 times after the button has been pressed.
Is there a reason why the function gets called so many times?
<button id="button" type="button">
Set folder
</button>
<div>
<!-- ko template: { data: selectedFolder, if: isTemplateVisible, name: 'selectedFoldersProperties-template' } --><!-- /ko -->
</div>
<script type="text/html" id="selectedFoldersProperties-template">
<span data-bind="text: FolderName"></span>
<ul data-bind="foreach: $root.getFiles($data)">
<li>
<span data-bind="text: FileName"></span>
</li>
</ul>
</script>
var viewModel = {
selectedFolder: ko.observable(null),
getFiles: function(folderData) {
console.log("hit");
return [
{ FileName: "File 1" },
{ FileName: "File 2" }
];
}
};
viewModel.isTemplateVisible = ko.computed(function(){
return this.selectedFolder();
}, viewModel);
ko.applyBindings(viewModel);
document.getElementById("button").onclick = function() {
viewModel.selectedFolder({
FolderName: "Folder 1"
});
};
Your isTemplateVisible has a dependency on selectedFolder so that makes you template to re-render.
change isTemplateVisible to not be a computed first:
viewModel.isTemplateVisible = function(){
return this.selectedFolder();
};
and then change your if binding on the template to execute the value out:
if: isTemplateVisible()
then your tmeplate won't run twice due to the dependency.
Here is the working fiddle
Alternatively
You can simply the whole thing by removing the iftemplateisvisible stuff:
<button id="button" type="button">
Set folder
</button>
<div>
<!-- ko if: selectedFolder -->
<!-- ko template: { data: selectedFolder, name: 'selectedFoldersProperties-template' } --><!-- /ko -->
<!-- /ko -->
</div>
<script type="text/html" id="selectedFoldersProperties-template">
<span data-bind="text: FolderName"></span>
<ul data-bind="foreach: $root.getFiles($data)">
<li>
<span data-bind="text: FileName"></span>
</li>
</ul>
</script>
and viewmodel:
var viewModel = {
selectedFolder: ko.observable(null),
getFiles: function(folderData) {
console.log("hit");
return [
{ FileName: "File 1" },
{ FileName: "File 2" }
];
}
};
ko.applyBindings(viewModel);
document.getElementById("button").onclick = function() {
viewModel.selectedFolder({
FolderName: "Folder 1"
});
};
Here is fiddle for the second solution
Further clarification to why you have this issue
If you want to delve deeper in what's going wrong I have created another fiddle which is time stamping your template:
So what is happening is that on the same comment binding you have an observable and a computed which are interdependent, therefore they are causing your template to render twice inseatd of only one time.
So you either as I suggested need to separate them on different bindings so one has resolved by the time you are evaluating the other or you eliminate one of them.
If you change your binding to not have inter-dependency and be for example:
if: selectedFolder(), data: selectedFolder
Then the loop won't occur as you don't have two things dependent on each other and responsible for rendering the template.
Based on this also Throttle is not going to help you as all it does it to delay the value change of an observable which would delay your first render, followed by the interdependency loop caused by cross referencing.
getFiles function runs twice due to being a self executing function on your binding. It tuns once when it's being placed on the page and again when it is its turn to run inside foreach.
This can be demonstrated here where you can see that the callers of your getFiles function are both function (){return $root.getFiles() } from your template.
If you change that function to be foreach: $root.getFiles without the parenthesis and make getFiles an observableArray so it gets resolved by knockout then you won't have the twice execution issue.
That is because you are having foreach binding.
This code <ul data-bind="foreach: $root.getFiles($data)"> will cause to compute it 4 times.
Hope its clear

Knockoutjs throws exception when trying to create series of unordered lists

I'm attempting to create a series of <ul> tags using the foreach: context. The goal is to iterate through the list, and start a new <ul> for every 4th item. My code so far is:
<ul data-bind="foreach: Areas">
<li><span>
<input type="checkbox" data-bind="value: AreaId, checked: $root.AreasImpacted" />
<label><span data-bind="text: Name"></span></label>
</span></li>
<!-- ko if: ($index() % 4 == 0) -->
</ul><ul>
<!-- /ko -->
</ul>
When I do this, I get the exception:
Microsoft JScript runtime error: Cannot find closing comment tag to
match: ko if: ($index() % 4 == 0)
It seems to not like the </li><li> content within the if comment block, probably because the DOM parser is scratching its head on how to actually parse this. If I change it to:
<!-- ko if: ($index() % 4 == 0) -->
<li>Fake!</li>
<!-- /ko -->
Then it'll work perfectly (that is, create a fake <li> every 4th element.
I'm open to other ideas of accomplishing this as well. Thanks!
Yeah, the initial DOM (before Knockout activates) is illegal, and Knockout doesn't work by pasting HTML into the DOM, it actually copies it into a javascript DOM object, which it inserts into the DOM. </ul><ul> isn't a legal object, so Knockout won't be able to turn it into a template. Even if it could, the foreach binding is on the original <ul>, not the new one started by the if, so the Knockout code that added items would still be operating on the first list.
So, in summation, Knockout's foreach and template bindings don't work by building HTML as if it's a string.
You will need a more complex solution.
Something like this would work, but I don't know if this is still what you are going for:
<!-- ko foreach: { data: chunkedList, as: 'areas' } -->
<span>SPLIT!</span>
<ul data-bind="foreach: areas">
<li><span data-bind="text: name"></span></li>
</ul>
<!-- /ko -->
var Viewmodel = function(data) {
var self = this;
self.items = ko.observableArray(data);
self.chunkedList = ko.computed(function() {
var result = [];
var chunk = [];
self.items().forEach(function(item, index) {
if (index % 4 === 0) {
chunk = [];
result.push(chunk);
};
chunk.push(item);
});
return result;
});
};

Categories

Resources