A components library for Knockout.js (3.0 and above)
knockout-components is a small library that sits on top of knockout.js and provides a framework to reuse and encapsulate logic in a semantic fashion.
Inspired from the new Web Components Spec and other polyfill libraries like polymer, although knockout-components comes packed the power of knockout.js.
To define a custom component, one must put a component declaration in the ko.components
namespace, where ko.components['widget-name']
corresponsed to a <widget-name>
HTML element being defined.
ko.components['widget-name'] = {
template: '<div>a custom template string or id name of template in document</div>',
attributes: [/* an array of attributes to pass into constructor */],
defaults: {
/* default values for attributes */
},
ctor: function (attributes, viewModel, bindingContext) {
/* construct your widget instance. the template
* is bound to the value of `this`
*/
}
};
simple example:
ko.components['placekitten'] = {
template: '<img data-bind="attr: { src: src, width: width, height: height }" />',
attributes: ['width', 'height'],
defaults: {
width: 100,
height: 100
},
ctor: function (attributes, viewModel, bindingContext) {
this.src = ko.computed(function(){
return 'http://placekitten.com/'
+ ko.unwrap(attributes.width) + '/'
+ ko.unwrap(attributes.height);
});
}
};
ko.components['my-component'] = {
ctor: function (attributes, viewModel, bindingContext) {
}
};
The ctor
property of the component definition is the widget's constructor function. It is called once every time the widget is created and the value bound to this
(or the value the function returns) is what the template is bound to.
ko.components['random-number'] = {
template: '<span data-bind="text: value"></span>',
ctor: function () {
this.value = Math.random();
}
};
The constructor function has three parameters passed into it. In order,
attributes
param:This is an object with values specified through HTML attributes on the widget element. The attribute values are parsed with regard to the current knockout binding scope, and can be thought of as similar to the data-bind
attributes that knockout uses.
For example, we can declare a "fancy-name" element as follows:
ko.components['fancy-name'] = {
template: '<div data-bind="text: fullName"></span>',
attributes: ['first', 'last'],
ctor: function (attributes) {
this.first = attributes.first;
this.last = attributes.last;
this.fullName = ko.computed(function(){
return [ko.unwrap(this.first), ko.unwrap(this.last)].join(' ');
}, this);
}
};
In this case, I would use a <fancy-name>
component like:
<input data-bind="value: firstName"/>
<input data-bind="value: lastName"/>
<fancy-name data-first="firstName" data-last="lastName"></fancy-name>
ko.applyBindings({ firstName: ko.observable(), lastName: ko.observable() });
viewModel
param:The second parameter of the constructor function is the viewmodel that was in current binding scope where the tag was declared, (ie, it will be whatever $data
of the binding context was)
This is useful if your component is somehow behaves differently depending on the context where it was bound, or if it uses the parent viewModel in any way.
//TODO: good example of this here.
bindingContext
param:The third parameter of the constructor function is the binding context where the tag was declared.
ko.components['fancy-name'] = {
/* ... */
ctor: function (attributes, viewModel, bindingContext) {
if(bindingContext.$data === viewModel) {
// this will always be true.
}
}
};
It is recommended that the following CSS be included at the head of the page when using knockout-components in order to prevent a Flash of unstyled content:
:unresolved { opacity: 0; }
which will hide all custom tag content from rendering until bound by knockout.
Inside your component template, you can use the special HTML <content>
element to specify insertion points, along with an optional select
attribute which uses a CSS selector (only tag names and class names allowed right now).
For instance:
Let's say I have the following template defined for a custom component <foo>
:
<template id="foo_template">
<content select="h2"></content>
<content select=".title"></content>
<hr />
<div>
<div><b>Description:</b></div>
<content select="*"></content>
</div>
</template>
and then in our markup we have a foo widget:
<foo>
<h1 class="title">My Title</h1>
<h2>My Subtitle</h2>
<div>some cool description</div>
</foo>
The markup will get transcluded, and after binding will be:
<h2>My Subtitle</h2>
<h1 class="title">My Title</h1>
<hr />
<div>
<div><b>Description:</b></div>
<div>some cool description</div>
</div>
Or perhaps a more practical example:
<template id="modal_template">
<div class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-bind="click: close">×</button>
<h4 class="modal-title" data-bind="text:title"></h4>
</div>
<div class="modal-body">
<content></content>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-bind="click: close">Close</button>
<button type="button" class="btn btn-primary" data-bind="click: save, text: okText"></button>
</div>
</div>
</div>
</div>
</template>
with the markup:
<modal data-title="'Save Document?'" data-ok-text="'Save Changes'">
<h4>Your document has pending changes.</h4>
<p>
If you leave without saving, some of your work could be lost.
</p>
</modal>
attributes: ['foo','bar']
corresponds to the data-* attributes <my-tag data-foo="..." data-bar="..."></my-tag>
. It is possible I might release this constraint and allow any attributes to be defined so that it is more legible and compact, although I am not sure what sort of issues this could cause when they attribute names are used that have other meanings. If anyone has any opinions on this, I would like to hear them.prototype
option to the component declaration which automatically get's mapped to the actual constructor prototype, with the value of this
correctly bound to everything. Technically, this is already easily possible, just not super clean syntactically (ie, just extend ko.components['tag-name'].ctor.prototype
.