116

I'm building functionality onto a webpage which the user can perform multiple times. Through the user's action, an object/model is created and applied to HTML using ko.applyBindings().

The data-bound HTML is created through jQuery templates.

So far so good.

When I repeat this step by creating a second object/model and call ko.applyBindings() I encounter two problems:

  1. The markup shows the previous object/model as well as the new object/model.
  2. A javascript error occurs relating to one of the properties in the object/model, although it's still rendered in the markup.

To get around this problem, after the first pass I call jQuery's .empty() to remove the templated HTML which contains all the data-bind attributes, so that it's no longer in the DOM. When the user starts the process for the second pass the data-bound HTML is re-added to the DOM.

But like I said, when the HTML is re-added to the DOM and re-bound to the new object/model, it still includes data from the the first object/model, and I still get the JS error which doesn't occur during the first pass.

The conclusion appears to be that Knockout is holding on to these bound properties, even though the markup is removed from the DOM.

So what I'm looking for is a means of removing these bound properties from Knockout; telling knockout that there is no longer an observable model. Is there a way to do this?

EDIT

The basic process is that the user uploads a file; the server then responds with a JSON object, the data-bound HTML is added to the DOM, then the JSON object model is bound to this HTML using

mn.AccountCreationModel = new AccountViewModel(jsonData.Account);
ko.applyBindings(mn.AccountCreationModel);

Once the user has made some selections on the model, the same object is posted back to the server, the data-bound HTML is removed from then DOM, and I then have the following JS

mn.AccountCreationModel = null;

When the user wishes to do this once more, all these steps are repeated.

I'm afraid the code is too 'involved' to do a jsFiddle demo.

awj
  • 7,482
  • 10
  • 66
  • 120
  • Calling ko.applyBindings multiple times especially on the same containing dom element is not recommended. There may be another way to achieve what you want. You will need to provide more code though. Please include a jsfiddle if possible. – madcapnmckay Apr 06 '12 at 21:10
  • Why not exposes an `init` function in which you pass the data to be applied? – KyorCode Jan 29 '14 at 09:59

10 Answers10

175

Have you tried calling knockout's clean node method on your DOM element to dispose of the in memory bound objects?

var element = $('#elementId')[0]; 
ko.cleanNode(element);

Then applying the knockout bindings again on just that element with your new view models would update your view binding.

Eat at Joes
  • 4,937
  • 1
  • 40
  • 40
KodeKreachor
  • 8,852
  • 10
  • 47
  • 64
  • 34
    This works - thanks. I'm unable to find any documentation on this method, though. – awj Apr 07 '12 at 07:43
  • I too have searched for documentation on this knockout utility function. As near as I can tell from the source code, it is calling `delete` on certain keys on the dom elements themselves, which is apparently where all the knockout magic is stored. If anyone has a source on documentation, I would be much obliged. – Patrick M Nov 09 '12 at 15:40
  • 2
    You won't find it. I've searched high and low for docs on ko utility functions, but none exists. This blog post is the closest thing you'll find, but it only covers members of ko.utils: http://www.knockmeout.net/2011/04/utility-functions-in-knockoutjs.html – Nick Daniels Dec 28 '12 at 17:14
  • 1
    You'll also want to manually remove events as shown in my answer below. – Mike B Jan 21 '13 at 21:52
  • It is probably better not to use internal methods like this. It would be better to clear the ViewModel's applicable property using something like `.removeAll()` or just assigning it to a null observable. – Zac Oct 22 '13 at 19:12
  • 1
    @KodeKreachor I had already posted the working example below. My point was that it might be better to keep the binding intact, while releasing the data within the ViewModel instead. That way you never have to deal with un/re-binding. It seems cleaner than using undocumented methods to manually unbind directly from the DOM. Beyond that, CleanNode is problematic because it does not release any of the event handlers (see the answer here for more detail: http://stackoverflow.com/questions/15063794/can-cleannode-be-used-to-clean-binding) – Zac Oct 22 '13 at 21:53
  • The closest I found to clean-up logic documented is this: http://knockoutjs.com/documentation/custom-bindings-disposal.html – Phil May 15 '14 at 22:10
  • You can still [read the source code](https://github.com/knockout/knockout/blob/master/src/utils.domNodeDisposal.js). – totymedli Dec 20 '16 at 11:54
32

For a project I'm working on, I wrote a simple ko.unapplyBindings function that accepts a jQuery node and the remove boolean. It first unbinds all jQuery events as ko.cleanNode method doesn't take care of that. I've tested for memory leaks, and it appears to work just fine.

ko.unapplyBindings = function ($node, remove) {
    // unbind events
    $node.find("*").each(function () {
        $(this).unbind();
    });

    // Remove KO subscriptions and references
    if (remove) {
        ko.removeNode($node[0]);
    } else {
        ko.cleanNode($node[0]);
    }
};
Mike B
  • 2,660
  • 3
  • 20
  • 22
  • Just one caveat, I haven't tested re-binding to something that's just had `ko.cleanNode()` called and not the entire html replaced. – Mike B Nov 21 '12 at 03:57
  • 4
    wouldn't your solution unbind all other event bindings as well? is there a possibility to only remove ko's event handlers? – lordvlad Feb 13 '13 at 16:54
  • without altering the ko core that is – lordvlad Feb 13 '13 at 17:16
  • 1
    True, however that's not possible as far as I can tell without a core alteration. See this issue I brought up here: https://github.com/SteveSanderson/knockout/issues/724 – Mike B Feb 15 '13 at 16:53
  • Isn't the idea of KO that you should very rarely be touching the dom yourself? This answer loops through the dom , and would certainly be far from unusable in my use case. – Blowsie May 14 '13 at 09:55
  • However, you may want to get rid of a knockout instance without refreshing the page. If you do that, you need to remove all the event handlers or you'll have a memory leak on your hands. – Mike B May 23 '13 at 22:04
  • Worked for me on my approach to clean and rebind. I think the `$(this).unbind();` did the job. Thanks! – Koscik Aug 22 '16 at 06:20
12

You could try using the with binding that knockout offers: http://knockoutjs.com/documentation/with-binding.html The idea is to use apply bindings once, and whenever your data changes, just update your model.

Lets say you have a top level view model storeViewModel, your cart represented by cartViewModel, and a list of items in that cart - say cartItemsViewModel.

You would bind the top level model - the storeViewModel to the whole page. Then, you could separate the parts of your page that are responsible for cart or cart items.

Lets assume that the cartItemsViewModel has the following structure:

var actualCartItemsModel = { CartItems: [
  { ItemName: "FirstItem", Price: 12 }, 
  { ItemName: "SecondItem", Price: 10 }
] }

The cartItemsViewModel can be empty at the beginning.

The steps would look like this:

  1. Define bindings in html. Separate the cartItemsViewModel binding.

      
        <div data-bind="with: cartItemsViewModel">
          <div data-bind="foreach: CartItems">
            <span data-bind="text: ItemName"></span>
            <span data-bind="text: Price"></span>
          </div>
        </div>
      
    
  2. The store model comes from your server (or is created in any other way).

    var storeViewModel = ko.mapping.fromJS(modelFromServer)

  3. Define empty models on your top level view model. Then a structure of that model can be updated with actual data.

      
        storeViewModel.cartItemsViewModel = ko.observable();
        storeViewModel.cartViewModel = ko.observable();
     
    
  4. Bind the top level view model.

    ko.applyBindings(storeViewModel);

  5. When the cartItemsViewModel object is available then assign it to the previously defined placeholder.

    storeViewModel.cartItemsViewModel(actualCartItemsModel);

If you would like to clear the cart items: storeViewModel.cartItemsViewModel(null);

Knockout will take care of html - i.e. it will appear when model is not empty and the contents of div (the one with the "with binding") will disappear.

Sylwester Gryzio
  • 997
  • 6
  • 12
9

I have to call ko.applyBinding each time search button click, and filtered data is return from server, and in this case following work for me without using ko.cleanNode.

I experienced, if we replace foreach with template then it should work fine in case of collections/observableArray.

You may find this scenario useful.

<ul data-bind="template: { name: 'template', foreach: Events }"></ul>

<script id="template" type="text/html">
    <li><span data-bind="text: Name"></span></li>
</script>
aamir sajjad
  • 3,019
  • 1
  • 27
  • 26
  • 1
    I've spent at least 4 hours trying to solve similar issue and only aamir's solution works for me. – Antonin Jelinek Apr 17 '13 at 11:39
  • @AntoninJelinek the other thing which I experienced in my scenario, to completely remove the html, and dynamicaly append it again to absolutly remove everything. for example i have Knockout code container div
    on $.Ajax Success result I do following each time the server method calls $("#knockoutContainerDiv").children.remove();//remove content of it call the method to append dynamic html with knockout code $("#knockoutContainerDiv").append("childelements with knockout binding code") and calling applyBinding again
    – aamir sajjad Apr 18 '13 at 03:33
  • 1
    Hey ammir, i had pretty much the same scenario as you. Everything that you mentioned worked great except the fact that I had to use ko.cleanNode(element); prior every re-binding. – Radoslav Minchev Jul 29 '14 at 21:26
  • @RadoslavMinchev do you think I can help you further, if yes then how, I ll be happy to share my thoughts/experience about particular question. – aamir sajjad Jul 30 '14 at 05:31
  • Thx. @aamirsajjad, just wanted to mentioned that what worked for me was to call the cleanNode() function to make it works. – Radoslav Minchev Jul 30 '14 at 08:22
  • Thanks!! I'm using knockout with SharePoint and trying to re-apply bindings would be a nightmare. This saved me from days of work. – Stephen Gilboy May 27 '15 at 18:45
  • @Antonin Jelinek. Nazdar Tonda! Nasel jsem te tady nahodou. – Sentinel Dec 19 '16 at 10:14
  • Glad to have found this answer. I had a similar situation with an observableArray not working in foreach binding. The problem raises from knockout holding the foreach template in domData that gets cleaned and lost upon DOM node removal if the array is empty. The template binding with foreach solved this (as the template element now is always available). – Jamby Apr 12 '17 at 11:51
6

Instead of using KO's internal functions and dealing with JQuery's blanket event handler removal, a much better idea is using with or template bindings. When you do this, ko re-creates that part of DOM and so it automatically gets cleaned. This is also recommended way, see here: https://stackoverflow.com/a/15069509/207661.

Community
  • 1
  • 1
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
4

I think it might be better to keep the binding the entire time, and simply update the data associated with it. I ran into this issue, and found that just calling using the .resetAll() method on the array in which I was keeping my data was the most effective way to do this.

Basically you can start with some global var which contains data to be rendered via the ViewModel:

var myLiveData = ko.observableArray();

It took me a while to realize I couldn't just make myLiveData a normal array -- the ko.oberservableArray part was important.

Then you can go ahead and do whatever you want to myLiveData. For instance, make a $.getJSON call:

$.getJSON("http://foo.bar/data.json?callback=?", function(data) {
    myLiveData.removeAll();
    /* parse the JSON data however you want, get it into myLiveData, as below */
    myLiveData.push(data[0].foo);
    myLiveData.push(data[4].bar);
});

Once you've done this, you can go ahead and apply bindings using your ViewModel as usual:

function MyViewModel() {
    var self = this;
    self.myData = myLiveData;
};
ko.applyBindings(new MyViewModel());

Then in the HTML just use myData as you normally would.

This way, you can just muck with myLiveData from whichever function. For instance, if you want to update every few seconds, just wrap that $.getJSON line in a function and call setInterval on it. You'll never need to remove the binding as long as you remember to keep the myLiveData.removeAll(); line in.

Unless your data is really huge, user's won't even be able to notice the time in between resetting the array and then adding the most-current data back in.

Zac
  • 1,569
  • 1
  • 11
  • 10
  • In the time since posting the question, this is what I now do. It's just that some of these Knockout methods are either undocumented or you really need to look around (already knowing the function name) to find what it does. – awj Oct 22 '13 at 13:36
  • I had to look very hard to find this as well (when the number of hours digging through docs exceeds the resultant number of lines of code... wow). Glad you got it working. – Zac Oct 22 '13 at 17:11
2

I had a memory leak problem recently and ko.cleanNode(element); wouldn't do it for me -ko.removeNode(element); did. Javascript + Knockout.js memory leak - How to make sure object is being destroyed?

Community
  • 1
  • 1
Matas Vaitkevicius
  • 58,075
  • 31
  • 238
  • 265
  • In knockout 3.1, ko.removeNode actually calls ko.cleanNode. I don't know that that was the case for earlier versions, though. – kiprainey May 20 '14 at 14:35
1

Have you thought about this:

try {
    ko.applyBindings(PersonListViewModel);
}
catch (err) {
    console.log(err.message);
}

I came up with this because in Knockout, i found this code

    var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
    if (!sourceBindings) {
        if (alreadyBound) {
            throw Error("You cannot apply bindings multiple times to the same element.");
        }
        ko.utils.domData.set(node, boundElementDomDataKey, true);
    }

So to me its not really an issue that its already bound, its that the error was not caught and dealt with...

Andy
  • 2,124
  • 1
  • 26
  • 29
0

I have found that if the view model contains many div bindings the best way to clear the ko.applyBindings(new someModelView); is to use: ko.cleanNode($("body")[0]); This allows you to call a new ko.applyBindings(new someModelView2); dynamically without the worry of the previous view model still being binded.

  • 4
    There's a couple of points I'd like to add: (1) this will clear ALL bindings from your webpage, which may suit your application but I imagine there are plenty of applications where bindings have been added to multiple parts of the page for separate reasons. It may not be helpful to many users to clean ALL bindings in a single, sweeping command. (2) a quicker, more efficient, native JavaScript method of retrieving `$("body")[0]` is `document.body`. – awj Jul 04 '13 at 08:15
0
            <div id="books">
                <ul data-bind="foreach: booksImReading">
                    <li data-bind="text: name"></li>
                </ul>
            </div>
            
            var bookModel = {
                booksImReading: [
                    { name: "Effective Akka" }, 
                    { name: "Node.js the Right Way" }]
            };
                                        
            ko.applyBindings(bookModel, el);
            
            var bookModel2 = {
                booksImReading: [
                    { name: "SQL Performance Explained" },
                    { name: "Code Connected" }]
            };
            
            ko.cleanNode(books);
            ko.applyBindings(bookModel2, books);
Dhara Tank
  • 11
  • 2
  • Hi Dhara! Even your code may look simple and a good answer, it's always recommended to add some explanation of your own in order to help the others to understand the idea you pursuit. Thanks! – juagicre Jun 23 '21 at 13:33