Skip to content

Commit 515c659

Browse files
Migrate from hasher.js to history.js to support HTML5-style navigation. Also clean up the HMR support.
1 parent bbdbb44 commit 515c659

File tree

14 files changed

+387
-187
lines changed

14 files changed

+387
-187
lines changed

templates/KnockoutSpa/ClientApp/boot.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@ import 'bootstrap';
22
import 'bootstrap/dist/css/bootstrap.css';
33
import './css/site.css';
44
import * as ko from 'knockout';
5-
import appLayout from './components/app-layout/app-layout';
5+
import { createHistory } from 'history';
66

7-
ko.components.register('app-layout', appLayout);
8-
ko.applyBindings();
7+
// Load and register the <app-root> component
8+
ko.components.register('app-root', require('./components/app-root/app-root').default);
9+
10+
// Tell Knockout to start up an instance of your application
11+
ko.applyBindings({ history: createHistory() });
912

1013
// Basic hot reloading support. Automatically reloads and restarts the Knockout app each time
1114
// you modify source files. This will not preserve any application state other than the URL.
1215
declare var module: any;
1316
if (module.hot) {
14-
module.hot.dispose(() => {
15-
ko.cleanNode(document.body);
16-
17-
// TODO: Need a better API for this
18-
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
19-
ko.components.unregister(componentName);
20-
});
21-
});
22-
module.hot.accept();
17+
module.hot.accept();
18+
module.hot.dispose(() => ko.cleanNode(document.body));
2319
}

templates/KnockoutSpa/ClientApp/components/app-layout/app-layout.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.
File renamed without changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as ko from 'knockout';
2+
import { Route, Router } from '../../router';
3+
4+
// Declare the client-side routing configuration
5+
const routes: Route[] = [
6+
{ url: '', params: { page: 'home-page' } },
7+
{ url: 'counter', params: { page: 'counter-example' } },
8+
{ url: 'fetch-data', params: { page: 'fetch-data' } }
9+
];
10+
11+
class AppRootViewModel {
12+
public route: KnockoutObservable<Route>;
13+
private _router: Router;
14+
15+
constructor(params: { history: HistoryModule.History }) {
16+
// Activate the client-side router
17+
this._router = new Router(params.history, routes)
18+
this.route = this._router.currentRoute;
19+
20+
// Load and register all the KO components needed to handle the routes
21+
ko.components.register('nav-menu', require('../nav-menu/nav-menu').default);
22+
ko.components.register('home-page', require('../home-page/home-page').default);
23+
ko.components.register('counter-example', require('../counter-example/counter-example').default);
24+
ko.components.register('fetch-data', require('../fetch-data/fetch-data').default);
25+
}
26+
27+
// To support hot module replacement, this method unregisters the router and KO components.
28+
// In production scenarios where hot module replacement is disabled, this would not be invoked.
29+
public dispose() {
30+
this._router.dispose();
31+
32+
// TODO: Need a better API for this
33+
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
34+
ko.components.unregister(componentName);
35+
});
36+
}
37+
}
38+
39+
export default { viewModel: AppRootViewModel, template: require('./app-root.html') };

templates/KnockoutSpa/ClientApp/components/nav-menu/nav-menu.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@
77
<span class='icon-bar'></span>
88
<span class='icon-bar'></span>
99
</button>
10-
<a class='navbar-brand' href='#'>WebApplicationBasic</a>
10+
<a class='navbar-brand' href='/'>WebApplicationBasic</a>
1111
</div>
1212
<div class='clearfix'></div>
1313
<div class='navbar-collapse collapse'>
1414
<ul class='nav navbar-nav'>
1515
<li>
16-
<a class='navbar-brand' href='#' data-bind='css: { active: route().page === "home-page" }'>
16+
<a class='navbar-brand' href='/' data-bind='css: { active: route().page === "home-page" }'>
1717
<span class='glyphicon glyphicon-home'></span> Home
1818
</a>
1919
</li>
2020
<li>
21-
<a class='navbar-brand' href='#counter' data-bind='css: { active: route().page === "counter-example" }'>
21+
<a class='navbar-brand' href='/counter' data-bind='css: { active: route().page === "counter-example" }'>
2222
<span class='glyphicon glyphicon-education'></span> Counter
2323
</a>
2424
</li>
2525
<li>
26-
<a class='navbar-brand' href='#fetch-data' data-bind='css: { active: route().page === "fetch-data" }'>
26+
<a class='navbar-brand' href='/fetch-data' data-bind='css: { active: route().page === "fetch-data" }'>
2727
<span class='glyphicon glyphicon-th-list'></span> Fetch data
2828
</a>
2929
</li>

templates/KnockoutSpa/ClientApp/router.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as ko from 'knockout';
22
import * as crossroads from 'crossroads';
3-
import * as hasher from 'hasher';
4-
import { routes } from './routes';
53

64
// This module configures crossroads.js, a routing library. If you prefer, you
75
// can use any other routing library (or none at all) as Knockout is designed to
@@ -11,34 +9,45 @@ import { routes } from './routes';
119
// specifies a 'page', which is a Knockout component) - there's nothing built into
1210
// Knockout that requires or even knows about this technique. It's just one of
1311
// many possible ways of setting up client-side routes.
14-
1512
export class Router {
1613
public currentRoute = ko.observable<Route>({});
14+
private disposeHistory: () => void;
15+
private clickEventListener: EventListener;
1716

18-
constructor(routes: Route[]) {
19-
// Configure Crossroads route handlers
17+
constructor(history: HistoryModule.History, routes: Route[]) {
18+
// Reset and configure Crossroads so it matches routes and updates this.currentRoute
19+
crossroads.removeAllRoutes();
20+
crossroads.resetState();
21+
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
2022
routes.forEach(route => {
2123
crossroads.addRoute(route.url, (requestParams) => {
2224
this.currentRoute(ko.utils.extend(requestParams, route.params));
2325
});
2426
});
2527

26-
// Activate Crossroads
27-
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
28-
hasher.initialized.add(hash => crossroads.parse(hash));
29-
hasher.changed.add(hash => crossroads.parse(hash));
30-
hasher.init();
28+
// Make history.js watch for navigation and notify Crossroads
29+
this.disposeHistory = history.listen(location => crossroads.parse(location.pathname));
30+
this.clickEventListener = evt => {
31+
let target: any = evt.target;
32+
if (target && target.tagName === 'A') {
33+
let href = target.getAttribute('href');
34+
if (href && href.charAt(0) == '/') {
35+
history.push(href);
36+
evt.preventDefault();
37+
}
38+
}
39+
};
40+
41+
document.addEventListener('click', this.clickEventListener);
42+
}
43+
44+
public dispose() {
45+
this.disposeHistory();
46+
document.removeEventListener('click', this.clickEventListener);
3147
}
3248
}
3349

3450
export interface Route {
3551
url?: string;
3652
params?: any;
3753
}
38-
39-
export function instance() {
40-
// Ensure there's only one instance. This is needed to support hot module replacement.
41-
const windowOrDefault: any = typeof window === 'undefined' ? {} : window;
42-
windowOrDefault._router = windowOrDefault._router || new Router(routes);
43-
return windowOrDefault._router;
44-
}

templates/KnockoutSpa/ClientApp/routes.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

templates/KnockoutSpa/Views/Home/Index.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
ViewData["Title"] = "Home Page";
33
}
44

5-
<app-layout></app-layout>
5+
<app-root params="history: history"></app-root>
66

77
@section scripts {
88
<script src="~/dist/main.js" asp-append-version="true"></script>

templates/KnockoutSpa/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"crossroads": "^0.12.2",
2323
"domain-task": "^1.0.0",
2424
"es6-promise": "^3.1.2",
25-
"hasher": "^1.2.0",
25+
"history": "^2.0.1",
2626
"knockout": "^3.4.0"
2727
}
2828
}

templates/KnockoutSpa/tsd.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@
88
"whatwg-fetch/whatwg-fetch.d.ts": {
99
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
1010
},
11-
"crossroads/crossroads.d.ts": {
11+
"knockout/knockout.d.ts": {
1212
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
1313
},
14-
"js-signals/js-signals.d.ts": {
14+
"requirejs/require.d.ts": {
1515
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
1616
},
17-
"hasher/hasher.d.ts": {
17+
"es6-promise/es6-promise.d.ts": {
1818
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
1919
},
20-
"knockout/knockout.d.ts": {
20+
"history/history.d.ts": {
2121
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
2222
},
23-
"requirejs/require.d.ts": {
23+
"react-router/history.d.ts": {
2424
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
2525
},
26-
"es6-promise/es6-promise.d.ts": {
26+
"crossroads/crossroads.d.ts": {
27+
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
28+
},
29+
"js-signals/js-signals.d.ts": {
2730
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
2831
}
2932
}

0 commit comments

Comments
 (0)