1
votes

I want to use the Knockout.js If binding but include animation. I am using a single view model container to allow for a large number of separate views to be loaded into the same container. If I use the visibility binding on the "template" the bindings are hidden and all throw errors since their view model is not currently loaded. I am fearful these view models will begin to slowdown the page if everything is loaded.

From the knockout If documentation:

The if binding, however, physically adds or removes the contained markup in your DOM, and only applies bindings to descendants if the expression is true.

The knockout animated transitions documentation creates a custom binding using jQuery's Show/Hide functions. However, these also hide/show the DOM elements.

In short I am trying to learn one of two things.

What would be the appropriate way to remove/add DOM elements in jQuery so that it may be used in a custom binding?

Or

How does the if binding in knockout work, so that I may reverse engineer it?

Clarifying Edit:

To clarify more how the code is setup. The admin section of the site that raised this question will contain a place to edit all of the standard content pages, and access reports for the business.

Html "templates" are stored as such (To be clear, these are not knockout templates, but rather html files that contain data-bindings. This could be changed with a compelling reason.)

Admin
  Page Edit
  User Edit
  etc
Reports 
  Product
  User
  etc

Our javascript is similar to this

BaseViewModel.js: 
  Content view model 

AdminEditViewModels.js: 
  UserEditViewModel
  ContentEditViewModel
  [1 view model per item]

AdminReportsViewModels.js
  [similar to above]

When a link is clicked the main page content view model is loaded into the Base view model and is made visible by the binding that inspired this question. Then each view model has it's own Load to trigger the ajax calls.

self.ViewOrders = function () {
  self.Content("Orders");
  self.ContentVM(new AdminOrdersViewModel());
  self.ContentVM().Load();
}

Right now there are only about 9 different "templates" and we have normalized them the best we can, but it is likely they will grow. The binding would just prevent each "template" from throwing errors into the console.

2
The source code for the "if" binding is available on github, but it looks somewhat complicated: github.com/knockout/knockout/blob/master/src/binding/…Jason Spake
@deblocker actually reminded me via link to another SO question that if you're just trying to solve the problem of bindings being applied to non-existent objects you might be better off just wrapping them in a "with" binding. On the other hand if the concern is loading too many unused objects then it might be best to rethink how you're loading all your templates. It's unusual for a view template to be loaded without a corresponding view-model. Can you share more about how your code is structured?Jason Spake
@JasonSpake I added more onto my question to hopefully clarify. If you have some better practices. I am interested in improving this code.John Pavek
Are you using asynchronous module loading to load the html files or are they all included in the initial main html page? It sounds like they're all in the main page. If so it would probably be best to store them in <script type="text/html"> blocks so they aren't executed and then use them along with the view-models in a knockout-template. See knockoutjs.com/documentation/template-binding.htmlJason Spake

2 Answers

2
votes

Using the fadeIn/fadeOut example you mentioned I tried to create a binding that performs fading on the element, and then initializes an "if" binding on the element's inner content by wrapping that content in a new div. The if binding is then passed a new observable property that gets set using the callback from jQuery's fade function. It feels a little hacky and probably doesn't work in overly complex scenarios, but perhaps you or the SO community can improve upon it.

var viewModel = function(){
    var self = this;
    
    self.showContent = ko.observable(false);
    self.content = ko.observable("content goes here");    
}

//Uses the IF binding to remove the element's content from the DOM, but also fades before/after.
ko.bindingHandlers.fadedIf = {
    init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
        // Initially set the element to be instantly visible/hidden depending on the value
        var value = valueAccessor();
        //If the value is a normal function make it a computed so that it updates properly
        if (!ko.isObservable(value)) {
            value = ko.computed({ read: valueAccessor });
        }
        //attach our observable property to the accessor so that it can be used in the update function
        valueAccessor.domShown = ko.observable(ko.unwrap(value));

        //Wrap any contents of the element in a new div, and then bind that div using the "if" binding.
        //This way the element and its event hooks for fading in/out never leaves the dom, but all content does.
        //it also prevents applying multiple bindings to the same element.
        var contentWrapper = $(element).children().wrapAll(document.createElement("div")).parent()[0];
        ko.applyBindingAccessorsToNode(contentWrapper, { 'if': function () { return valueAccessor.domShown } }, bindingContext);  
    },
    update: function (element, valueAccessor) {
        // Whenever the value subsequently changes, slowly fade the element in or out
        var value = valueAccessor();

        if (ko.unwrap(value)) {
            valueAccessor.domShown(true); //restore the element to the DOM
            $(element).fadeIn();
        } else {
            $(element).fadeOut({
                complete: function () {
                    valueAccessor.domShown(false); //remove the element from the DOM
                }
            });
        }
    }
};
  
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div style="border: 1px solid blue; width:600px; margin:auto; padding: 32px; text-align:center;">
  Show Content<input type="checkbox" data-bind="checked: showContent">
  <br/>
  
  <div data-bind="fadedIf: showContent">
    <div style="background-color: silver; padding: 20px;">
      <h3 data-bind="text: content"></h3>
    </div>
  </div>
  
</div>
2
votes

If you are using a template for your separated views, then you could get the afterRender callback by using a factory function to initialize the template.

Here is a simply stub for that purpose:

ko.components.register("ItemTemplate", {
    viewModel:  function(params) {
        function Item(params) {
            var self = this;
            // observables
            self.enhance = function(elements) {
                // enhance/animate here the DOM elements
            };
        }
        Item.prototype.dispose = function() {
            // dispose what has been created inside here
        };
        var item = new Item(params);
        return item;
    },
    template: '<div data-bind="template: {afterRender: enhance}">'+
            // component markup
            '</div>',
    synchronous: true
});