Skip to content

Instantly share code, notes, and snippets.

@hjc
Created June 18, 2013 14:27
Show Gist options
  • Save hjc/5805799 to your computer and use it in GitHub Desktop.
Save hjc/5805799 to your computer and use it in GitHub Desktop.
Simple Knockout Binding Handler to enable auto focusing fields (i.e. when one field gets max input, jump to the other).
/**
* This binding has required parameters and should be an object, they are:
*
* observable - the observable we want to look at and evaluate to see if we
* should change focus
*
* evaluator - a function to determine if we should change focus. It should
* return true whenever we want to change focus and false otherwise, see
* below for how to add extra arguments onto the function. Any function can
* be used as long as you can provide the correct scope for it from the HTML
* binding. This likely has to be global scope. Alternatively, this can just
* be a number which corresponds to a length check where the focus is changed
* whenever the observable's value's length is >= to the number passed in.
*
* target - a CSS selector that tells JS where to focus next. It will be run
* through $, which will be whatever you set it to (i.e. jQuery or Prototype).
*
* note: can use an evaluator function. It should take one argument which is analogous to $data
* or the current value of the observable that has been bound (you can then reference it as a
* value, it is not an observable)
* note: if you want to pass extra arguments to your evaluator function, include them in the function
* and then use the .bind function in the data-bind attribute. For example, if my function was:
* self.checkEquals = function(value, equalor) { return value === equalor }
* and I wanted to change focus when field 1 (an observable named field1) and field 2 (another
* observable named field2) were equal, I would bind field1 like so:
* data-bind="autoFocus: {observable: field1, target: '#field2', evaluator.bind($data, field1(), field2())
* Now, when field1() gets updated and happens to equal field2(), we swap to field2!
* NOTE: $data should always be the first parameter to .bind so the scope of the binding is
* correct. If you have a LOT of parameters to pass your evaluator function and you're not sure
* what value (or where said value will come from) to pass it, you can make the value passed by
* KO your last parameter and then not bind that parameter (i.e. call bind with X number of
* arguments where X is the number of args your function has [NOTE: 1 ARG IS FOR THIS!!! X + 1
* ARGS TO BIND WOULD BIND ALL ARGS!!!]). The binding will pass the value of the observable to
* the function, and correctly.
* note: if a disabled element is going to be the target of an autofocus, it needs to load in with
* disabled, otherwise this binding gets run before knockout disables it, i.e. if your UI starts
* out disabled and with data populated
* note: when using this with the value binding, you need to change the value
* update to update on keyup, otherwise this just doesn't work. Add this line:
* valueUpdate: 'keyup',
* target overrides focus
* @type {{update: Function}}
*/
ko.bindingHandlers.autoFocus = {
//params: element: actual DOM element
// valueAccessor: gets current model property (function)
// allBindingsAccessor: function that you can use to get ALL the model's properties.
// data: the viewModel the bindings were applied to
// ctx: an object that holds the binding context availabe to this element's bindings, has goodies like $parent and $root
update: function(element, valueAccessor, allBindingsAccessor, data, ctx) {
var options = valueAccessor() || {};
//we need an observable to watch, otherwise we can't check values or
// get anything dynamically
if (options.hasOwnProperty('observable')) {
//the value we're going to use to test will be stored under val and
// SHOULD be an observable, but that's not needed
var val = options.observable;
//if this is a function, run it with no arguments to get the true value
// we need to evaluate under, otherwise leave it val untouched
if (typeof options.observable == "function") {
val = options.observable();
}
//now we need SOME sort of evaluator. For us it can be either a
// function or a number. If its a function, we will run the
// function and pass it the value of the observable. If the evaluator
// returns true, we change focus, otherwise do nothing. If the
// evaluator is a number we will make an assumption: that the user
// wants the field to change focus whenever the string in that box
// gets to that length (where length === evaluator). We will do
// this since this is a VERY common use of autoFocus (consider a
// phone number split into 3 fields)
if (options.hasOwnProperty('evaluator')) {
//dont want to overwrite eval....
var _eval = options.evaluator;
if (typeof _eval == "function") {
} else if (typeof _eval == "number") {
//they gave us a number, they want a length check, so lets
// set it up
var length = parseInt(_eval, 10);
//go ahead and set eval to our new function for convenience,
// that way our code is DRY and we use the same logic to
// evaluate no matter what
_eval = function() {
return val.length >= length;
};
}
//allow users to key the focus target under target or focus to make
// the binding a bit more intuitive
if (options.hasOwnProperty('target') || options.hasOwnProperty('focus')) {
var target = options.target || options.focus;
if (_eval(val)) {
var elem = $(target);
//if the target element has been disabled or is hidden,
// we don't want to focus is it since that makes no sense
if (this.firstRun) {
this.firstRun = false;
return;
}
if (elem.attr('disabled') === undefined && elem.css('display') !== 'none') {
$(target).focus();
$(target).select();
}
}
} else {
console.log('Warning: autoFocus binding NEEDS a DOM element to swap focus to when evaluator is true!' +
' This should be a string selector keyed under: "target".');
return;
}
} else {
console.log('Warning: autoFocus binding NEEDS an evaluation function keyed under the "evaluator" key to work!' +
' This function should return a true when we should change focus and a false if not.');
return;
}
} else {
console.log('Warning: autoFocus binding NEEDS an observable keyed under the "observable" key to work!');
return;
}
//if this is a function, subscribe to it
/*if (typeof options.observable == 'function') {
var subscription = options.observable.subscribe(this);
}*/
},
init: function(element, valueAccessor, allBindingsAccessor, data, ctx) {
return;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment