Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can a Knockout JS component viewmodel function be used as a callback?

Tags:

knockout.js

According to Knockout Documentation here, a component viewModel is only instantiated "on demand", considered it is declared such as:

<div data-bind='component: {
    name: componentNameObservable,
    params: { mode: "detailed-list", items: productsList }
}'></div>

Consider the scenario of a Single Page Application with a routing mechanism such as SammyJS, Crossroads.js, or any other.

When a route change request matches a route pattern, it is usual to make the routing library handler for the route matched event to set a new value for componentNameObservable. This will trigger the injection of a new component into the bound element. On top of that, also inside this route matched handler, let's say I want to execute a function declared inside the component viewModel being bound, to refresh/setup view model data, responding to route changed event. How would that be possible? Since the component viewModel instantiation in these cases is controlled by Knockout's internal component binding mechanism, I cannot access its functions so that I could reference them as callbacks to be executed. If it were possible, I would pass a component viewModel function as a callback parameter to the routing library route matched handler, then execute the callback function, all would be nice... But doesn't seem to work that way...

I am registering the component using CommonJS to work with Browserify, such as described here

http://knockoutjs.com/documentation/component-loaders.html#note-integrating-with-browserify

In this case, the module.exports on the component view model has to expose its entire non-instantiated constructor function, so that whatever require('myViewModel') gets translated into, gets the view model to be correctly "newed up" upon binding to the element.

Any suggestions?

like image 222
Renato Xavier Avatar asked Oct 19 '22 19:10

Renato Xavier


1 Answers

let's say I want to execute a function declared inside the component viewModel being bound, to refresh/setup view model data, responding to route changed event. How would that be possible?

In the component registration documentation you have 4 different options to provide the view model:

  • 1st: A constructor function
  • 2nd: A shared object instance
  • 3rd: A createViewModel factory function
  • 4th: An AMD module whose value describes a viewmodel

In fact, the 4th option is using an AMD module to return one of the other 3 options. So there are only 3 possible options, with or without using require.

If your SPA will only use a single instance of your component, you can use the second solution: create a viewmodel, store it in a variable which is always in scope (for example in global scope or inside a module), and register the component to use it. In this way, when the component is instanced by the routing event, you can access the viewmodel through the variable to invoke the desired functionality.

If your SPA can have several different instances of your component, or you simply don't want to use the previous solution, you must use the 1st or 3rd option (regarding this question, it doesn't matter which one). In this case you can pass a callback function to your component, which will be available in the constructor (1st option) or factory method (3rd option) parameters. The constructor (or factory) can invoke this callback to expose its functionality. You can implement something like this, but not necessarily exactly like this:

In the main scope of your application

// Here you'll store the component APIs to access them:
var childrenComponentsApi = {};
// This will be passed as a callback, so that the child component
// can register the API
var registerChildComponentApi = function(api) {
    childrenComponentsApi.componentX = api;
};

NOTE: it's important to have an object where the functionality can be registered so that you don't lose the reference

In the view:

<div data-bind='component: {
  name: 'componentX',
  params: { registerApi: registerChildComponentApi, /*other params*/ }
}'></div>

In the components view model constructor (or factory) body:

params.registerApi({ // The callback is available in thereceived params
   func1: func1,  // register the desired functions
   func2: func2}); 

Later on, in the main scope, you can access the component's functionality like this:

childrenComponentsApi.componentX.fun1(/* params */);

This is not exactly working code, but I hope it gives you an idea on how to implement what you need.

This solution works perfectly if the API isn't invoked inmediately. I used this implementation when the functionality will be invoked ny a user's action, so that I'm sure that the component is already instanced.

But, in your case, the component creation is asynchronous, so you have to modify the implementation. There are at least two possible ways:

1) the easier one is to change the registerApi implementation and use it to cal the initialization. Something like this:

In the viewmodels constructor:

params.registerApi({
   init: init,    // initialization function
   func1: func1,  // other exposed functionality
   func2: func2}); 

In the main scope:

var registerChildComponentApi = function(api) {
    childrenComponentsApi.componentX = api;
    childrenComponentsApi.componentx.init(/* params*/)
}

In this implementacion init is called after the callback has been run by the client component, so you can be sure the component is available.

2) a more complex solution involves using promises. If your component needs to do asynchronous operations to get ready (for example doing AJAX calls), you can make it return a promise besides all the exposed API, so that the component solves the promise when it's really ready, and the main scope runs the API functionality only when the promise has been solved. Something like this:

In the view model constructor:

params.registerApi({
   ready: ready,  // promise created and solved by the component
   func1: func1,  // exposed functionality
   func2: func2}); 

In the main scope:

var registerChildComponentApi = function(api) {
    childrenComponentsApi.componentX = api;
}
childrenComponentsApi.componentx.ready().then(
   childrenComponentsApi.componentx.func1;
);

These are some implementation samples, but there can be many variations on them. For example, if you only need to run the init function of the component viewmodel, you can supply a parameter like provideInit to the component, and run it in the consturctor by pasing the component's init. Something like this:

<div data-bind='component: {
  name: 'componentX',
  params: { provideInit: provideInit, /*other params*/ }
}'></div>

var proviedInit: function(init) {
   init(/* params */);
};

And don't forget that it's also possible to initialize the component's viewmodel by passing all the necessary parameters to the constructor.Or even by passing observables as parameters and modifying them from the main scope.

The last best advice I can give you is that you standardize and document correctly the registerApi functonality, so that all the components are implemented and used in the same way.

As I told in the beginning, any of these solutions can be implemented directly, or by using AMD modules. I.e. you can register the component providing the view model constructor directly (1st option), or defining the constructor as an AMD module and using the 4th option.

like image 54
JotaBe Avatar answered Jan 04 '23 05:01

JotaBe