13 min read

This article by Timothy Moran, author of Mastering KnockoutJS, teaches you how to use the new Knockout components feature.

(For more resources related to this topic, see here.)

In Version 3.2, Knockout added components using the combination of a template (view) with a viewmodel to create reusable, behavior-driven DOM objects. Knockout components are inspired by web components, a new (and experimental, at the time of writing this) set of standards that allow developers to define custom HTML elements paired with JavaScript that create packed controls. Like web components, Knockout allows the developer to use custom HTML tags to represent these components in the DOM. Knockout also allows components to be instantiated with a binding handler on standard HTML elements. Knockout binds components by injecting an HTML template, which is bound to its own viewmodel.

This is probably the single largest feature Knockout has ever added to the core library. The reason we started with RequireJS is that components can optionally be loaded and defined with module loaders, including their HTML templates! This means that our entire application (even the HTML) can be defined in independent modules, instead of as a single hierarchy, and loaded asynchronously.

The basic component registration

Unlike extenders and binding handlers, which are created by just adding an object to Knockout, components are created by calling the ko.components.register function:

ko.components.register('contact-list, {
viewModel: function(params) { },
template: //template string or object
});

This will create a new component named contact-list, which uses the object returned by the viewModel function as a binding context, and the template as its view. It is recommended that you use lowercase, dash-separated names for components so that they can easily be used as custom elements in your HTML.

To use this newly created component, you can use a custom element or the component binding. All the following three tags produce equivalent results:

<contact-list params="data: contacts"><contact-list>
<div data-bind="component: { name: 'contact-list', params: { data: 
 contacts }"></div>
<!-- ko component: { name: 'contact-list', params: { data: 
 contacts } --><!-- /ko -->

Obviously, the custom element syntax is much cleaner and easier to read. It is important to note that custom elements cannot be self-closing tags. This is a restriction of the HTML parser and cannot be controlled by Knockout.

There is one advantage of using the component binding: the name of the component can be an observable. If the name of the component changes, the previous component will be disposed (just like it would if a control flow binding removed it) and the new component will be initialized.

The params attribute of custom elements work in a manner that is similar to the data-bind attribute. Comma-separated key/value pairs are parsed to create a property bag, which is given to the component. The values can contain JavaScript literals, observable properties, or expressions. It is also possible to register a component without a viewmodel, in which case, the object created by params is directly used as the binding context.

To see this, we’ll convert the list of contacts into a component:

<contact-list params="contacts: displayContacts,
edit: editContact,
delete: deleteContact">
</contact-list>

The HTML code for the list is replaced with a custom element with parameters for the list as well as callbacks for the two buttons, which are edit and delete:

ko.components.register('contact-list', {
template:
'<ul class="list-unstyled" data-bind="foreach: contacts">'
   +'<li>'
     +'<h3>'
       +'<span data-bind="text: displayName"></span> <small data-
          bind="text: phoneNumber"></small> '
       +'<button class="btn btn-sm btn-default" data-bind="click: 
          $parent.edit">Edit</button> '
       +'<button class="btn btn-sm btn-danger" data-bind="click: 
          $parent.delete">Delete</button>'
     +'</h3>'
   +'</li>'
+'</ul>'
});

This component registration uses an inline template. Everything still looks and works the same, but the resulting HTML now includes our custom element.

Mastering KnockoutJS

Custom elements in IE 8 and higher

IE 9 and later versions as well as all other major browsers have no issue with seeing custom elements in the DOM before they have been registered. However, older versions of IE will remove the element if it hasn’t been registered. The registration can be done either with Knockout, with ko.components.register(‘component-name’), or with the standard document.createElement(‘component-name’) expression statement. One of these must come before the custom element, either by the script containing them being first in the DOM, or by the custom element being added at runtime.

When using RequireJS, being in the DOM first won’t help as the loading is asynchronous. If you need to support older IE versions, it is recommended that you include a separate script to register the custom element names at the top of the body tag or in the head tag:

<!DOCTYPE html>
<html>
<body>
   <script>
     document.createElement('my-custom-element');
   </script>
   <script src='require.js' data-main='app/startup'></script>
 
   <my-custom-element></my-custom-element>
</body>
</html>

Once this has been done, components will work in IE 6 and higher even with custom elements.

Template registration

The template property of the configuration sent to register can take any of the following formats:

ko.components.register('component-name', {
template: [OPTION]
});

The element ID

Consider the following code statement:

template: { element: 'component-template' }

If you specify the ID of an element in the DOM, the contents of that element will be used as the template for the component. Although it isn’t supported in IE yet, the template element is a good candidate, as browsers do not visually render the contents of template elements.

The element instance

Consider the following code statement:

template: { element: instance }

You can pass a real DOM element to the template to be used. This might be useful in a scenario where the template was constructed programmatically. Like the element ID method, only the contents of the elements will be used as the template:

var template = document.getElementById('contact-list-template');
ko.components.register('contact-list', {
template: { element: template }
});

An array of DOM nodes

Consider the following code statement:

template: [nodes]

If you pass an array of DOM nodes to the template configuration, then the entire array will be used as a template and not just the descendants:

var template = document.getElementById('contact-list-template')
nodes = Array.prototype.slice.call(template.content.childNodes);
ko.components.register('contact-list', {
template: nodes
});

Document fragments

Consider the following code statement:

template: documentFragmentInstance

If you pass a document fragment, the entire fragment will be used as a template instead of just the descendants:

var template = document.getElementById('contact-list-template');
ko.components.register('contact-list', {
template: template.content
});

This example works because template elements wrap their contents in a document fragment in order to stop the normal rendering. Using the content is the same method that Knockout uses internally when a template element is supplied.

HTML strings

We already saw an example for HTML strings in the previous section. While using the value inline is probably uncommon, supplying a string would be an easy thing to do if your build system provided it for you.

Registering templates using the AMD module

Consider the following code statement:

template: { require: 'module/path' }

If a require property is passed to the configuration object of a template, the default module loader will load the module and use it as the template. The module can return any of the preceding formats. This is especially useful for the RequireJS text plugin:

ko.components.register('contact-list', {
template: { require: 'text!contact-list.html'}
});

Using this method, we can extract the HTML template into its own file, drastically improving its organization. By itself, this is a huge benefit to development.

The viewmodel registration

Like template registration, viewmodels can be registered using several different formats. To demonstrate this, we’ll use a simple viewmodel of our contacts list components:

function ListViewmodel(params) {
this.contacts = params.contacts;
this.edit = params.edit;
this.delete = function(contact) {
   console.log('Mock Deleting Contact', ko.toJS(contact));
};
};

To verify that things are getting wired up properly, you’ll want something interactive; hence, we use the fake delete function.

The constructor function

Consider the following code statement:

viewModel: Constructor

If you supply a function to the viewModel property, it will be treated as a constructor. When the component is instantiated, new will be called on the function, with the params object as its first parameter:

ko.components.register('contact-list', {
template: { require: 'text!contact-list.html'},
viewModel: ListViewmodel //Defined above
});

A singleton object

Consider the following code statement:

viewModel: { instance: singleton }

If you want all your component instances to be backed by a shared object—though this is not recommended—you can pass it as the instance property of a configuration object. Because the object is shared, parameters cannot be passed to the viewmodel using this method.

The factory function

Consider the following code statement:

viewModel: { createViewModel: function(params, componentInfo) {} }

This method is useful because it supplies the container element of the component to the second parameter on componentInfo.element. It also provides you with the opportunity to perform any other setup, such as modifying or extending the constructor parameters. The createViewModel function should return an instance of a viewmodel component:

ko.components.register('contact-list', {
template: { require: 'text!contact-list.html'},
viewModel: { createViewModel: function(params, componentInfo) {
   console.log('Initializing component for', 
      componentInfo.element);
   return new ListViewmodel(params);
}}
});

Registering viewmodels using an AMD module

Consider the following code statement:

viewModel: { require: 'module-path' }

Just like templates, viewmodels can be registered with an AMD module that returns any of the preceding formats.

Registering AMD

In addition to registering the template and the viewmodel as AMD modules individually, you can register the entire component with a require call:

ko.components.register('contact-list', { require: 'contact-list' 
 });

The AMD module will return the entire component configuration:

define(['knockout', 'text!contact-list.html'], function(ko, 
 templateString) {
 
function ListViewmodel(params) {
   this.contacts = params.contacts;
   this.edit = params.edit;
   this.delete = function(contact) {
     console.log('Mock Deleting Contact', ko.toJS(contact));
   };
}
 
return { template: templateString, viewModel: ListViewmodel };
});

As the Knockout documentation points out, this method has several benefits:

  • The registration call is just a require path, which is easy to manage.
  • The component is composed of two parts: a JavaScript module and an HTML module. This provides both simple organization and clean separation.
  • The RequireJS optimizer, which is r.js, can use the text dependency on the HTML module to bundle the HTML code with the bundled output. This means your entire application, including the HTML templates, can be a single file in production (or a collection of bundles if you want to take advantage of lazy loading).

Observing changes in component parameters

Component parameters will be passed via the params object to the component’s viewmodel in one of the following three ways:

  • No observable expression evaluation needs to occur, and the value is passed literally:
    <component params="name: 'Timothy Moran'"></component>
    <component params="name: nonObservableProperty">
     </component>
    <component params="name: observableProperty"></component>
    <component params="name: viewModel.observableSubProperty
     "></component>

    In all of these cases, the value is passed directly to the component on the params object. This means that changes to these values will change the property on the instantiating viewmodel, except for the first case (literal values). Observable values can be subscribed to normally.

  • An observable expression needs to be evaluated, so it is wrapped in a computed observable:
    <component params="name: name() + '!'"></component>

    In this case, params.name is not the original property. Calling params.name() will evaluate the computed wrapper. Trying to modify the value will fail, as the computed value is not writable. The value can be subscribed to normally.

  • An observable expression evaluates an observable instance, so it is wrapped in an observable that unwraps the result of the expression:
    <component params="name: isFormal() ? firstName : 
     lastName"></component>

    In this example, firstName and lastName are both observable properties. If calling params.name() returned the observable, you will need to call params.name()() to get the actual value, which is rather ugly. Instead, Knockout automatically unwraps the expression so that calling params.name() returns the actual value of either firstName or lastName.

If you need to access the actual observable instances to, for example, write a value to them, trying to write to params.name will fail, as it is a computed observable. To get the unwrapped value, you can use the params.$raw object, which provides the unwrapped values. In this case, you can update the name by calling params.$raw.name(‘New’).

In general, this case should be avoided by removing the logic from the binding expression and placing it in a computed observable in the viewmodel.

The component’s life cycle

When a component binding is applied, Knockout takes the following steps.

  1. The component loader asynchronously creates the viewmodel factory and template. This result is cached so that it is only performed once per component.
  2. The template is cloned and injected into the container (either the custom element or the element with the component binding).
  3. If the component has a viewmodel, it is instantiated. This is done synchronously.
  4. The component is bound to either the viewmodel or the params object.
  5. The component is left active until it is disposed.
  6. The component is disposed. If the viewmodel has a dispose method, it is called, and then the template is removed from the DOM.

The component’s disposal

If the component is removed from the DOM by Knockout, either because of the name of the component binding or a control flow binding being changed (for example, if and foreach), the component will be disposed. If the component’s viewmodel has a dispose function, it will be called. Normal Knockout bindings in the components view will be automatically disposed, just as they would in a normal control flow situation. However, anything set up by the viewmodel needs to be manually cleaned up. Some examples of viewmodel cleanup include the following:

  • The setInterval callbacks can be removed with clearInterval.
  • Computed observables can be removed by calling their dispose method. Pure computed observables don’t need to be disposed. Computed observables that are only used by bindings or other viewmodel properties also do not need to be disposed, as garbage collection will catch them.
  • Observable subscriptions can be disposed by calling their dispose method.
  • Event handlers can be created by components that are not part of a normal Knockout binding.

Combining components with data bindings

There is only one restriction of data-bind attributes that are used on custom elements with the component binding: the binding handlers cannot use controlsDescendantBindings. This isn’t a new restriction; two bindings that control descendants cannot be on a single element, and since components control descendant bindings that cannot be combined with a binding handler that also controls descendants. It is worth remembering, though, as you might be inclined to place an if or foreach binding on a component; doing this will cause an error. Instead, wrap the component with an element or a containerless binding:

<ul data-bind='foreach: allProducts'>
<product-details params='product: $data'></product-details>
</ul>

It’s also worth noting that bindings such as text and html will replace the contents of the element they are on. When used with components, this will potentially result in the component being lost, so it’s not a good idea.

Summary

In this article, we learned that the Knockout components feature gives you a powerful tool that will help you create reusable, behavior-driven DOM elements.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here