'Knockout 'if' binding that 'unwraps' a child script

The knockoutjs if binding can be used to show or hide some HTML based on a condition.

However the browser will still 'see' and parse anything inside the if binding before knockout has had a chance to apply bindings. If the contents is an image then that image may get downloaded by the browser even if it would be immediately hidden.

This can be a problem on mobile devices.

I realize one way to solve this problem is using data binding on the image tag, which won't set the source until knockout has ran the bindings:

 <img data-bind="attr: {'src': .....}"/>

For the purpose of this question I don't want to do that (the contents is much more complicated than a single image). So another solution is to use template binding within an if binding like this :

 <!-- ko if: model.banner() == 'cat' -->

    <script type="text/html" id="cat_header">
        <figure>
            <img src="https://largeimages.com/cat.jpg" width="1920" height="1080">
        </figure>
    </script>

    <!-- ko template: { name: 'cat_header' } --><!-- /ko -->

<!-- /ko -->

The script tag is ignored by the browser so the image isn't downloaded, until the bindings are applied and an instance of the template is rendered.

This works just fine, but I feel is a little klunky.

What I'd really like to do is something much simpler with a new binding that automatically unwraps the template from the script tag :

 <!-- ko if2: model.banner() == 'cat' -->

    <script type="text/html">
        <figure>
            <img src="https://largeimages.com/cat.jpg" width="1920" height="1080">
        </figure>
    </script>

 <!-- /ko -->

Or perhaps this which is even shorter:

    <script type="text/html" data-bind="if2: model.banner() == 'cat'">
        <figure>
            <img src="https://largeimages.com/cat.jpg" width="1920" height="1080">
        </figure>
    </script>

I just don't have enough skill in knockout to be able to write the if2 binding, plus I would have really thought somebody else had already done this.

So if anybody has already done this, or it is relatively simple to do I think it would be very useful.



Solution 1:[1]

Before answering: I don't really see the need for such a markup. But I'll try to help without questioning why you want to do it as you described:

The markup you want to use is this:

<script type="text/html" data-bind="if2: model.banner() == 'cat'">
    <figure>
        <img src="https://largeimages.com/cat.jpg" width="1920" height="1080">
    </figure>
</script>

The browser will not look at the content, but knockout will apply data-binds to this type of node: a good start.

I figured that what we're trying to do boils down to this:

  1. Once the if binding value returns true, swap out the script tag for a div
  2. Once the div is in the document, apply the original if data-bind.

So I wrote a custom binding handler that does those two things.

1. Wait until the binding's value returns true for the first time:

 init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
  var initialized = false;

  ko.computed(function() { 
    if (!initialized && ko.unwrap(valueAccessor())) {

2. Copy the contents of the script node to a virtual div

      var scriptTag = element;
      var replacementDiv = document.createElement("div");
      replacementDiv.innerHTML = scriptTag.innerHTML;

3. Apply the original binding with our virtual div instead of the script node

      ko.bindingHandlers.if.init.call(null, 
        replacementDiv, valueAccessor, allBindings, viewModel, bindingContext);

4. Inject the virtual div into the DOM and get rid of the script node

      scriptTag.parentElement.replaceChild(replacementDiv, scriptTag);

5. Wrap up

      initialized = true;
    }
  }, null, { disposeWhenNodeIsRemoved: element });

  return { controlsDescendantBindings: true };
}

A (hopefully) working example:

ko.bindingHandlers.if2 = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    var initialized = false;

    ko.computed(function() {
      if (!initialized && ko.unwrap(valueAccessor())) {
        var scriptTag = element;
        var replacementDiv = document.createElement("div");
        replacementDiv.innerHTML = scriptTag.innerHTML;

        ko.bindingHandlers.if.init.call(null, replacementDiv, valueAccessor, allBindings, viewModel, bindingContext);

        scriptTag.parentElement.replaceChild(replacementDiv, scriptTag);
        initialized = true;
      }
    }, null, {
      disposeWhenNodeIsRemoved: element
    });

    return {
      controlsDescendantBindings: true
    };
  }
}

ko.applyBindings({
  toggled: ko.observable(false)
})
img {
  max-width: 300px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/html" data-bind="if2: toggled">
  <img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_March_2010-1.jpg">
</script>
<div data-bind="text: toggled"></div>
<button data-bind="click: toggled.bind(null, !toggled())">toggle</button>

Solution 2:[2]

Using the ko template is actually a good idea, altough it should more likely serve the purpose of rendering some part of html that you use repeatedly, not only once. That beeing said, i would make the template more general (i suppose you may want to use similar template for other images too), so it accepts a js class representing let´s say an image in this case.

function AnimalImage(){
    var self = this;
    self.imageUrl = null;
    // plus any other attributes you may want to specify for the image

    self.init = function(imageUrl){
        self.imageUrl = imageUrl;
    }
} 

<script type="text/html" id="animal_header">
    <figure>
        <img data-bind="attr: {'src': imageUrl}" width="1920" height="1080">
    </figure>
</script>

Then if you would want to show multiple images, you can use the 'foreach' parameter in template binding, which accepts observableArray of the objects that binds to the template

<div data-bind='template: { name: 'animal_header', foreach: animals }'>                                       
</div>

in viewmodel

self.animals = ko.observableArray();

var cat = new AnimalImage();
cat.init("https://largeimages.com/cat.jpg");
self.animals.push(cat);

var chicken = new AnimalImage();
chicken.init("https://largeimages.com/chicken.jpg");
self.animals.push(chicken);

Or for using the template for a single object multple times

<div data-bind='template: { name: 'animal_header', data: cat }'>                                       
</div>
<div data-bind='template: { name: 'animal_header', data: chicken }'>                                       
</div>

in viewmodel

self.chicken = ko.observable();
self.cat = ko.observable();

var cat = new AnimalImage();
cat.init("https://largeimages.com/cat.jpg");
self.cat(cat);

var chicken = new AnimalImage();
chicken.init("https://largeimages.com/chicken.jpg");
self.chicken(chicken);

Solution 3:[3]

This is forever old, but if you're only trying to hide content until bindings are rendered, then you don't need to hide it in a one-time template.

Instead, just put what you're hiding in a <div style="display: none" data-bind="visible: true">. Until knockout bindings are applied, it won't show.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 user3297291
Solution 2 Martin
Solution 3 Simon_Weaver