Cleanup Binding - Areson/Knockout-Bindings GitHub Wiki

Cleanup Binding

Knockout is pretty awesome, and I love being able to achieve response pages with very little work. However, whenever I have to setup fairly complex bindings my HTML ends up looking like something threw up on it:

<ul data-bind="sortable: { data: servers, afterMove: $root.updateServerLocation, 
options: {cursor: 'move', dropOnEmpty: true}, isEnabled: $root.sortEnabled, }, 
css: {'empty-list-padding': hasNoServers}">
	<li data-bind="css: {odd: $index() % 2 == 0, even: $index() % 2 == 1, 
	'list-selected': meta.editing}" class="sortable-item">
		<div class="server-working">
			<div data-bind="meta.statusIcon: {icon: meta.statusIcon.icon, 
			title: meta.statusIcon.title}, fadeVisible: meta.statusIcon.show"></div>
		</div>
		<input type="checkbox" data-bind="jqCheckbox: {checked: meta.editing, id: 'server_'  + serverId,
 		label: {text: 'Edit Server'}, options: {icons: {primary: 'ui-icon-pencil'}, text: false}},
 		jquioptions: {button: {disabled: meta.noEdit}, tooltip: {disabled: meta.noEdit}}"></input>`
	</li>
</ul>

I've already created custom bindings to encapsulate some of the behavior, and added additional computed observables, but there is only so much one can do to trim the fat. What I wanted was programmatic bindings, but then I lost the context that was inherent to the HTML structure, which meant I would need to attach classes or IDs to each element and then clutter up my javascript.

Enter the cleanup binding. The cleanup binding allows me to define bindings programmatically, but reference them in the declarative fashion that Knockout prefers, but with a clean shorthand instead.

Example - Basic Bindings

The simplest way to use the cleanup binding is to define bindings within your view model:

function Model() {
	this.text = ko.observable(text);
	this.visible = ko.observable(true);

	this.binding = {
		text: this.text,
		visible: this.visible
	}
}

and then reference them in your HTML using the cleanup binding:

<span data-bind="cleanup: [binding]"></span>

Selectable Bindings

Using the above model, let's assume that instead of just conditionally showing a single span of text, we instead wanted to always show our model text, but then conditionally show some fixed message. We could create two bindings on our model, like this:

	this.bindingText: {...};
	this.bindingVisible: {...};

If we had a lot of bindings, we'd end up having a large number of cluttered bindings all together. Instead we can tell the cleanup binding which bindings we want to use and it will assemble them for us. In order to do this though we have to used named bindings. These are very similar to our original binding though!

this.bindingCollection = {
	textBinding: {
		text: this.text
	},
	visibleBinding: {
		visible: this.visible
	}
};

...and indicate which bindings to use:

<h1 data-bind="cleanup: [bindingCollection, 'textBinding']"></h1>
<span data-bind="cleanup: [bindingCollection, 'visibleBinding']">Foobar!</span>

The cleanup binding also allows us to combine multiple named bindings together, so we could also do this:

<span data-bind="cleanup: [bindingCollection, 'visibleBinding,textBinding']">Foobar!</span>

Reusable Bindings

If we had a more complicated view, we would probably have some additional models that were referenced in some sort of template, possibly using Knockouts foreach binding. We'd probably want to be able to display text and control the visibility of the child elements as well, but our bindings are setup specifically for our parent model. Since we want to be DRY, we'd rather not create the same bindings again on each view model we create.

The cleanup binding allows for bindings that are functions. When it sees a binding that is a function, it passes the binding context to the function to use when setting up. Let's expand our view model and update our bindings:

function Child(num) {
	this.text = ko.observable('child: ' + num);
	this.visible = ko.observable(true);
}

function Model() {
	this.text = ko.observable('parent');
	this.visible = ko.observable(true);
	this.children = ko.observableArray([new Child(1), new Child(2)]);
	
	this.bindingCollection = {
		textBinding: function(context) {
			return {text: context.$data.text};
		},
		visibleBinding: function(context) {
			return {visible: context.$data.visible}
		}
	};
}

Now we can reuse these bindings on the child view models as well:

<h1 data-bind="cleanup: [bindingCollection, 'textBinding']"></h1>
<span data-bind="cleanup: [bindingCollection, 'visibleBinding']">Foobar!</span>
<!-- ko foreach: children -->
<span data-bind="cleanup: [$root.bindingCollection, 'textBinding,visibleBinding']"></span>
<!-- /ko -->

Registered Bindings

The cleanup binding allows us to register a binding with it, which makes that binding available to any of our view models, not just the ones we define the binding within. To register a view model, we can call the cleanup register method. Using this, our model now becomes:

ko.cleanup.register('textBinding', function(context) {
	return {text: context.$data.text};
});

ko.cleanup.register('visibleBinding', function(context) {
	return {visible: context.$data.visible}
});

function Child(num) {
	this.text = ko.observable('child: ' + num);
	this.visible = ko.observable(true);
}

function Model() {
	this.text = ko.observable('parent');
	this.visible = ko.observable(true);
	this.children = ko.observableArray([new Child(1), new Child(2)]);
}

Because we are referencing a registered binding, we no longer have to pass the collection of bindings to cleanup, instead we can just pass the names of the bindings we want to use:

<h1 data-bind="cleanup: ['textBinding']"></h1>
<span data-bind="cleanup: ['visibleBinding']">Foobar!</span>
<!-- ko foreach: children -->
<span data-bind="cleanup: ['textBinding,visibleBinding']"></span>
<!-- /ko -->

Context Filtering

Reusable bindings are nice, but we don't always have the same structure in our view models. For example, what if our child view model was setup like this:

function Child(num) {
	this.foo = {};
	this.foo.text = ko.observable('child: ' + num);
	this.visible = ko.observable(true);
}

Our bindings will no longer work! Fortunately, cleanup will let us filter our binding context for each binding we reference, which means with a little refactoring we can make our bindings reusable again:

ko.cleanup.register('textBinding', function(context) {
	return {text: context.text};
});

ko.cleanup.register('visibleBinding', function(context) {
	return {visible: context.visible}
});

Notice that we are no longer deciding what portion of the binding context we are using. Instead, these bindings will rely on cleanup to filter them. We can specify that filtering with an additional argument to the cleanup binding:

<h1 data-bind="cleanup: ['textBinding', '$data']"></h1>
<span data-bind="cleanup: ['visibleBinding', '$data']">Foobar!</span>
<!-- ko foreach: children -->
<span data-bind="cleanup: ['textBinding,visibleBinding', '$data.foo,$data']"></span>
<!-- /ko -->

Performance

The cleanup binding seems to stack up pretty well against Knockout's normal binding process, at least in the simple example I put together: Cleanup Binding Performance

⚠️ **GitHub.com Fallback** ⚠️