53

How is it possible to make knockout data-bind work on dynamically generated elements? For example, I insert a simple html select menu inside a div and want to populate options using the knockout options binding. This is what my code looks like:

$('#menu').html('<select name="list" data-bind="options: listItems"></select>');

but this method doesn't work. Any ideas?

King Julien
  • 10,981
  • 24
  • 94
  • 132

8 Answers8

33

If you add this element on the fly after you have bound your viewmodel it will not be in the viewmodel and won't update. You can do one of two things.

  1. Add the element to the DOM and re-bind it by calling ko.applyBindings(); again
  2. OR add the list to the DOM from the beginning and leave the options collection in your viewmodel empty. Knockout won't render it until you add elements to options on the fly later.
Liam
  • 27,717
  • 28
  • 128
  • 190
PlTaylor
  • 7,345
  • 11
  • 52
  • 94
  • 14
    If i call applyBindings again it throw's an error: Error: You cannot apply bindings multiple times to the same element. – Chris Apr 09 '14 at 08:11
  • 2
    That must be a new feature of the more recent frameworks. The second option is still viable, and honestly the better option to begin with. – PlTaylor Apr 09 '14 at 11:42
  • 1
    Yes i figured as much, it is bad practice to apply bindings more then one time on an element. As it will fire 2 times, i think that's why they added some warning's in KO 3 – Chris Apr 09 '14 at 15:18
17

Knockout 3.3

ko.bindingHandlers.htmlWithBinding = {
          'init': function() {
            return { 'controlsDescendantBindings': true };
          },
          'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
              element.innerHTML = valueAccessor();
              ko.applyBindingsToDescendants(bindingContext, element);
          }
    };

Above code snippet allows you to inject html elements dynamically with the "htmlWithBinding" property. The child elements which are added are then also evaluated... i.e. their data-bind attributes.

Stevanicus
  • 7,561
  • 9
  • 49
  • 70
11

rewrite html binding code or create a new. Because html binding prevents "injected bindings" in dynamical html:

ko.bindingHandlers['html'] = {
  //'init': function() {
  //  return { 'controlsDescendantBindings': true }; // this line prevents parse "injected binding"
  //},
  'update': function (element, valueAccessor) {
    // setHtml will unwrap the value if needed
    ko.utils.setHtml(element, valueAccessor());
  }
};
Salomón
  • 111
  • 1
  • 2
  • 3
    After looking at all the answers and comments on this thread, IMO this is actually the best solution to the question "knockout data-bind on dynamically generated elements". Good solution! – MattSizzle Apr 24 '15 at 18:45
  • 1
    If someone that understands this could edit this to be more comprehensible, I would appreciate it :) – Andrew Feb 22 '19 at 17:14
4

EDIT: It seems that this doesn't work since version 2.3 IIRC as pointed by LosManos

You can add another observable to your view model using myViewModel[newObservable] = ko.observable('')

After that, call again to ko.applyBindings.

Here is a simple page where I add paragraphs dynamically and the new view model and the bindings work flawlessly.

// myViewModel starts only with one observable
     var myViewModel = {
         paragraph0: ko.observable('First')
     };
    
     var count = 0;
    
     $(document).ready(function() {
      ko.applyBindings(myViewModel);
    
      $('#add').click(function() {
       // Add a new paragraph and make the binding
       addParagraph();
       // Re-apply!
       ko.applyBindings(myViewModel);   
       return false; 
      });
     });
    
     function addParagraph() {
      count++;
      var newObservableName = 'paragraph' + count;
         $('<p data-bind="text: ' + newObservableName + '"></p>').appendTo('#placeholder');
      
         // Here is where the magic happens
      myViewModel[newObservableName] = ko.observable('');
      myViewModel[newObservableName](Math.random());
    
      // You can also test it in the console typing
      // myViewModel.paragraphXXX('a random text')
     }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>

<div id="placeholder">
    <p data-bind="text: paragraph0"></p>
</div>
    
<a id="add" href="#">Add paragraph</a>
Zachary Craig
  • 2,192
  • 4
  • 23
  • 34
Ivan Malagon
  • 307
  • 1
  • 3
  • 5
  • 3
    This seems to throw the same "cannot apply bindings twice" error. Is it possible to tell ko.applyBindings() a very specific element you want to add to the bindings? – Chris Jun 05 '14 at 16:39
  • This solution does not address the problem. Generally, the dynamically added element would be of the same type which means it ought to invoke the same handler as should be bound to the VM. Ex: VM.loadData = f(){ handling.. } and would want that to be data bound to any (specific) div that would dynamically be added to the DOM. How can a new element have the same function hooked on its click using your approach? – Vaibhav Oct 06 '14 at 09:54
  • 1
    Check this fiddle: jsfiddle.net/rniemeyer/F7pLN – Vaibhav Oct 06 '14 at 10:21
  • From version 2.3 IIRC it is an error to call applyBindings twice. – LosManos Feb 20 '15 at 23:09
4

For v3.4.0 use the custom binding below:

ko.bindingHandlers['dynamicHtml'] = {
    'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        // setHtml will unwrap the value if needed
        ko.utils.setHtml(element, valueAccessor());
        ko.applyBindingsToDescendants(bindingContext, element);
    }
};
vivanov
  • 1,422
  • 3
  • 21
  • 29
2

It's an old question but here's my hopefully up-to-date answer (knockout 3.3.0):

When using knockout templates or custom components to add elements to prebound observable collections, knockout will bind everything automatically. Your example looks like an observable collection of menu items would do the job out of the box.

johanneslink
  • 4,877
  • 1
  • 20
  • 37
  • Correct answer. Just load the elements into a knockout observable array instead of adding to the page directly. That's what knockout is for. OR, don't bind to the dynamic element until it has been added to the page. – Andrew Feb 22 '19 at 17:29
1

Based on this existing answer, I've achived something similar to your initial intentions:

function extendBinding(ko, container, viewModel) {
    ko.applyBindings(viewModel, container.children()[container.children().length - 1]);
}

function yourBindingFunction() {
    var container = $("#menu");
    var inner = $("<select name='list' data-bind='options: listItems'></select>");
    container.empty().append(inner);


    extendBinding(ko, container, {
        listItems: ["item1", "item2", "item3"]
    });
}

Here is a JSFiddle to play with.

Be warned, once the new element is part of the dom, you cannot re-bind it with a call to ko.applyBindings- that is why I use container.empty(). If you need to preserve the new element and make it change as the view model changes, pass an observable to the viewModel parameter of the extendBinding method.

Community
  • 1
  • 1
Ivaylo Slavov
  • 8,839
  • 12
  • 65
  • 108
0

Checkout this answer: How do define a custom knockout 'options binding' with predefined Text and Value options

ko.applyBindingsToNode is particularly useful.