knockout.jsko-custom-binding

Slick.js carousel with Knockout custom binding


I wanted to use the awesome slick carousel (http://kenwheeler.github.io/slick/) with Knockout. After looking at some of the other questions (https://stackoverflow.com/questions/26368176/slick-js-carousel-not-working-with-knockout-template-binding) it seems like the way to go would be to use a custom binding, but I'm having trouble getting it to load properly - it seems like the slick carousel is initializing before the DOM has completely loaded so it doesn't connect the last slide to the first slide. I also looked at knockout js custom binding called after internal dom elements rendered and am trying to use child binding contexts to have the custom binding force the child elements to bind first.

Here is the html markup:

<div data-bind = "slick">
    <!-- ko foreach: results -->
      <div>
        <result-thumbnail params="result:$data"></result-thumbnail>
      </div>
    <!-- /ko -->
</div>

result-thumbnail is a separate knockout component that formats the data I wanted to present in the carousel. In the custom bindings file:

ko.bindingHandlers.slick = {
init: function(element, valueAccessor, allBindingsAccessor, data, bindingContext) {
    var options = {
        infinite:true,
        slidesToShow: 3,
        slidesToScroll: 1,
    };

    var childBindingContext = bindingContext.createChildContext(
        bindingContext.$rawData, 
        null, 
        function(context) {
            ko.utils.extend(context, valueAccessor());
        });
    ko.applyBindingsToDescendants(childBindingContext, element);

    var local = ko.utils.unwrapObservable(valueAccessor());
    ko.utils.extend(options, local);
    $(element).slick(options);
    return { controlsDescendantBindings: true };
},
update: function(element, valueAccessor, allBindingsAccessor, data, context) {
  }
};

A few things I was considering: - Should I move the slick initialization to the update function? - Does the slick custom binding I wrote need to be bound to a different element? - If I were changing the value of the results observable, would I need to update slick by destroying then recreating the carousel?

Any help would be appreciated! I am still learning about custom bindings and if any one has had experience with getting the slick carousel to work with Knockout that would be great to hear about too.


Solution

  • Disclaimer: I have never used slick.js.

    You're right in thinking that the carousel is initializing before the DOM has completely loaded.

    If you search on stackoverflow you'll find many options on how to call a function when all elements have finished rendering - the one I like best is described here: https://stackoverflow.com/a/19941134/3620458.

    Once you have such a callback you can use it to set an observable in your main viewModel to true, like this:

    var allElementsRendered = ko.observable(false);
    var yourAfterRenderFunction = function () {
        allElementsRendered(true);
    }
    

    You can then pass that observable as a parameter to your 'slick' binding, like this:

    <div data-bind = "slick: allElementsRendered">
    

    Whenever the allElementsRendered() observable changes, the 'update' function in your custom binding will be called - so just move the initialization logic there, like this:

        update: function(element, valueAccessor, allBindingsAccessor, data, context) {
            var shouldInit = ko.unwrap(valueAccessor());
    
            if (shouldInit) {
                var options = {
                    infinite:true,
                    slidesToShow: 3,
                    slidesToScroll: 1,
                };
    
                $(element).slick(options);
            } else {
                $(element).slick("unslick");
            }
        }
    

    Handling updating the results observableArray may require some additional work - what you can do is subscribe to it and set the allElementsRendered() to false any time it changes, like this:

    var resultsSubscription = results.subscribe(function(newValue){
        allElementsRendered(false);
    })
    

    This, combined with the above-mentioned afterRenderFunction should hopefully keep your carousel updated.