Knockout.Punches 0.5.0

Enhanced binding syntaxes for Knockout 3+

View the Project on GitHub mbest/knockout.punches

Knockout.Punches

Using the new APIs in Knockout 3.0.0, this plugin provides a set of enhanced binding syntaxes. Each syntax can be enabled individually as described in the sections below. But generally you’ll just enable everything as follows:

ko.punches.enableAll();

Embedded bindings – Handlebars style

Text

Rather than using the text binding, you can use double curly-brace syntax to insert dynamic text content. This method works by converting {{expression}} markup to <!--ko text:expression--><!--/ko--> before binding. For example:

<div>Hello {{name}}.</div>

HTML

If you want to insert HTML markup and have it rendered as HTML instead of as text, you can use triple curly-braces. You should be careful to only use this binding with trusted model values; otherwise you open up the possibility of a script injection attack. Example:

<div>{{{ content }}}</div>

Other bindings

You can embed any binding that works with virtual elements, including if, ifnot, foreach, with, and template, by using the block-section syntax. Examples:

<!-- With a colon -->
<ul>
    {{#foreach: items}}
        <li>The current item is: {{name}}</li>
    {{/foreach}}
</ul>

<!-- Without a colon -->
{{#ifnot items().length}}
    <div>There are no items.</div>
{{/ifnot}}

<!-- Self-closing -->
{{#template 'addItem'/}}

These syntaxes are enabled by default through ko.punches.enableAll(). To enable them specifically, call ko.punches.interpolationMarkup.enable().

Attributes

Rather than using the attr binding, you can use double curly-brace syntax to insert dynamic attributes. For example:

<div title="{{userName}}">Element content</div>

This method works by converting title="{{expression}}" markup to data-bind="attr.title: expression" before binding. It intelligently handles multiple expressions intermixed with text, and multiple attribute bindings. For example, "This is the {{which}} option." is converted to "This is the " + ko.unwrap(which) + " option.".

If the attribute name matches the name of a binding handler, that handler will be used instead of attr. This means you can use this syntax for two-way bindings, such as value. For example:

<input value="{{myTitle}}" />
Note that embedded bindings may not work correctly for all attributes.

This syntax is enabled by default through ko.punches.enableAll(). To enable it individually, call ko.punches.attributeInterpolationMarkup.enable().

Text filters

Rather than directly calling a function or using a computed observable to format output, you can use the filtering syntax. For example:

<span data-bind="text: name | fit:20 | uppercase"></span>

This method works by converting expression|filter[:arg1...] to ko.filters.filter(expression, arg1 ...) before binding. A matching filter function must exist in ko.filters.

This syntax is enabled by default for the text and attr bindings through ko.punches.enableAll(). To enable it for individual bindings, call ko.punches.textFilter.enableForBinding(<binding>).

Built-in filters

Knockout.Punches includes the following filters:

  1. default:<defaultValue> - If the value is blank, null, or an empty array, replace it with the given default value.
  2. fit:<length>[:<replacement>][:<where>] - Trim the value if it’s longer than the given length. The trimmed portion is replaced with ... or the replacement value, if given. By default, the value is trimmed on the right but can be changed to left or middle through the where option. For example: name | fit:10::'middle' will convert Shakespeare to Shak...are.
  3. json[:space] - Convert the value to a JSON string using ko.toJSON. You can give a space value to format the JSON output.
  4. lowercase - Convert the value to lowercase.
  5. number - Format the value using toLocaleString.
  6. replace:<search>:<replace> - Perform a search and replace on the value using String#replace.
  7. uppercase - Convert the value to uppercase.

Custom filters

You can create your own filters by adding them to ko.filters. Here is an example:

// Custom filter can be used like "| append: 'xyz'"
ko.filters.append = function(value, arg1) {
    return '' + value + arg1;
};

Namespaced dynamic bindings

When you have a set of bindings with the same functionality, the namespacing syntax allows you to dynamically create the handlers for those bindings based on which ones are used. For example, if you want to bind to arbitrary data attributes, you could create a data namespace, which you would then bind as follows:

<div data-bind="data.color: color"></div>

Here’s how you would define the handler for the data namespace:

ko.bindingHandlers.data = {
    getNamespacedHandler: function(binding) {
        return {
            update: function(element, valueAccessor) {
                element.setAttribute('data-' + binding, ko.unwrap(valueAccessor()));
            }
        };
    }
};

As the bindings in your documents are processed, Knockout.Punches looks for bindings with the format x.y: value that don’t already have a binding handler. It then creates an x.y handler using the x namespace handler, or if none is found, it uses a default handler that calls the x binding with the value {y: value}. This default behavior allows you to use the namespace syntax to bind events, attributes, styles, and classes using the event, attr, style, and css namespaces respectively. For example:

<div data-bind="style.color: currentProfit() < 0 ? 'red' : 'black'"></div>

Automatic namespacing

Namespaced bindings allow you to define and interact with dynamic bindings just like any other binding. For example, suppose you want to enable the filter syntax for the title attribute; you can do so by referencing it using attr.title like this: ko.punches.textFilter.enableForBinding('attr.title') This will work as long as you bind the title attribute using attr.title. But what if you want to use filters with the original attr syntax like attr: {title: name | caps}? Knockout.Punches provides a simple solution, a preprocessor that converts the attr: {title: name} syntax to attr.title: name. This preprocessor is enabled by default through ko.punches.enableAll() for the attr, css, event, and style bindings. To enable it individually, call ko.punches.namespacedBinding.enableForBinding(<binding>).

Wrapped event callbacks

When binding functions in your model to events, it’s easy to simply provide the function reference to the binding such as click: $parent.removePlace. When bound this way, though, the reference to $parent is lost; the removePlace method will be called with a default this value (the current model object). (See this Github issue for more information.)

By wrapping the method call in an anonymous function, like click: function() {$parent.removePlace($data)}, the this value is set correctly. The wrapped callback preprocessor in Knockout.Punches does this for you, so you can use the simple reference syntax and know that this is the correct object in your method.

This preprocessor is enabled by default through ko.punches.enableAll() for the click, submit, and event bindings, as well as for optionsAfterRender, template#beforeRemove, template#afterAdd, and template#afterRender. To enable this preprocessor individually, call ko.punches.wrappedCallback.enableForBinding(<binding>) for each binding that you want to use it with. If you want this functionality for a binding parameter, such as template#afterRender, call ko.punches.preprocessBindingProperty.addPreprocessor('template', 'afterRender', ko.punches.wrappedCallback.preprocessor). If you want to use it for all dynamically created bindings with a certain namespace (such as event), call ko.punches.namespacedBinding.addDefaultBindingPreprocessor('event', ko.punches.wrappedCallback.preprocessor).

Expression-based event handling

If you’re familiar with the original on... syntax for defining event handlers, you may wish that Knockout allowed you to bind an expression to events. Knockout.Punches provides this ability for event handling using the on namespace. Thus, for example, if you want to run an expression when the user clicks a button, you could bind it like this:

<button type="button" data-bind="on.click: x = x + 1">Increment</button>

Any of the model and context properties are available in the expression. In addition, you can access the event object through $event.

Using expression syntax for callback bindings

If you want to use the expression syntax for other bindings, you can enable it using ko.punches.expressionCallback.enableForBinding(<binding>, <args>); The args parameter is a string that defines the names of the parameters available in the expression. For example, you could enable this syntax for the click binding using ko.punches.expressionCallback.enableForBinding('click', '$data,$event')

API reference

Background

Knockout 3.0 includes three new APIs to extend the binding system with new syntaxes. Here’s a quick summary of the three methods:

  1. Extend ko.getBindingHandler to dynamically create binding handlers.
  2. Implement ko.bindingProvider.instance.preprocessNode to modify or replace DOM nodes before bindings are processed.
  3. Implement <bindingHandler>.preprocess to modify the binding value before it is evaluated.

License and Contact

License: MIT (http://www.opensource.org/licenses/mit-license.php)

Michael Best
https://github.com/mbest
mbest@dasya.com