4

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!

jbgarr
  • 1,378
  • 1
  • 12
  • 23
  • http://stackoverflow.com/questions/2641347/how-to-short-circuit-array-foreach-like-calling-break – Magnus Ahlin Dec 16 '14 at 17:53
  • This looks pretty tough! Have you tried using some of the [Knockout debugging tricks](http://www.knockmeout.net/2013/06/knockout-debugging-strategies-plugin.html)? In particular, `
    ` within the binding context of the suspect null value?
    – alex Dec 16 '14 at 21:16
  • @MagnusAhlin Thanks for pointing me to the info regarding general array 'foreach' references. Unfortunately, since I am using knockout 'foreach' was the option I had to go with. – jbgarr Dec 17 '14 at 00:24
  • 1
    @alex That's a great suggestion to use smarter debugging. I was just hacking the ko core to log the info in the console, but it seems much easier to use the tricks you pointed out! I'll keep those in mind for future reference. – jbgarr Dec 17 '14 at 00:26

1 Answers1

3

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:

<div data-bind="foreach: myObservableArray">
    <!-- ko compose: $data --><!--/ko-->
</div>

with something like this:

<div data-bind="foreach: myObservableArray">
    <div data-bind="compose: $data"></div>
</div>
Community
  • 1
  • 1
jbgarr
  • 1,378
  • 1
  • 12
  • 23
  • 2
    Also, it's not a good idea to combine data-bindings that involve the `visible` or `if` binding. Those bindings should be on their own on a parent binding. In fact, before you conclude that the problem lies in the virtual binding, go ahead and test the virtual binding in the presence of the advice I just gave above. Also, do you need to break out of the `foreach` for any purpose other than to solve this problem you've encountered? –  Dec 17 '14 at 01:36
  • @EricTaylor Thanks for the feedback. I tested my situation with updated code to put the `visible` binding on its own in a parent div and then used the virtual elements, but I ran into the same issue, so it seems the virtual elements were the cause of my problem. I will keep the `visible` binding on its own though, so thanks for that advice. Also no, the only reason I wanted to break out of the `foreach` was to solve the error being thrown by knockout in my situation where I was removing items from my `ko.observableArray` before they had been rendered. I didn't need to do it otherwise. – jbgarr Dec 17 '14 at 17:35
  • O.K. That's good to know. I have had trouble with virtual bindings when rendering tables, but have always found a way out of the situation. Glad I could help. –  Dec 17 '14 at 17:43