1

I have a single page app that consists of 2 main pieces:

1.-A top bar that has dynamic content(there is a shopping cart)
2.-A dynamic component that gets loaded based on the url.

A few times the components use postbox in order to communicate, the problem is that once the component itself is disposed the subscriptions created inside are not. I know I can manually add a dispose function to each component and then inside, dispose the subscriptions, but is there a way to do this in an automated way for all components?

I do know how to loop all properties and check if they are subscriptions, but I need a way to somehow attach this behavior to all components without manually attaching this dispose function to all of them.

I know postbox comes with a reset method I can call inside my routing library but I do not want to do that because then the top bar will lose its subscriptions too.

To give you some perspective, this is how the main index page looks like:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Participant Dashboard</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">
    <!-- styles -->
    <link href="../css/bs3/bootstrap.css" rel="stylesheet">
    <link href="../css/bs3/override-bs3.css" rel="stylesheet">
    <script src="../scripts/global/requireConfig.js"></script>
    <script data-main="../consumer/scripts/require-config" src="../scripts/require.js"></script>

</head>
<body>
<top-bar params="routeParams:currentPageArguments"></top-bar>

<div data-bind="component: { name: currentPage, params: currentPageArguments }">
</div>


</body>
</html>

This is my custom component loader:

    function registerConventionLoader() {
        var koNamingConventionLoader = {
            getConfig: function (name, callback) {
                var widgetName;
                var widgetConfig = common.findComponentConfig(name);
                if (widgetConfig != null) {
                    widgetName = name.substr(widgetConfig.Prefix.length);
                    var widgetNamePascalCase = common.toPascalCase(widgetName);
                    var filePath = widgetConfig.Path;
                    var viewModelConfig = {require: filePath + widgetNamePascalCase};
                    var templateConfig = {require: "text!" + filePath + widgetNamePascalCase + '.html'};

                    callback({viewModel: viewModelConfig, template: templateConfig});
                }
                else {
                    
                    callback(null);
                }
            }
        };
        ko.components.loaders.push(koNamingConventionLoader);
    }
Daniel 1.618
  • 107
  • 1
  • 8

2 Answers2

2

Already implemented behavior

The postbox plugin adds a dispose method to your observables to dispose any dependencies it created.

The dispose function removes all the subscriptions that an observable has on any topic as well as all the subscriptions used to automatically publish changes to the observable.

This function is attached to the observable when publishOn, subscribeTo or syncWith is called.

Source: postbox github docs

If your component's viewmodel has a dispose method, knockout will call it when removing the component.

Optionally, your viewmodel class may have a dispose function. If implemented, Knockout will call this whenever the component is being torn down and removed from the DOM

Source: component-binding

The custom disposal logic

Knowing these two library/plugin behaviors, we can conclude that this general idea should do the trick:

MyCustomComponent.prototype.dispose = function() {
  /* call `.dispose` on all properties that support it */
};

The only code left we'll have to write is the commented out part:

  • Loop over a viewmodel's properties
  • Check if they support a dispose method
  • Call it if they do

Which boils down to:

MyCustomComponent.prototype.dispose = function() {
  var self = this;
  var propNames = Object.keys(this);

  propNames.forEach(function(key) { // Loop over vm's properties
    var val = self[key];

    if (typeof val.dispose === "function") { // Check of dispose implementation
      val.dispose(); // call dispose
    }
  });
};

Or, in a different style:

MyCustomComponent.prototype.dispose = function() {
  Object
    .keys(this)
    .filter(k => typeof this[k] === "function")
    .forEach(k => this[k]());
};

Making sure all components implement the disposal logic

I'd highly recommend using a "regular" inheritance or composition pattern to make sure all your components implement this feature.

Al though this forces you to edit all of your components, it also explicitly shows other readers/future you the implemented behavior.

If you really want to go wild, you could overwrite the component engine's register method to add the method to the vm upon instantiation, but I wouldn't recommend it:

var _register = ko.components.register;
ko.components.register = function(name, opts) {
  var ogVM = opts.viewmodel;
  opts.viewmodel = function(params) {
     ogVM.call(this, params);
     this.dispose = function() { /* ... */ }
  }
  return _register(name, opts);
};
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • Thanks @user3297291. I have provided a better description of what I want, I know how to traverse all properties and check if there are subscriptions, what I do not know is if there is a way to automatically attach this functions to all components without doing it manually. – Daniel 1.618 Aug 16 '17 at 13:27
  • You use a regular inheritance or composition pattern to make sure the `dispose` method is added to the viewmodel... Would it help if I gave you an example? – user3297291 Aug 16 '17 at 13:30
  • Thanks again!, I tried this approach but I think I forgot to mention something important, I am using a custom component loader, when I try your solution to intercept the register function it seems like it is never executed. – Daniel 1.618 Aug 16 '17 at 16:37
  • i believe your "Loop over a viewmodel's properties" should be read as: "Deep traverse the ViewModel", what is your opinion about that? – deblocker Aug 19 '17 at 01:43
-1

Found it! I had to get the viewmodel from require, then attach the dispose function and finally pass it in the callback: Thanks @user3297291 for guiding me in the correct direction

var koNamingConventionLoader = {
            getConfig: function (name, callback) {
               
                var widgetName;
                var widgetConfig = common.findComponentConfig(name);
                
                if (widgetConfig != null) {
                    widgetName = name.substr(widgetConfig.Prefix.length);
                    var widgetNamePascalCase = common.toPascalCase(widgetName);
                    var filePath = widgetConfig.Path;
                    
                    require([filePath + widgetNamePascalCase], function (mainViewModel) {
                        mainViewModel.prototype.dispose = function () {
                            var self = this;
                            for (var property in self) {
                                if (Boolean(self[property]) && typeof self[property].dispose === "function") {
                                    
                                    self[property].dispose();
                                }
                            }
                        };
                        
                        var templateConfig = {require: "text!" + filePath + widgetNamePascalCase + '.html'};

                        callback({viewModel: mainViewModel, template: templateConfig});
                    });
                }
                else {
                    
                    console.log("widget name not resolved", name);
                    callback(null);
                }
            }
        };
        ko.components.loaders.push(koNamingConventionLoader);
    }
Daniel 1.618
  • 107
  • 1
  • 8