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. )