Commit 1d831397 authored by Addy Osmani's avatar Addy Osmani

Merge pull request #218 from AlbertoMonteiro/master

Knockout - routing with crossroad js
parents a4b14fa1 43b78f41
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Knockout.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add"
placeholder="What needs to be done?" autofocus>
</header>
<section id="main" data-bind="visible: todos().length">
<input id="toggle-all" type="checkbox" data-bind="checked: allCompleted">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: todos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view" data-bind="event: { dblclick: $root.editItem }">
<input class="toggle" type="checkbox" data-bind="checked: completed">
<label data-bind="text: title"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }" >
</li>
</ul>
</section>
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<span id="todo-count">
<strong data-bind="text: remainingCount">1</strong>
<span data-bind="text: getLabel( remainingCount )"></span> left
</span>
<button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">Clear completed (<span data-bind="text: completedCount"></span>)</button>
</footer>
</section>
<footer id="info">
<span data-bind="visible: todos().length">Double-click to edit a todo.</span>
<p>Original Knockout version from <a href="https://github.com/ashish01/knockoutjs-todos">Ashish Sharma</a></p>
<p>Rewritten to use Knockout 2.0 and standard template by <a href="http://knockmeout.net">Ryan Niemeyer</a></p>
<p>Patches/fixes for cross-browser compat: <a href="http://twitter.com/addyosmani">Addy Osmani</a></p>
</footer>
<script src="../../assets/base.js"></script>
<script src="js/lib/knockout-2.0.0.js"></script>
<script src="js/app.js"></script>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<title>Knockout.js • TodoMVC</title>
<link href="../../assets/base.css" rel="stylesheet">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" autofocus data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" type="text">
</header>
<section id="main" data-bind="visible: todos().length">
<input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view" data-bind="event: { dblclick: $root.editItem }">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }">
</li>
</ul>
</section>
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<span id="todo-count">
<strong data-bind="text: remainingCount">1</strong>
<span data-bind="text: getLabel( remainingCount )"></span> left
</span>
<ul id="filters">
<li>
<a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">Clear completed (<span data-bind="text: completedCount"></span>)</button>
</footer>
</section>
<footer id="info">
<span data-bind="visible: todos().length">Double-click to edit a todo.</span>
<p>Original Knockout version from <a href="https://github.com/ashish01/knockoutjs-todos">Ashish Sharma</a></p>
<p>Rewritten to use Knockout 2.0 and standard template by <a href="http://knockmeout.net">Ryan Niemeyer</a></p>
<p>Patches/fixes for cross-browser compat: <a href="http://twitter.com/addyosmani">Addy Osmani</a></p>
</footer>
<script src="../../assets/base.js"></script>
<script src="js/lib/knockout-2.0.0.js"></script>
<!--Crossroad.js Begin -->
<script src="js/lib/signals.js"></script>
<script src="js/lib/crossroads.js"></script>
<script src="js/lib/route.js"></script>
<script src="js/lib/pattern_lexer.js"></script>
<script src="js/lib/intro.js"></script>
<!--Crossroad.js End -->
<script src="js/app.js"></script>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
// Crossroads --------
//====================
/**
* @constructor
*/
function Crossroads() {
this._routes = [];
this._prevRoutes = [];
this.bypassed = new signals.Signal();
this.routed = new signals.Signal();
}
Crossroads.prototype = {
greedy : false,
greedyEnabled : true,
normalizeFn : null,
create : function () {
return new Crossroads();
},
shouldTypecast : false,
addRoute : function (pattern, callback, priority) {
var route = new Route(pattern, callback, priority, this);
this._sortedInsert(route);
return route;
},
removeRoute : function (route) {
var i = arrayIndexOf(this._routes, route);
if (i !== -1) {
this._routes.splice(i, 1);
}
route._destroy();
},
removeAllRoutes : function () {
var n = this.getNumRoutes();
while (n--) {
this._routes[n]._destroy();
}
this._routes.length = 0;
},
parse : function (request, defaultArgs) {
request = request || '';
defaultArgs = defaultArgs || [];
var routes = this._getMatchedRoutes(request),
i = 0,
n = routes.length,
cur;
if (n) {
this._notifyPrevRoutes(routes, request);
this._prevRoutes = routes;
//shold be incremental loop, execute routes in order
while (i < n) {
cur = routes[i];
cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params));
cur.isFirst = !i;
this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur]));
i += 1;
}
} else {
this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request]));
}
},
_notifyPrevRoutes : function(matchedRoutes, request) {
var i = 0, prev;
while (prev = this._prevRoutes[i++]) {
//check if switched exist since route may be disposed
if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) {
prev.route.switched.dispatch(request);
}
}
},
_didSwitch : function (route, matchedRoutes){
var matched,
i = 0;
while (matched = matchedRoutes[i++]) {
// only dispatch switched if it is going to a different route
if (matched.route === route) {
return false;
}
}
return true;
},
getNumRoutes : function () {
return this._routes.length;
},
_sortedInsert : function (route) {
//simplified insertion sort
var routes = this._routes,
n = routes.length;
do { --n; } while (routes[n] && route._priority <= routes[n]._priority);
routes.splice(n+1, 0, route);
},
_getMatchedRoutes : function (request) {
var res = [],
routes = this._routes,
n = routes.length,
route;
//should be decrement loop since higher priorities are added at the end of array
while (route = routes[--n]) {
if ((!res.length || this.greedy || route.greedy) && route.match(request)) {
res.push({
route : route,
params : route._getParamsArray(request)
});
}
if (!this.greedyEnabled && res.length) {
break;
}
}
return res;
},
toString : function () {
return '[crossroads numRoutes:'+ this.getNumRoutes() +']';
}
};
//"static" instance
crossroads = new Crossroads();
crossroads.VERSION = '::VERSION_NUMBER::';
crossroads.NORM_AS_ARRAY = function (req, vals) {
return [vals.vals_];
};
crossroads.NORM_AS_OBJECT = function (req, vals) {
return [vals];
};
var crossroads,
UNDEF;
// Helpers -----------
//====================
function arrayIndexOf(arr, val) {
if (arr.indexOf) {
return arr.indexOf(val);
} else {
//Array.indexOf doesn't work on IE 6-7
var n = arr.length;
while (n--) {
if (arr[n] === val) {
return n;
}
}
return -1;
}
}
function isKind(val, kind) {
return '[object '+ kind +']' === Object.prototype.toString.call(val);
}
function isRegExp(val) {
return isKind(val, 'RegExp');
}
function isArray(val) {
return isKind(val, 'Array');
}
function isFunction(val) {
return typeof val === 'function';
}
//borrowed from AMD-utils
function typecastValue(val) {
var r;
if (val === null || val === 'null') {
r = null;
} else if (val === 'true') {
r = true;
} else if (val === 'false') {
r = false;
} else if (val === UNDEF || val === 'undefined') {
r = UNDEF;
} else if (val === '' || isNaN(val)) {
//isNaN('') returns false
r = val;
} else {
//parseFloat(null || '') returns NaN
r = parseFloat(val);
}
return r;
}
function typecastArrayValues(values) {
var n = values.length,
result = [];
while (n--) {
result[n] = typecastValue(values[n]);
}
return result;
}
//borrowed from AMD-Utils
function decodeQueryString(str) {
var queryArr = (str || '').replace('?', '').split('&'),
n = queryArr.length,
obj = {},
item, val;
while (n--) {
item = queryArr[n].split('=');
val = typecastValue(item[1]);
obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val;
}
return obj;
}
// Pattern Lexer ------
//=====================
crossroads.patternLexer = (function () {
var
//match chars that should be escaped on string regexp
ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g,
//trailing slashes (begin/end of string)
LOOSE_SLASHES_REGEXP = /^\/|\/$/g,
LEGACY_SLASHES_REGEXP = /\/$/g,
//params - everything between `{ }` or `: :`
PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g,
//used to save params during compile (avoid escaping things that
//shouldn't be escaped).
TOKENS = {
'OS' : {
//optional slashes
//slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?`
rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,
save : '$1{{id}}$2',
res : '\\/?'
},
'RS' : {
//required slashes
//used to insert slash between `:{` and `}{`
rgx : /([:}])\/?(\{)/g,
save : '$1{{id}}$2',
res : '\\/'
},
'RQ' : {
//required query string - everything in between `{? }`
rgx : /\{\?([^}]+)\}/g,
//everything from `?` till `#` or end of string
res : '\\?([^#]+)'
},
'OQ' : {
//optional query string - everything in between `:? :`
rgx : /:\?([^:]+):/g,
//everything from `?` till `#` or end of string
res : '(?:\\?([^#]*))?'
},
'OR' : {
//optional rest - everything in between `: *:`
rgx : /:([^:]+)\*:/g,
res : '(.*)?' // optional group to avoid passing empty string as captured
},
'RR' : {
//rest param - everything in between `{ *}`
rgx : /\{([^}]+)\*\}/g,
res : '(.+)'
},
// required/optional params should come after rest segments
'RP' : {
//required params - everything between `{ }`
rgx : /\{([^}]+)\}/g,
res : '([^\\/?]+)'
},
'OP' : {
//optional params - everything between `: :`
rgx : /:([^:]+):/g,
res : '([^\\/?]+)?\/?'
}
},
LOOSE_SLASH = 1,
STRICT_SLASH = 2,
LEGACY_SLASH = 3,
_slashMode = LOOSE_SLASH;
function precompileTokens(){
var key, cur;
for (key in TOKENS) {
if (TOKENS.hasOwnProperty(key)) {
cur = TOKENS[key];
cur.id = '__CR_'+ key +'__';
cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id;
cur.rRestore = new RegExp(cur.id, 'g');
}
}
}
precompileTokens();
function captureVals(regex, pattern) {
var vals = [], match;
while (match = regex.exec(pattern)) {
vals.push(match[1]);
}
return vals;
}
function getParamIds(pattern) {
return captureVals(PARAMS_REGEXP, pattern);
}
function getOptionalParamsIds(pattern) {
return captureVals(TOKENS.OP.rgx, pattern);
}
function compilePattern(pattern) {
pattern = pattern || '';
if(pattern){
if (_slashMode === LOOSE_SLASH) {
pattern = pattern.replace(LOOSE_SLASHES_REGEXP, '');
}
else if (_slashMode === LEGACY_SLASH) {
pattern = pattern.replace(LEGACY_SLASHES_REGEXP, '');
}
//save tokens
pattern = replaceTokens(pattern, 'rgx', 'save');
//regexp escape
pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&');
//restore tokens
pattern = replaceTokens(pattern, 'rRestore', 'res');
if (_slashMode === LOOSE_SLASH) {
pattern = '\\/?'+ pattern;
}
}
if (_slashMode !== STRICT_SLASH) {
//single slash is treated as empty and end slash is optional
pattern += '\\/?';
}
return new RegExp('^'+ pattern + '$');
}
function replaceTokens(pattern, regexpName, replaceName) {
var cur, key;
for (key in TOKENS) {
if (TOKENS.hasOwnProperty(key)) {
cur = TOKENS[key];
pattern = pattern.replace(cur[regexpName], cur[replaceName]);
}
}
return pattern;
}
function getParamValues(request, regexp, shouldTypecast) {
var vals = regexp.exec(request);
if (vals) {
vals.shift();
if (shouldTypecast) {
vals = typecastArrayValues(vals);
}
}
return vals;
}
function interpolate(pattern, replacements) {
if (typeof pattern !== 'string') {
throw new Error('Route pattern should be a string.');
}
var replaceFn = function(match, prop){
var val;
if (prop in replacements) {
val = replacements[prop];
if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) {
throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".');
}
}
else if (match.indexOf('{') !== -1) {
throw new Error('The segment '+ match +' is required.');
}
else {
val = '';
}
return val;
};
if (! TOKENS.OS.trail) {
TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$');
}
return pattern
.replace(TOKENS.OS.rgx, TOKENS.OS.save)
.replace(PARAMS_REGEXP, replaceFn)
.replace(TOKENS.OS.trail, '') // remove trailing
.replace(TOKENS.OS.rRestore, '/'); // add slash between segments
}
//API
return {
strict : function(){
_slashMode = STRICT_SLASH;
},
loose : function(){
_slashMode = LOOSE_SLASH;
},
legacy : function(){
_slashMode = LEGACY_SLASH;
},
getParamIds : getParamIds,
getOptionalParamsIds : getOptionalParamsIds,
getParamValues : getParamValues,
compilePattern : compilePattern,
interpolate : interpolate
};
}());
// Route --------------
//=====================
/**
* @constructor
*/
function Route(pattern, callback, priority, router) {
var isRegexPattern = isRegExp(pattern),
patternLexer = crossroads.patternLexer;
this._router = router;
this._pattern = pattern;
this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(this._pattern);
this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(this._pattern);
this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern);
this.matched = new signals.Signal();
this.switched = new signals.Signal();
if (callback) {
this.matched.add(callback);
}
this._priority = priority || 0;
}
Route.prototype = {
greedy : false,
rules : void(0),
match : function (request) {
request = request || '';
return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule.
},
_validateParams : function (request) {
var rules = this.rules,
values = this._getParamsObject(request),
key;
for (key in rules) {
// normalize_ isn't a validation rule... (#39)
if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){
return false;
}
}
return true;
},
_isValidParam : function (request, prop, values) {
var validationRule = this.rules[prop],
val = values[prop],
isValid = false,
isQuery = (prop.indexOf('?') === 0);
if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) {
isValid = true;
}
else if (isRegExp(validationRule)) {
if (isQuery) {
val = values[prop +'_']; //use raw string
}
isValid = validationRule.test(val);
}
else if (isArray(validationRule)) {
if (isQuery) {
val = values[prop +'_']; //use raw string
}
isValid = arrayIndexOf(validationRule, val) !== -1;
}
else if (isFunction(validationRule)) {
isValid = validationRule(val, request, values);
}
return isValid; //fail silently if validationRule is from an unsupported type
},
_getParamsObject : function (request) {
var shouldTypecast = this._router.shouldTypecast,
values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast),
o = {},
n = values.length,
param, val;
while (n--) {
val = values[n];
if (this._paramsIds) {
param = this._paramsIds[n];
if (param.indexOf('?') === 0 && val) {
//make a copy of the original string so array and
//RegExp validation can be applied properly
o[param +'_'] = val;
//update vals_ array as well since it will be used
//during dispatch
val = decodeQueryString(val);
values[n] = val;
}
o[param] = val;
}
//alias to paths and for RegExp pattern
o[n] = val;
}
o.request_ = shouldTypecast? typecastValue(request) : request;
o.vals_ = values;
return o;
},
_getParamsArray : function (request) {
var norm = this.rules? this.rules.normalize_ : null,
params;
norm = norm || this._router.normalizeFn; // default normalize
if (norm && isFunction(norm)) {
params = norm(request, this._getParamsObject(request));
} else {
params = this._getParamsObject(request).vals_;
}
return params;
},
interpolate : function(replacements) {
var str = crossroads.patternLexer.interpolate(this._pattern, replacements);
if (! this._validateParams(str) ) {
throw new Error('Generated string doesn\'t validate against `Route.rules`.');
}
return str;
},
dispose : function () {
this._router.removeRoute(this);
},
_destroy : function () {
this.matched.dispose();
this.switched.dispose();
this.matched = this.switched = this._pattern = this._matchRegexp = null;
},
toString : function () {
return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']';
}
};
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment