Part one.
After the first round, I felt compelled to KnockOut the code a bit more. I’d
mentioned I wasn’t pleased with the code exactly. It needed some refactoring.
So, I’ve created a new Knockout binding handler. This binding handler replaces
named parameters with a model’s properties in a path.
For example, given this object:
Property Name |
Value |
id |
A123 |
first_name |
Aaron |
state |
WI |
The following paths would be converted thusly:
Original |
Replaced |
/person/{id} |
/person/A123 |
/person/{state}/{id} |
/person/WI/A123 |
You get the idea. Here’s the JavaScript code:
ko.bindingHandlers['route'] = {
// Examples:
// <a data-bind="route: {model: $data, url: 'person_details', attr: 'href' }" >
// or, you can shortcut the syntax to default to the currently bound object and just pass
// the url or the route name as a string directly
// <a data-bind="route: 'person_details' }" >
update: function (element, valueAccessor, allBindingsAccessor, $data) {
var valueUnwrapped = ko.utils.unwrapObservable(valueAccessor());
var options = ko.bindingHandlers.route.options || {};
// look for the model property
var model = ko.utils.unwrapObservable(valueUnwrapped['model']);
if (typeof model !== 'object') {
model = ko.utils.unwrapObservable($data);
if (typeof model === 'undefined' || model === null) {
throw new Error('set route model to object (or nothing bound?)');
}
}
// look for the url property first
var url = ko.utils.unwrapObservable(valueUnwrapped['url']);
// validate we've got something as a url (might be a name, might be a full url)
if (typeof url !== 'string' || url == "") {
url = valueUnwrapped;
if (typeof url !== 'string' || url == "") {
throw new Error("set route url property to route name or url directly");
}
}
// is it on the keyed collection?
var map = options.map;
if (typeof map !== 'undefined' && map !== null) {
if (map.hasOwnProperty(url)) {
url = map[url];
}
}
// check for a routing function as well
var fn = options.routeNameToUrl;
if (typeof fn === 'function') { url = fn.call(null, url); }
// did we get something meaningful?
if (url !== null && url !== '' && url.length > 0) {
url = ko.bindingHandlers.route.buildUrl(url, model);
}
// the url might need some fixin after a routing, anything goes here (might just be a default)
fn = options.fixUrl;
if (typeof fn === 'function') { url = fn.call(null, url); }
element.setAttribute(ko.utils.unwrapObservable(valueUnwrapped['attr']) || 'href', url);
},
// given a model, this function replaces named parameters in a simple string
// with values from the model
// /path/to/some/{id}/{category}
// with object { 'id' : 'abc', 'category' : 'cars' }
// becomes
// /path/to/some/abc/cars
buildUrl : function(url, model) {
// unfixed if there's not a thing
if (typeof model === 'undefined' || model === null) { return url; }
var propValue;
for (var propName in model) {
if (model.hasOwnProperty(propName)) {
propValue = model[propName];
if (ko) { propValue = ko.utils.unwrapObservable(propValue); }
if (typeof propValue === 'undefined' || propValue === null) {
propValue = "";
} else {
propValue = propValue.toString();
}
url = url.replace('{' + propName.toLowerCase() + '}', propValue);
}
}
return url;
},
options: {
// ** convert a route name to a url through whatever means you'd like
// routeNameToUrl : function(routeName) { return url; }
// ** anything you want, called after routeNameToUrl, might add a virtual directory
// ** for example
// fixUrl: function(url) { return url; }
// ** A map route names to URLs **
// all other functions are called if set (to possibly override this)
// this is not required if you use one of the other functions
// map : { 'a_route_name' : '/path/to/something/{id}/{action}' }
}
};
In another JavaScript file, I did initialize some of the options:
ko.bindingHandlers.route.options.routeNameToUrl = getRoute;
ko.bindingHandlers.route.options.fixUrl = app_url;
The getRoute function just maps a route name to a path, and the
app_url prepends the virtual directory to the path as needed.
Here it is in use:
<div data-bind="foreach: data.persons">
<h3 class="title" data-bind="text: Title"></h3>
<div>
<a data-bind="route: { model: $data, url: 'person_details', attr: 'href' } ">Details2</a>
<a data-bind="route: '/data/details/{id}/{title}' ">Details</a>
<a data-bind="route: 'person_details' ">Details</a>
</div>
</div>
You’ll probably like the new way better syntactically at least compared to the
old way. A route binding requires one
input when used in it’s most basic form:
-
url = the URL or route name to use as the template for the
replacement. It should contain (or later resolve to) curly-braced enclosed
property name keys which will be substituted by values from the model. The value
of the property could be either a route name (see options below) or a path.
When using just the basic form you can use the shortened syntax:
<a data-bind="route: 'research_details' ">Details</a>
This handily binds to the current object (via the bindingContext.$data property of
the bindingHandler update function call, which is also the fourth
parameter, which I’ve renamed to $data rather than the typical viewModel). So, you
won’t need to necessarily (explicitly) set the model for the binding.
If you need a a bit more control, you can change the syntax and get access to a few
other options (including direct access to the model you want to bind to if the
current item isn’t directly what you want to use).
-
model = this is the object that contains the properties and
values to be used as replacements within the url
-
attr (optional) = the name of the attribute to set the
generated url into. Defaults to href if not set.
There are a few global options you can control as well:
-
routeNameToUrl = (function)(routeName) optionally, given a
route name, should return the path (or the original value). Here you can do a
lookup of routeName to path.
-
map = {object} the object properties should be route names and
set equal to the path. this optional lookup is performed before the
routeNameToUrl function is called.
-
fixUrl = (function)(url) do anything here. this is called after
the mapping and routeNameToUrl is optionally called. I use this to correct
javascript Ajax request paths by appending the application virtual directory
(Thanks to Ryan for the suggestion to use
the $data on the bindingContext, and then again for the nudge to just use the 4th
parameter.
)