this is my first post on SO, so please go easy on me :)
I am building a web app with Durandal.js and I have a situation where I am running a simple knockout foreach data-bind that iterates over a ko.observableArray and composes child views for each item in the array. This array is made up of a number of child view/viewmodels that I want to render on the page, but the array can contain a large number of items at a time (sometimes over 400). The app also can have multiple pages of results and I am handling the paging by replacing the contents of the ko.observableArray when a new page is clicked on.
The issue that I am running into on occasion (it's not every time, but it is repeatable) is that I am getting an error thrown by knockout.js (I am running knockout v3.0.0) that says the following: "Uncaught TypeError: Cannot read property 'insertBefore' of null".
Here is the part of knockout.js's virtualElements' prepend method which is throwing the error:
prepend: function(containerNode, nodeToPrepend) {
if (!isStartComment(containerNode)) {
if (containerNode.firstChild)
containerNode.insertBefore(nodeToPrepend, containerNode.firstChild);
else
containerNode.appendChild(nodeToPrepend);
} else {
// Start comments must always have a parent and at least one following sibling (the end comment)
containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling);
}
}
After digging around in the knockout code I can see that what is being called null is the containerNode.parentNode. In my html code snippet below, the containerNode.parentNode is the #photosContainer, so I find it strange that that is null since the foreach is obviously still running (since I am getting an error thrown for each item that hasn't been rendered yet).
Here are some code snippets from my durandal app: html code from my view, which contains the foreach binding that iterates over the 'photos' observableArray and composes a child view:
<div id='photosContainer' data-bind="visible: !video_mode_on(), foreach: photos">
<!-- ko compose: $data --><!--/ko-->
</div>
and here is code from my viewmodel that basically replaces the contents of the 'photos' observableArray:
newHome.prototype.getPhotos = function() {
var self = this;
var defs = [];
var arr = [];
var args, newArr;
self.rawPhotos().forEach( function( set ) {
defs.push( self.some_more_all( set.mf, set.images ) );
});
$.when.apply($, defs).done(function( res ) {
args = Array.prototype.slice.call(arguments, 0);
newArr = args.sort();
newArr.forEach( function( set ) {
set.arr.forEach( function( p ) {
arr.push( self.addPhoto( p, self.mfOwnedByViewer( set.mf ) ? { ownedByViewer: true } : { ownedByViewer: false, owner_uuid: set.mf.owner_uuid } ) );
});
});
self.photos( arr );
});
};
The error that I mentioned seems to occur when I navigate to another page of results before the foreach binding has completed running in my view. So my question is: is there a way to stop or break out of the foreach binding in my view so that I can navigate to another page of results without having to wait for the current list of results to render?
Also some things that I've tried, but have not helped: I was initially removing all the contents of the photos array and then pushing into it directly when running my getPhotos() function, but thought that the error may have been caused by an empty array. I have also tried to run knockout's ko.virtualElements.emptyNode() function on the #photosContainer before iterating over the rawPhotos() array in my getPhotos() function to see if that would empty out the unfinished renderings in the view's foreach binding, but that didn't seem to do anything either. I've tried to wrap my compose binding with a check function to see if the #photosContainer node exists like this:
<div id='photosContainer' data-bind="visible: !video_mode_on(), foreach: photos">
<!-- ko if: $parent.checkForPhotosContainer() -->
<!-- ko compose: $data --><!--/ko-->
<!-- /ko -->
</div>
and the check function:
newHome.prototype.checkForPhotosContainer = function() {
var self = this;
var el = $(self.element).find('#photosContainer');
if( el.length ) {
return true;
} else {
return false;
}
};
So far nothing has worked and I'm running out of ideas (and hair!!). Any help would be greatly appreciated!
It seems that there is no way to stop or break out of a ko
foreach
binding as pointed out by Magnus Ahlin's link to How to short circuit Array.forEach like calling break?, however the root cause of my issue was actually related to my use of virtual elements rather than normal elements.For anyone who runs into the issue of knockout complaining about "Uncaught TypeError: Cannot read property 'insertBefore' of null" and is using virtual elements to compose child views in a ko.observableArray by using the
foreach
binding, try substituting your virtual elements with normal elements.So replace html code in your view similar to this:
with something like this: