javascriptsingle-page-applicationdurandal-2.0durandal-navigation

DurandalJS - Enforcing Full Activation Cycle on Page Root (aka: fire "deactivate" on app.setRoot())


[Edit] There's also Google Groups thread over here. I'll try to keep both updated as I learn more.

According to the Durandal Documentation the page Root View Model only implements limited Activator functionality. Specifically, it states that:

....

4) When you call app.setRoot a (limited) activator is used on your root module.

Note: Case 4 is a bit different as it only enforces canActivate and activate callbacks; not the deactivation lifecycle. To enable that, you must use a full activator yourself (cases 1-3).

Which means that unlike all the nested page ViewModels, the root ones do not get a chance to dispose of their junk objects and clean up.

What is a friendly way to fire both canDeactivate and deactivate on these objects when I call app.setRoot()? I could perhaps do this manually, but I'd need a reference to the root ViewModel, which isn't necessarily in scope.

I've considered creating an pub/sub system of sorts, though I'm weary of that because it has the potential to cause MORE memory issues and cleanup complexity than reduce it.

I've considered forking the project, though that comes with some heavy overhead as I'd have to maintain my fork from then on.

[EDIT]

Example:

Let's say I was to start my application and set the shell to the "main" shell.

  app.start().then(function () {
      app.setRoot('views/shells/main');
  })

At some time later, I wish to switch to a new shell. Perhaps a different layout, full screen, or a different skin.

 app.setRoot('views/shells/accounts');

This will cause the shell "accounts" to check and/or fire activate and canActivate, but it will NOT check the shell "main" for a canDeactivate and a deactivate. I have cleanup logic that I wish to run when these go away. How can I accomplish this?

[EDIT]

The portion of the durandal code the handles this is a local function living under the module 'durandal/app' setRoot() finishComposition() It looks like this

  function finishComposition() {
            if(settings.model) {
                if (settings.model.canActivate) {
                    try {
                        var result = settings.model.canActivate();
                        if (result && result.then) {
                            result.then(function (actualResult) {
                                if (actualResult) {
                                    composition.compose(hostElement, settings);
                                }
                            }).fail(function (err) {
                                system.error(err);
                            });
                        } else if (result) {
                            composition.compose(hostElement, settings);
                        }
                    } catch (er) {
                        system.error(er);
                    }
                } else {
                    composition.compose(hostElement, settings);
                }
            } else {
                composition.compose(hostElement, settings);
            }
        }

Perhaps there's a way to monkey patch this? I'd like to avoid editing the core of Durandal if possible.


Solution

  • You don't really have to implement multiple shells. We looked at this, too, in the beginning. You can have one shell, and then immediately and dynamically compose an inner shell within that. Inner shells could be swapped in with dynamic composition, which would still preserve your high-level routing. In other words, it wouldn't be necessary for every view to become a child route.

    In other words, the following would sit two levels down, not one:

    data-bind="router: { model: router.activeItem, cacheViews:false, compositionComplete: router.compositionComplete, attached: router.attached }">
    

    This would get moved into an inner shell:

    <div data-bind="compose: {model: currentShell()}"></div>
    

    where currentShell() is an observable that holds the current shell (inside the viewModel of your "super shell").