十个有用的knockout binding用法

89233916 2015-01-07

Knockout.js is a very powerful library to build interactive web applications in. The creators of knockout tried really hard to keep it as light-weight as possible (and for good reason). That being said, if you're using knockout to do a lot, it often isn't long before your markup is filled with not-so-pretty-looking data-bind attribute selectors.

One of the most criminally unused features of knockout tends to be custom binding handlers. I've talked at length about them before, but I thought it would be nice to simply enumerate a couple of really simple binding handlers which I have found invaluable... and if not, at least aesthetically helpful!

href and src attributes

It's pretty common to bind <img /> elements or anchor tags programmatically... but how annoying is it to have to use the attr binding handler? You can quickly create "shortcut" binding handlers that give you a cleaner syntax.

ko.bindingHandlers.href ={
    update:function(element, valueAccessor){
        ko.bindingHandlers.attr.update(element,function(){return{ href: valueAccessor()}});}};

ko.bindingHandlers.src ={
    update:function(element, valueAccessor){
        ko.bindingHandlers.attr.update(element,function(){return{ src: valueAccessor()}});}};

Thus we now have:

<imgdata-bind="src: imgSrc"/><adata-bind="href: myUrl">Click Me</a><!-- instead of --><imgdata-bind="attr: { src: imgSrc }"/><adata-bind="attr: { href: myUrl }">Click Me</a>

Hidden

It's easy to quickly get annoyed by the asymmetry of some of the bindings which consume booleans. This is because knockout's binding parser allows you to simply reference an observable by name... ie, text: myTextObservable which is equivalent totext: myTextObservable(), however, to do anything more complicated than that, the parser expects a valid expression, in which case you then need to evaluate the observable and do it the second way.

A common example I run into is the visible binding (one of the top 3 most commonly used bindings in my experience). However, sometimes what you really want is a hiddenbinding. No problem!

ko.bindingHandlers.hidden ={
    update:function(element, valueAccessor){var value = ko.utils.unwrapObservable(valueAccessor());
        ko.bindingHandlers.visible.update(element,function(){return!value;});}};

And thus, we have:

<formdata-bind="hidden: hideForm">
    ...
</form><!-- instead of --><formdata-bind="visible: !hideForm()">
    ...
</form>

Instant Value

Of course any interactive web application ought to have user inputs, and thus we find ourselves using the value binding handler.

Well, part of the fun of using a fancy javascript library such as knockout.js is to give our users immediate feedback. As a result, I found myself using the value binding with it's binding option valueUpdate: 'afterkeydown' more often than not! If you're like me, writing that darn option every time is a sight for sore eyes... let's get rid of it:

ko.bindingHandlers.instantValue ={
    init:function(element, valueAccessor, allBindings){var newAllBindings =function(){// for backwards compatibility w/ knockout  < 3.0return ko.utils.extend(allBindings(),{ valueUpdate:'afterkeydown'});};
        newAllBindings.get=function(a){return a ==='valueupdate'?'afterkeydown': allBindings.get(a);};
        newAllBindings.has =function(a){return a ==='valueupdate'|| allBindings.has(a);};
        ko.bindingHandlers.value.init(element, valueAccessor, newAllBindings);},
    update: ko.bindingHandlers.value.update
};

And thus, we can now use:

<inputdata-bind="instantValue: val"/><!-- instead of --><inputdata-bind="value: val, valueUpdate: 'afterkeydown'"/>

Note: If you want, you can even replace the original value binding handler. I don't recommend this, since you are overriding some default behavior of knockout, but if you like to live on the edge, go for it:

(function(original)){var extend = ko.utils.extend,
        unwrap = ko.utils.unwrapObservable;
    ko.bindingHandlers.value ={
        init:function(element, valueAccessor, allBindingsAccessor){var origBindings = allBindingsAccessor(),
                origValueUpdate = origBindings.valueUpdate,
                newAllBindings =function(){return origValueUpdate ===undefined? extend(origBindings,{valueUpdate:'afterkeydown'}: origBindings;};return original.init(element, valueAccessor, newAllBindings);},
        update: original.update
    };}(ko.bindingHandlers.value));

Toggle

This little gem can really help reduce some clutter in your viewmodels. Often times you need to bind a click handler that simply toggles (or negates) a boolean observable. voila:

ko.bindingHandlers.toggle ={
    init:function(element, valueAccessor){var value = valueAccessor();
        ko.applyBindingsToNode(element,{
            click:function(){
                value(!value());}});}};

Usage:

<buttondata-bind="toggle: isHidden">Show / Hide</button><!-- instead of --><buttondata-bind="click: function(){isHidden(!isHidden());}">Show / Hide</button>

Time Ago

Not every application needs this, but I've certainly used it plenty. Dates are a tough thing to get right in javascript. Especially when your users could be from anywhere in the world, in any time zone. There are certainly more sophisticated approaches, but one approach is just to ditch the exact time and show a relative time.

While you can certainly replace my hacky relative-time function with something more complex, like moment.js, but this did the trick for me since I just needed "time ago" dates (and only dates in the past).

function toTimeAgo (dt){var secs =(((newDate()).getTime()- dt.getTime())/1000),
        days =Math.floor(secs /86400);return days ===0&&(
        secs <60&&"just now"||
            secs <120&&"a minute ago"||
            secs <3600&&Math.floor(secs /60)+" minutes ago"||
            secs <7200&&"an hour ago"||
            secs <86400&&Math.floor(secs /3600)+" hours ago")||
        days ===1&&"yesterday"||
        days <31&& days +" days ago"||
        days <60&&"one month ago"||
        days <365&&Math.ceil(days /30)+" months ago"||
        days <730&&"one year ago"||Math.ceil(days /365)+" years ago";};

ko.bindingHandlers.timeAgo ={
    update:function(element, valueAccessor){var val = unwrap(valueAccessor()),
            date =newDate(val),// WARNING: this is not compatibile with IE8
            timeAgo = toTimeAgo(date);return ko.bindingHandlers.html.update(element,function(){return'<time datetime="'+ encodeURIComponent(val)+'">'+ timeAgo +'</time>';});}};

With this you can use:

<divdata-bind="timeAgo: dateCreated"></div>

Which will result in the following machine-and-human-friendly HTML being created:

<divdata-bind="timeAgo: dateCreated"><timedatetime="2014-03-04T03:19:06.627">3 hours ago</time></div>

Currency

It seems more often than not, the need to format currency arises. Why fight it? Although it might be overly simplistic for some use cases, the following binding has done the job reasonably well for me.

ko.bindingHandlers.currency ={
    symbol: ko.observable('$'),
    update:function(element, valueAccessor, allBindingsAccessor){return ko.bindingHandlers.text.update(element,function(){var value =+(ko.utils.unwrapObservable(valueAccessor())||0),
                symbol = ko.utils.unwrapObservable(allBindingsAccessor().symbol ===undefined? allBindingsAccessor().symbol
                            : ko.bindingHandlers.currency.symbol);return symbol + value.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g,"$1,");});}};

This allows you to globally change the "symbol" being used as currency, as well as replace it only locally.

For example:

<spandata-bind="currency: price"></span><!-- Use a different dollar sign --><spandata-bind="currency: price, symbol: '€'"></span>

Which, with price = 1853.251; will produce standard formatted currency:

<spandata-bind="currency: price">$1,853.25</span><!-- Use a different dollar sign --><spandata-bind="currency: price, symbol: '€'">€1,853.25</span>

Markdown

Although this requires an external dependency, it's worth simply pointing out how simple it is to integrate an external library into knockout. Look how easy it is to pull in a markdown compiler:

var converter =Markdown.getConverter();
ko.bindingHandlers.markdown ={
    update:function(element, valueAccessor){return ko.bindingHandlers.html.update(element,function(){return converter.makeHtml(ko.utils.unwrapObservable(valueAccessor()));});}};

And then it's as easy as:

<divdata-bind="markdown: markdownSource"></div>

Stop Binding

I can't take credit for this incredibly simple but useful binding. Ryan Neimeyer originally introduced me to this concept.

For large knockout applications, it is inevitable that you will want to bind specific subsections of HTML with a viewmodel that's isolated from everything else. It is important to make sure that that HTML subtree stays completely unbinded from anything else. Well here is a simple way to do it:

ko.bindingHandlers.stopBinding ={
    init:function(){return{ controlsDescendantBindings:true};}};
ko.virtualElements.allowedBindings.stopBinding =true;

Usage:

<!-- ko stopBinding: true --><divid="widget">
    ...
</div><!-- /ko -->

toJSON

There may be a use for this in an actual app, but I have mostly found it useful as a debugging tool.

Knockout comes with a useful utility function, ko.toJSON. Although it has many other uses, you can use it to quickly bind entire viewmodels (which may have a bunch of observable properties) and quickly see what sort of changes are going on in your viewmodel. So simple:

ko.bindingHandlers.toJSON ={
    update:function(element, valueAccessor){return ko.bindingHandlers.text.update(element,function(){return ko.toJSON(valueAccessor(),null,2);});}};

Usage:

<h3>Debug Info:</h3><predata-bind="toJSON: $root"></pre>

That's all I got for today. Have any useful binding handlers that you've built for knockout? Share them below!

相关推荐