Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Knockout.js 3.2 Preview : Components

| Comments

Knockout 3.2 will include some exciting new functionality out-of-the-box to do modular development through creating components. From Knockout’s point of view, a component allows you to asynchronously combine a template and data (a view model) for rendering on the page. Components in Knockout are heavily inspired by web components, but are designed to work with Knockout and all of the browsers that it supports (all the way back to IE6).

Components allow you to combine independent modules together to create an application. For example, a view could look like:

1
2
3
4
5
<myapp-nav></myapp-nav>

<myapp-grid params="data: items, paging: true, sorting: true"></myapp-grid>

<myapp-footer></myapp-footer>

The idea of doing modular development with Knockout is certainly not a new one. Libraries like Durandal with its compose binding and the module binding from my knockout-amd-helpers have been doing this same type of thing for a while and have helped prove that it is a successful way to build and organize Knockout functionality. Both of these libraries have focused on AMD (Asynchronous Module Definition) to provide the loading and organization of modules.

Knockout’s goal is to make this type of development possible as part of the core without being tied to any third-party library or framework. Developers will be able to componentize their code, by default, rather than only after pulling in various plugins. However, the functionality is flexible enough to support different or more advanced ideas/opinions through extensibility points. When KO 3.2 is released, developers should seriously consider factoring components heavily into their application architecture (unless already successfully using one of the other plugins mentioned).

How does it work?

By default, in version 3.2, Knockout will include:

  1. a system for registering/defining components
  2. custom elements as an easy and clean way to render/consume a component
  3. a component binding as an alternative to custom elements that supports dynamically binding against components
  4. extensibility points for modifying or augmenting this functionality to suit individual needs/opinions

Let’s take a look at how this functionality is used:

Registering a component

The default component loader for Knockout looks for components that were registered via a ko.components.register API. This registration expects a component name along with configuration that describes how to determine the viewModel and the template. Here is a simple example of registering a component:

1
2
3
4
5
6
ko.components.register("simple-name", {
    viewModel: function(data) {
        this.name = (data && data.name) || "none";
    },
    template: "<div data-bind=\"text: name\"></div>"
});

The viewModel key

  • can be a function. If so, then it is used as a constructor (called with new).
  • can pass an instance property to use an object directly.
  • can pass a createViewModel property to call a function that can act as a factory and return an object to use as the view model (has access to the DOM element as well for special cases).
  • can pass a require key to call the require function with the supplied value. This will work with whatever provides a global require function (like require.js). The result will again go through this resolution process.

Additionally, if the resulting object supplies a dispose function, then KO will call it whenever tearing down the component. Disposal could happen if that part of the DOM is being removed/re-rendered (by a parent template or control-flow binding) or if the component binding has its name changed dynamically.

The template key

  • can be a string of markup
  • can be an array of DOM nodes
  • can be an element property that supplies the id of an element to use as the template
  • can be an element property that supplies an element directly
  • can be a require property that like for viewModel will call require directly with the supplied value.

A component could choose to only specify a template, in cases where a view model is not necessary. The supplied params will be used as the data context in that case.

The component binding

With this functionality, Knockout will provide a component binding as an option for rendering a component on the page (with the other option being a custom element). The component binding syntax is fairly simple.

1
2
3
4
5
<div data-bind="component: 'my-component'"></div>

<div data-bind="component: { name: 'my-component', params: { name: 'ryan' } }"></div>

<!-- ko component: 'my-component' --><!-- /ko -->

The component binding supports binding against an observable and/or observables for the name and params options. This allows for handling dynamic scenarios like rendering different components to the main content area depending on the state of the application.

Custom Elements

While the component binding is an easy way to display a component and will be necessary when dynamically binding to components (dynamically changing the component name), custom elements will likely be the “normal” way for consuming a component.

1
<my-component params="name: userName, type: userType"></my-component>

Matching a custom element to a component

Knockout automatically does all of the necessary setup to make custom elements work (even in older browsers), when ko.registerComponent is called. By default, the element name will exactly match the component name. For more flexibility though, Knockout provides an extensibility point (ko.components.getComponentNameForNode) that is given a node and expected to return the name of the component to use for it.

How params are passed to the component

The params are provided to initialize the component, like in the component binding, but with a couple of differences:

  • If a parameter creates dependencies itself (accesses the value of an observable or computed), then the component will receive a computed that returns the value. This helps to ensure that the entire component does not need to be rebuilt on parameter changes. The component itself can control how it accesses and handles any dependencies. For example, in this case:
1
<my-component params="name: first() + ' ' + last()"></my-component>

The component will receive a params object that contains a name property that is supplied as a computed in this case. The component can then determine how to best react to the name changing rather than simply receiving the result of the expression and forcing the entire component to re-load on changes to either of the observables.

  • The params object supplied when using the custom element syntax will also include a $raw property (unless the params happens to supply a property with that same name) which gives access to computeds that return the original value (rather than the unwrapped value). For example:
1
<my-component params="value: selectedItem().value"></my-component>

In this case, since selectedItem is accessed, the param is supplied as a computed. When the computed is accessed, the unwrapped value is returned to avoid having to double-unwrap a param to get its value. However, you may want access to the value observable in this case, rather than its unwrapped value. In the component, this could be achieved by accessing params.$raw.value(). The default functionality is slanted towards ease of use (not having to unwrap a param twice) while providing $raw for advanced cases.

Custom loaders

Knockout let’s you add multiple “component loaders” that can choose how to understand what a component is and how to load/generate the DOM elements and data.

A loader provides two functions: getConfig and loadComponent. Both receive a callback argument that is called when the function is ready to proceed (to support asynchronous operations).

  • getConfig can asynchronously return a configuration object to describe the component given a component name.
  • loadComponent will take the configuration and resolve it to an array of DOM nodes to use as the template and a createViewModel function that will directly return the view model instance.

The default loader

To understand creating a custom component loader, it is useful to first understand the functionality provided by the default loader:

The default getConfig function does the following:

  • this function simply looks up the component name from the registered components and calls the callback with the defined config (or null, if it is not defined).

The default loadComponent function does the following:

  • tries to resolve both the viewModel and template portions of the config based on the various ways that it can be configured.
  • if using require will call require with the configured module name and will take the result and go through the resolution process again.
  • when it has resolved the viewModel and template it will return an array of DOM nodes to use as the template and a createViewModel function that will return a view model based on however the viewModel property was configured.

A sample custom loader

Let’s say that we want to create a widget directory where we place templates and view model definitions that we want to require via AMD. Ideally, we want to just be able to do:

1
<div data-bind="component: 'widget-one'"></div>

In this case, we could create a pretty simple loader to handle this functionality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//add a loader to the end of the list of loaders (one by default)
ko.components.loaders.push({
    getConfig: function(name, callback) {
        var widgetName;

        //see if this is a widget
        if (name.indexOf("widget-") > -1) {
            widgetName = name.substr(7).toLowerCase();

            //provide configuration for how to load the template/widget
            callback({
                viewModel: {
                    //require the widget from the widget directory with just the name
                    require: "widgets/" + widgetName
                },
                template: {
                    //use the text plugin to load the template from the same directory
                    require: "text!widgets/" + widgetName + ".tmpl.html"
                }
            });
        } else {
            //tell KO that we don't know and it can move on to additional loaders
            callback(null);
        }
    },
    //use the default loaders functionality for loading
    loadComponent: ko.components.defaultLoader.loadComponent
});

In this custom loader, we just dynamically build the configuration that we want, so we don’t necessarily have to register every “widget” as its own component, although registering will properly setup custom elements to work with the component. Loading the widget-one component would load a one.js view model and one.tmpl.html template from a widgets directory in this sample loader. If the component is not a “widget”, then the callback is called with null, so other loaders can try to fulfill the request.

Summary

Components are a major addition to Knockout’s functionality. Many developers have found ways to do this type of development in their applications using plugins, but it will be great to have standard support in the core and the possibility for extensibility on top of it. Steve Sanderson recently did a great presentation at NDC Oslo 2014 that highlighted the use of components in Knockout. Check it out here.

Knockout 3.2 is well underway and should be ready for release this summer.

Comments