1

I'm using knockout for an SPA application.

I'm managing this by loading up HTML in to an element object in javascript, and then attaching/detaching that element object as needed.

(The code in this question is TypeScript, in-case you think it's a bit funny looking for JavaScript.)

Loading

this.Element = $(this.Template)[0];
this.ViewModel = new this.ViewModelConstructor();

ko.applyBindings(this.ViewModel, this.Element);

Attaching

this._containerElement.children().detach();
this._containerElement.append(this._currentPage.Element);

In this way the pages are only loaded and initialised once, which is an annoying requirement of some controls we're using, and are swapped, rather than being re-initialised and re-rendered on each page load.

Much to my delight, Knockout handled this like a champ!

I'm using jQuery detach which preserves the knockout data on the elements, and then reattaching with attach.

This worked great, up until I started trying to use Knockout Components.

I found that the viewmodel was staying alive, and the observables were being updated, but the binding to the html elements were being removed.

I diagnosed this by putting some code in a computed observable that dumped it's info when it was updated:

public CurrentCompanyName = ko.computed(() => {
    let name = "None";
    let currentCompany = this.AccountManager.CurrentCompany();

    if (!Utils.IsNullOrUndefined(currentCompany))
        name = currentCompany.Name;

    if (this.CurrentCompanyName) {
        let subs = this.CurrentCompanyName.getSubscriptionsCount();
        let deps = this.CurrentCompanyName.getDependenciesCount();

        logger.Log(`CompanySelectorViewModel: CurrentCompany: ${name}, Sub Count: ${subs}, Deps Count: ${deps}, ID: ${this.InstanceID}`, logger.Level.HighLight);
    }

    return name;
});

What I found was that on first call, before detachment, there was 1 subscriber, but after detaching and reattaching that dropped to 0.

I had a search around and found this question from a while ago.

RP Niemeyer:

The evaluation of the bindings in a single data-bind are wrapped in a computed observable that will dispose of itself when it is re-evaluated and recognises that it is not part of the current document.

So, there is not a simple workaround that would allow you to do what you are trying. You could certainly hide the elements while updates are being made and then unhide them.

This rings true, but only for components, as mentioned I also have plenty of "normal" bindings, and they're working great.

I've spent a long time over on GitHub reading through the knockout source code, and unfortunately I can't find the code responsible for detecting when the component is no longer part of the body.

Does anyone know either, how I can stop the components from detecting that they've become detached, or where I can find the code that performs this detection so I can understand how it works?

Since detached binding appears to work great for non-components, if I can't get components to work, I'll probably write my own component loader based on knockout.

Update 1

I thought I might have found where the check was being performed.

https://github.com/knockout/knockout/blob/master/src/binding/bindingAttributeSyntax.js#L70

https://github.com/knockout/knockout/blob/master/src/utils.js#L353

I tired overriding that function to always return true, but it didn't solve the problem. the subscription to the observable was still lost.

// A hack to get component bindings working
// THIS DOESN'T WORK!
// https://github.com/knockout/knockout/blob/master/src/binding/bindingAttributeSyntax.js#L70
(<any>ko.utils).anyDomNodeIsAttachedToDocument = () => {
    logger.Log("anyDomNodeIsAttachedToDocument", logger.Level.HighLight);
    return true;
}

Update 2

I looked for a way to monitor what was unsubscribing the binding.

I found that ko.subscription had a dispose function.

I overrode that function with a wrapper that allowed me to add logging to see when it was being called, and then allowed me to easily drop a break point to inspect the callstack with the debugger.

let originalDispose = (<any>ko).subscription.prototype.dispose;

(<any>ko).subscription.prototype.dispose = function () {
    logger.Log("DISPOSE", logger.Level.HighLight);
    originalDispose.call(this);
};

Looking at the callstack I found that it wasn't anyDomNodeIsAttachedToDocument which was being called, but domNodeIsAttachedToDocument.

https://github.com/knockout/knockout/blob/master/src/subscribables/dependentObservable.js#L246

So, my hack from Update 1 was close, it just had to be applied to a slightly different function.

(<any>ko.utils).domNodeIsAttachedToDocument = () => {
    logger.Log("domNodeIsAttachedToDocument", logger.Level.HighLight);
    return true;
}

At this point I should probably point out that this is a nasty hack!

It's messing about with the private internals of a 3rd party library, and by doing this I'm quite possibly and probably causing other issues for myself.

The first obvious issue is that this will break the automatic disposing of bindings for handlers such as if and when.

So while this is a solution superficially working, I will now have to investigate this more deeply and decide:

Is there a way to implement this hack so that it only affects the situations I care about.

If the effects can't be limited in scope, if they are acceptable vs any alternative approaches.

If there are any further effects which are less obvious but must be taken in to account.

Update 3

As discovered in Update 2, I can fix the issue by globally disabling the auto-disposing.

However I was uncomfortable with making that decision at a global level as it would likely have unintended consequences, and would be a nasty hidden gotcha for other devs.

So I came up with a binding handler which I could apply to elements along side their normal bindings, and that would mark that element as one which the auto-disposing should ignore.

Example

<span data-bind="text: CurrentCompanyName, disableAutoDispose: true"></span>

Binding Handler

let disableAutoDisposeDataKey = "__ko__disableAutoDispose";
let originalDomNodeIsAttachedToDocument = (<any>ko.utils).domNodeIsAttachedToDocument;

(<any>ko.utils).domNodeIsAttachedToDocument = function (node: Node) {
    let disable = $(node).data(disableAutoDisposeDataKey);

    if (disable)
        return true;

    return originalDomNodeIsAttachedToDocument.call(this, node);
};

ko.bindingHandlers.disableAutoDispose = {
    init: (element, valueAccessor, allBindingsAccessor) => {
        $(element).data(disableAutoDisposeDataKey, true);
    }
};

This is still hacky and fragile.

Future changes to knockout could break this code, and because it's internal they have no need/duty to make these changes gracefully (e.g. phased deprecation) or to even publicly announce them.

But I'm much happier that it's now confined to a binding handler, and is optional at a per-binding level.

I will likely turn these updates in to an official answer soon, since I have found a solution.

Although I do hold out hope that someone else will chime in with some good suggestions! ;)

Community
  • 1
  • 1

1 Answers1

0

Have a look at this

Not entirely sure if this will satisfy your criteria but this is what i have used when building SPA applications with knockout and as far as i remember i've never bumped into a problem like yours.

I put the different pages HTML in

<script id="name-of-page" type="text/html">

tags so the HTML is always present in the DOM, and since it's knockouts own binding one can hope they know how to handle their own components :)

Hope it helps!

UberGrunk
  • 63
  • 8
  • Thanks for the info. I'm using AMD Knockout Components for most parts of my SPA application. http://knockoutjs.com/documentation/component-registration.html#registering-components-as-a-single-amd-module My root binding is handled manually because I want it to have extra features over standard knockout. I call lifecycle events on the pages (create/show/hide), and the pages don't normally get disposed between shows to save having to re-render every time they're displayed. Abandoning that in favour of pure AMD Components is an option, but the question is about handling it manually. –  Apr 27 '17 at 07:31