The "component" binding
The component
binding injects a specified component into an element, and optionally passes parameters to it.
- Live example
- API
- Component lifecycle
- Note: Template-only components
- Note: Using
component
without a container element - Note: Passing markup to components
- Disposal and memory management
Live example
First instance, without parameters
Second instance, passing parameters
Source code: View
<h4>First instance, without parameters</h4> <div data-bind='component: "message-editor"'></div> <h4>Second instance, passing parameters</h4> <div data-bind='component: { name: "message-editor", params: { initialText: "Hello, world!" } }'></div>
Source code: View model
ko.components.register('message-editor', { viewModel: function(params) { this.text = ko.observable(params && params.initialText || ''); }, template: 'Message: <input data-bind="value: text" /> ' + '(length: <span data-bind="text: text().length"></span>)' }); ko.applyBindings();
Note: In more realistic cases, you would typically load component viewmodels and templates from external files, instead of hardcoding them into the registration. See an example and registration documentation.
API
There are two ways to use the component
binding:
-
Shorthand syntax
If you pass just a string, it is interpreted as a component name. The named component is then injected without supplying any parameters to it. Example:
<div data-bind='component: "my-component"'></div>
The shorthand value can also be observable. In this case, if it changes, the
component
binding will dispose the old component instance, and inject the newly-referenced component. Example:<div data-bind='component: observableWhoseValueIsAComponentName'></div>
-
Full syntax
To supply parameters to the component, pass an object with the following properties:
name
— the name of the component to inject. Again, this can be observable.params
— an object that will be passed on to the component. Typically this is a key-value object containing multiple parameters, and is typically received by the component’s viewmodel constructor.
Example:
<div data-bind='component: { name: "shopping-cart", params: { mode: "detailed-list", items: productsList } }'></div>
Note that whenever a component is removed (either because the name
observable changed, or because an enclosing control-flow binding removed the entire element), the removed component is disposed
Component lifecycle
When a component
binding injects a component,
-
Your component loaders are asked to supply the viewmodel factory and template
- Multiple component loaders may be consulted, until the first one recognises the component name and supplies a viewmodel/template. This process only takes place once per component type, since Knockout caches the resulting definitions in memory.
- The default component loader supplies viewmodels/templates based on what you have registered. If applicable, this is the phase where it requests any specified AMD modules from your AMD loader.
Normally, this is an asynchronous process. It may involve requests to the server. For API consistency, Knockout by default ensures that the loading process completes as an asynchronous callback even if the component is already loaded and cached in memory. For more about this, and how to allow synchronous loading, see Controlling synchronous/asynchronous loading.
-
The component template is cloned and injected into the container element
Any existing content is removed and discarded.
-
If the component has a viewmodel, it is instantiated
If the viewmodel is given as a constructor function, this means Knockout calls
new YourViewModel(params)
.If the viewmodel is given as a
createViewModel
factory function, Knockout callscreateViewModel(params, componentInfo)
, wherecomponentInfo.element
is the element into which the not-yet-bound template has already been injected.This phase always completes synchronously (constructors and factory functions are not allowed to be asynchronous), since it occurs every time a component is instantiated and performance would be unacceptable if it involved waiting for network requests.
-
The viewmodel is bound to the view
Or, if the component has no viewmodel, then the view is bound to any
params
you’ve supplied to thecomponent
binding. -
The component is active
Now the component is operating, and can remain on-screen for as long as needed.
If any of the parameters passed to the component is observable, then the component can of course observe any changes, or even write back modified values. This is how it can communicate cleanly with its parent, without tightly coupling the component code to any parent that uses it.
-
The component is torn down, and the viewmodel is disposed
If the
component
binding’sname
value changes observably, or if an enclosing control-flow binding causes the container element to be removed, then anydispose
function on the viewmodel is called just before the container element is removed from the DOM. See also: disposal and memory management.Note: If the user navigates to an entirely different web page, browsers do this without asking any code running in the page to clean up. So in this case no
dispose
functions will be invoked. This is OK because the browser will automatically release the memory used by all objects that were in use.
Note: Template-only components
Components usually have viewmodels, but they don’t necessarily have to. A component can specify just a template.
In this case, the object to which the component’s view is bound is the params
object that you passed to the component
binding. Example:
ko.components.register('special-offer', {
template: '<div class="offer-box" data-bind="text: productName"></div>'
});
… can be injected with:
<div data-bind='component: {
name: "special-offer-callout",
params: { productName: someProduct.name }
}'></div>
… or, more conveniently, as a custom element:
<special-offer params='productName: someProduct.name'></special-offer>
Note: Using component
without a container element
Sometimes you may want to inject a component into a view without using an extra container element. You can do this using containerless control flow syntax, which is based on comment tags. For example,
<!-- ko component: "message-editor" -->
<!-- /ko -->
… or passing parameters:
<!-- ko component: {
name: "message-editor",
params: { initialText: "Hello, world!", otherParam: 123 }
} -->
<!-- /ko -->
The <!-- ko -->
and <!-- /ko -->
comments act as start/end markers, defining a “virtual element” that contains the markup inside. Knockout understands this virtual element syntax and binds as if you had a real container element.
Note: Passing markup to components
The element you attach a component
binding to may contain further markup. For example,
<div data-bind="component: { name: 'my-special-list', params: { items: someArrayOfPeople } }">
<!-- Look, here's some arbitrary markup. By default it gets stripped out
and is replaced by the component output. -->
The person <em data-bind="text: name"></em>
is <em data-bind="text: age"></em> years old.
</div>
Although the DOM nodes in this element will be stripped out and not bound by default, they are not lost. Instead, they are supplied to the component (in this case, my-special-list
), which can include them in its output however it wishes.
This is useful if you want to build components that represent “container” UI elements, such as grids, lists, dialogs, or tab sets, which need to inject and bind arbitrary markup into a common structure. See a complete example for custom elements, which also works without custom elements using the syntax shown above.
Disposal and memory management
Optionally, your viewmodel class may have a dispose
function. If implemented, Knockout will call this whenever the component is being torn down and removed from the DOM (e.g., because the corresponding item was removed from a foreach
, or an if
binding has become false
).
You must use dispose
to release any resources that aren’t inherently garbage-collectable. For example:
setInterval
callbacks will continue to fire until explicitly cleared.- Use
clearInterval(handle)
to stop them, otherwise your viewmodel might be held in memory.
- Use
ko.computed
properties continue to receive notifications from their dependencies until explicitly disposed.- If a dependency is on an external object, then be sure to use
.dispose()
on the computed property, otherwise it (and possibly also your viewmodel) will be held in memory. Alternatively, consider using a pure computed to avoid the need for manual disposal.
- If a dependency is on an external object, then be sure to use
- Subscriptions to observables continue to fire until explicitly disposed.
- If you have subscribed to an external observable, be sure to use
.dispose()
on the subscription, otherwise the callback (and possibly also your viewmodel) will be held in memory.
- If you have subscribed to an external observable, be sure to use
- Manually-created event handlers on external DOM elements, if created inside a
createViewModel
function (or even inside a regular component viewmodel, although to fit the MVVM pattern you shouldn’t) must be removed.- Of course, you don’t have to worry about releasing any event handlers created by standard Knockout bindings in your view, as KO automatically unregisters them when the elements are removed.
For example:
var someExternalObservable = ko.observable(123);
function SomeComponentViewModel() {
this.myComputed = ko.computed(function() {
return someExternalObservable() + 1;
}, this);
this.myPureComputed = ko.pureComputed(function() {
return someExternalObservable() + 2;
}, this);
this.mySubscription = someExternalObservable.subscribe(function(val) {
console.log('The external observable changed to ' + val);
}, this);
this.myIntervalHandle = window.setInterval(function() {
console.log('Another second passed, and the component is still alive.');
}, 1000);
}
SomeComponentViewModel.prototype.dispose = function() {
this.myComputed.dispose();
this.mySubscription.dispose();
window.clearInterval(this.myIntervalHandle);
// this.myPureComputed doesn't need to be manually disposed.
}
ko.components.register('your-component-name', {
viewModel: SomeComponentViewModel,
template: 'some template'
});
It isn’t strictly necessary to dispose computeds and subscriptions that only depend on properties of the same viewmodel object, since this creates only a circular reference which JavaScript garbage collectors know how to release. However, to avoid having to remember which things need disposal, you may prefer to use pureComputed
wherever possible, and explicitly dispose all other computeds/subscriptions whether technically necessary or not.