





















































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.
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.
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.
The template property of the configuration sent to register can take any of the following formats:
ko.components.register('component-name', { template: [OPTION] });
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.
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 } });
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 });
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.
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.
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.
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.
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 });
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.
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); }} });
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.
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:
Component parameters will be passed via the params object to the component's viewmodel in one of the following three ways:
<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.
<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.
<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.
When a component binding is applied, Knockout takes the following steps.
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:
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.
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.