Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanacement: Allow Parameter-Driven Config for Knockout Components #1656

Closed
mstarets opened this issue Dec 18, 2014 · 6 comments
Closed

Enhanacement: Allow Parameter-Driven Config for Knockout Components #1656

mstarets opened this issue Dec 18, 2014 · 6 comments

Comments

@mstarets
Copy link

I understand that Knockout Components were in many ways inspired by the Knockout AMD Helpers. In most cases, one can easily switch to using Knockout Components, but there i still one use case where AMD Helpers sfits better than core Knockout Components: loading dynamic markup from arbitrary external files. Suppose you are writing a single-page application where the center area is populated dynamically depending on the menu choice made by the user. If the user selects 'Billing', we want to load the billing sup-page along with its associated View Model. This can be easily achieved with the 'module' binding from AMD helpers (the name of the module can be specified as an observable), but with Knockout Components you would have to pre-register the 'Billing' page as a Knockout component. This is very inconvenient for metadata-driven applications or for applications with lots of pages maintained by different developers.

Would it be possible possible to add a new 'configParams' attribute on the component binding? The object would then be passed to the getConfig() method on the loader. This would enable creation of a Knockout component that would replace the 'module' binding from AMD Helpers:

And the custom loader's getConfig() would look similar to the following:
getConfig: function(name, configParams, callback) {
var moduleName = ko.utils.unwrapObservable(configParams['name']);

   callback({ viewModel: {require: 'modules' + moduleName} , 
                   template: {require:  'text!templates/' + moduleName + '.html'} });
}

Another alternative would be to pass the existing 'params' value into the getConfig(), but that would mean that we are mixing module config parameters with the parameters for the module itself.

And yet another alternative would be to create a new 'generic module' binding that would share 99% of its code with the component binding.
Thanks!

@rniemeyer
Copy link
Member

@mstarets you may want to have a look at the "custom loader" extensibility point available for the components functionality.

  • The main custom loader docs are here
  • At the end of my blog post here, there is an implementation of a sample custom loader that uses conventions to require a view model and template.

Hope that helps!

@mstarets
Copy link
Author

Thanks, Ryan.

I was aware of your blog (I am a fan ;)) This solution definitely works, but I am trying avoid having to encode the name of the module into the component name. Having configParams would provide a cleaner solution, and it would enable extra features, such as specifying template name that is different from the ViewModel module name.

Thanks,
Max

@SteveSanderson
Copy link
Contributor

This is interesting. The thing that might be tricky here is that, by design, component definitions are cached so that the loader only needs to get invoked the first time any given named component is requested. This guarantees that there's only one async load process, so it's very hard to have any pathologically bad cases like a foreach list in which every element triggers a separate load process (which might all but one be redundant, if they produce the same component definition).

Having this configParams parameter would mean at least we have to create one cache entry for each unique configParams value (where 'uniqueness' is somehow defined according to the configParams contents, not the object instance - always tricky).

The current design of requiring you to encode everything unique about the component definition into its name ensures that the caching behavior always lines up with what the developer is requesting. Arguably this is correct, and in your case you should create fake "component names" that encode all of the configParams you wish to supply (e.g., as a JSON string), then have a custom loader that decodes all that info and returns a matching component definition. This would produce the correct caching behaviour.

@mstarets, do you have any other views on how this should interact with component definition caching?

@mstarets
Copy link
Author

mstarets commented Apr 6, 2015

Steve, I suspected there was a good reason why the 'name' parameter was treated specially. Thanks for explaining it. The only workable solution I could see would be to document that caching of component definitions is only enabled if configParams is not specified or if configParams['definitionCacheKey'] is provided.

I am now thinking that my use case is not what Knockout Components Design should really target. That use case was automation of region navigation within a single-page application. The 'module' binding in AMD Helpers does a very good job with that, but it lacks certain features that are supported by Durandal Views (lifecycle events, view caching, etc.), and it has some problems like the lack of concurrency in fetching of template and ViewModel definitions (see rniemeyer/knockout-amd-helpers#29). Rather than complicating Knockout Components, I think I would now prefer to see the 'module' binding implemented by Core Knockout.
I ended up writing my own version, which I would be very happy to check into into a sandbox somewhere as a contribution to the Knockout community.

Thanks,
Max

@onlyurei
Copy link

onlyurei commented Feb 2, 2016

[As of v3.4.0]
I had a similar problem: I want to lazy-load components as they appear in the dynamically lazy-loaded templates (using https://github.com/rniemeyer/knockout-amd-helpers), and don't want to pre-register components no matter if I use them via custom elements or component bindings. I eventually came up with the following solution that works for me.

I initially followed the custom loader instructions, but the custom elements wouldn't processed by Knockout without pre-registration of their corresponding components, so I dug further and realized that I have to override the default getComponentNameForNode as well so that pre-registration is not required for Knockout to recognize the custom element tags.

This approach allows lazy load of the components using custom element (tag) as they appear in the markup templates, without having to explicitly pre-register them. Further, using the text plugin of requirejs r.js optimizer is able to package the whole component into 1 bundle for production build.

Original getComponentNameForNode method:

/*!
 * Knockout JavaScript library v3.4.0
 * (c) Steven Sanderson - http://knockoutjs.com/
 * License: MIT (http://www.opensource.org/licenses/mit-license.php)
 */
...
    // Overridable API for determining which component name applies to a given node. By overriding this,
    // you can for example map specific tagNames to components that are not preregistered.
    ko.components['getComponentNameForNode'] = function(node) {
        var tagNameLower = ko.utils.tagNameLower(node);
        if (ko.components.isRegistered(tagNameLower)) {
            // Try to determine that this node can be considered a *custom* element; see https://github.com/knockout/knockout/issues/1603
            if (tagNameLower.indexOf('-') != -1 || ('' + node) == "[object HTMLUnknownElement]" || (ko.utils.ieVersion <= 8 && node.tagName === tagNameLower)) {
                return tagNameLower;
            }
        }
    };

What I did:

   //override the overridable function so that pre-registration is not required for custom-elements
    ko.components['getComponentNameForNode'] = function (node) {
        var tagNameLower = node.tagName && node.tagName.toLowerCase(node);
        // Try to determine that this node can be considered a *custom* element; see https://github.com/knockout/knockout/issues/1603
        if (tagNameLower.indexOf('-') != -1 || ('' + node) == "[object HTMLUnknownElement]" ||
            (ko.utils.ieVersion <= 8 && node.tagName === tagNameLower)) {
            return tagNameLower;
        }
    };

   //create a custom loader to establish a naming/AMD module path convention
    ko.components.loaders.push({
        getConfig: function(name, callback) {
            callback({ require: 'component/' + name }); //set to where your component JS AMD modules are
        }
    });

Example of a component module, using a helper util JSFace (https://github.com/tnhu/jsface) for class construction

 define(['text!../../template/component/name.html', 'lib/jsface'], function (template, Class) {

    return {
        viewModel: Class({
            construction: function (params) {
            //viewModel constructor logic
            },
            classMethod: function () { //shared class method by this viewModel instances }
        }),
        template: template
    };

});

@mbest mbest modified the milestone: Not assigned Dec 2, 2016
@mbest
Copy link
Member

mbest commented Dec 3, 2016

I feel that Knockout already provides good component extensibility, and the needs described can be satisfied using the existing features.

@mbest mbest closed this as completed Dec 3, 2016
@mbest mbest removed the waiting label Dec 3, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants