Ian
Ian

Reputation: 34489

Delay Loading Custom Bindings

I'm working on an Single Page Application and we're using Knockout quite extensively. We've currently got a list of item that can be clicked, and upon doing so they'll load some content into a modal container. The image below illustrates the different items that'll trigger various content to be displayed:

enter image description here

The content of these containers differs substantially, and can have many different custom bindings spread over several tabs. The items in the image are fairly simple and just use Knockout Components but when we start displaying the modal contents they are much more heavy on the JavaScript hence using bindings.

I've recently added in lazy loading of the JavaScript and HTML templates required by the components and this has worked really well. I've had to use a custom component loader as for various reasons we don't want to use require or similar AMD module loader.

Now I'm faced with the same issue with custom knockout bindings, as I expect we could end up with 100 hundred bindings quite easily as this product expands. Unfortunately there doesn't seem to be an obvious way to load custom bindings in a lazy way like components though, and I'm trying to figure out if there's a way to do this, and what the best way would be. Note that I also don't know the name of the binding up front all of the time, sometimes I may wish to load them dynamically based on the name of an observable.

The only things I've managed to find of note so far, are that there is a ko.getBindingHandler() function which can be overridden, but it requires a synchronous load of a binding handler.


I have thought of an approach to try and do this, but it uses components and feels like a really backward way of achieving my end goal. It'd be something like this:

Replace a usual custom binding:

<div data-bind="lineChart: $data"/> 

with

<div data-bind="component { name: compName, params: { vm: $data } }"/>

I'd then use a custom component loader, which is actually just loading the binding handler JavaScript, and writing out essentially a placeholder div with the custom binding in:

var bindingLoader = {
   getConfig: function(name, callback) {
      if(name.startsWith("binding-")) {
         callback({ binding: name.replace("binding-", ""), jsUrl: "/bindings/" + name });
         return;
      }
      callback(null);
   },
  loadComponent(name, componentConfig, callback) {
     var obj = { };
     obj.template = '<div data-bind="' + componentConfig.name + ': $data"/>';
     $.ajax({ url: componentConfig.jsUrl, dataType: "text" })
       .done(function(data)) {
           (new Function(data))();
           callback(obj);
     });
  }
}

I'm sure however there must be a better way of achieving this, but I can't think of any other options right now.

Upvotes: 1

Views: 371

Answers (2)

Michael Best
Michael Best

Reputation: 16688

I've also answered this question on Github.

@Jeroen is right that there's no built-in way to asynchronously load custom bindings. But any binding can "lazily" perform its own action, which is what the component binding does. By overwriting ko.getBindingHandler, we can detect bindings that haven't yet been loaded, and start the loading process, then return a wrapper binding handler that applies the binding once it's loaded.

var originalGetBindingHandler = ko.getBindingHandler;
ko.getBindingHandler = function (bindingKey) {
    // If the binding handler is already known about then return it now
    var handler = originalGetBindingHandler(bindingKey);
    if (handler) {
        return handler;
    }

    if (bindingKey.startsWith("dl-")) {
        bindingKey = bindingKey.replace("dl-", "");
        if (ko.bindingHandlers[bindingKey]) {
            return ko.bindingHandlers[bindingKey];
        }

        // Work out the URL at which the binding handler should be loaded
        var url = customBindingUrl(bindingKey);

        // Load the binding from the URL
        var loading = $.getScript(url);

        return ko.bindingHandlers["dl-" + bindingKey] = {
            init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
                // Once the binding is loaded, apply it to the element
                loading.done(function() { 
                    var binding = {};
                    binding[bindingKey] = valueAccessor;
                    ko.applyBindingAccessorsToNode(element, binding);
                });
                // Assumes that all dynamically loaded bindings will want to control descendant bindings
                return { controlsDescendantBindings: true };
            }
        }
    }
};

http://jsfiddle.net/mbest/e718a123/

Upvotes: 1

Jeroen
Jeroen

Reputation: 63709

AFAIK: No, there is no generic way to lazily load custom bindings.

There are however a lot of options, but we can not recommend any specific one because they'll heavily depend on context. To summarize a few examples:

  • If possible you can use those bindings inside components, and lazily load the components;
  • Depending on what your binding handler does, it can itself delay loading until the latest needed time (e.g. in the init you'll merely register an event callback that will actually load the things you want to load);
  • If you properly use if bindings, any custom bindings inside of that will not be evaluated until needed. The same for foreach bindings, which will not apply custom bindings for array items unless those items are there.
  • You can call applyBindings to specific parts of the DOM only when you're ready to do so.

Et cetera. But again, your question borders on being too broad. Create one (or more?) new questions with actual scenario's, tell us why / how you'd need your custom binding to load lazily, and tell us what approaches you've tried and why they didn't work.

Upvotes: 0

Related Questions