Migrate from hasher.js to history.js to support HTML5-style navigation. Also clean up the HMR support.

This commit is contained in:
SteveSandersonMS
2016-03-08 15:56:43 +00:00
parent bbdbb449d5
commit 515c659a47
14 changed files with 390 additions and 190 deletions

View File

@@ -2,22 +2,18 @@ import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import './css/site.css'; import './css/site.css';
import * as ko from 'knockout'; import * as ko from 'knockout';
import appLayout from './components/app-layout/app-layout'; import { createHistory } from 'history';
ko.components.register('app-layout', appLayout); // Load and register the <app-root> component
ko.applyBindings(); ko.components.register('app-root', require('./components/app-root/app-root').default);
// Tell Knockout to start up an instance of your application
ko.applyBindings({ history: createHistory() });
// Basic hot reloading support. Automatically reloads and restarts the Knockout app each time // Basic hot reloading support. Automatically reloads and restarts the Knockout app each time
// you modify source files. This will not preserve any application state other than the URL. // you modify source files. This will not preserve any application state other than the URL.
declare var module: any; declare var module: any;
if (module.hot) { if (module.hot) {
module.hot.dispose(() => { module.hot.accept();
ko.cleanNode(document.body); module.hot.dispose(() => ko.cleanNode(document.body));
// TODO: Need a better API for this
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
ko.components.unregister(componentName);
});
});
module.hot.accept();
} }

View File

@@ -1,8 +0,0 @@
import * as ko from 'knockout';
import * as router from '../../router';
class AppLayoutViewModel {
public route = router.instance().currentRoute;
}
export default { viewModel: AppLayoutViewModel, template: require('./app-layout.html') };

View File

@@ -0,0 +1,39 @@
import * as ko from 'knockout';
import { Route, Router } from '../../router';
// Declare the client-side routing configuration
const routes: Route[] = [
{ url: '', params: { page: 'home-page' } },
{ url: 'counter', params: { page: 'counter-example' } },
{ url: 'fetch-data', params: { page: 'fetch-data' } }
];
class AppRootViewModel {
public route: KnockoutObservable<Route>;
private _router: Router;
constructor(params: { history: HistoryModule.History }) {
// Activate the client-side router
this._router = new Router(params.history, routes)
this.route = this._router.currentRoute;
// Load and register all the KO components needed to handle the routes
ko.components.register('nav-menu', require('../nav-menu/nav-menu').default);
ko.components.register('home-page', require('../home-page/home-page').default);
ko.components.register('counter-example', require('../counter-example/counter-example').default);
ko.components.register('fetch-data', require('../fetch-data/fetch-data').default);
}
// To support hot module replacement, this method unregisters the router and KO components.
// In production scenarios where hot module replacement is disabled, this would not be invoked.
public dispose() {
this._router.dispose();
// TODO: Need a better API for this
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
ko.components.unregister(componentName);
});
}
}
export default { viewModel: AppRootViewModel, template: require('./app-root.html') };

View File

@@ -7,23 +7,23 @@
<span class='icon-bar'></span> <span class='icon-bar'></span>
<span class='icon-bar'></span> <span class='icon-bar'></span>
</button> </button>
<a class='navbar-brand' href='#'>WebApplicationBasic</a> <a class='navbar-brand' href='/'>WebApplicationBasic</a>
</div> </div>
<div class='clearfix'></div> <div class='clearfix'></div>
<div class='navbar-collapse collapse'> <div class='navbar-collapse collapse'>
<ul class='nav navbar-nav'> <ul class='nav navbar-nav'>
<li> <li>
<a class='navbar-brand' href='#' data-bind='css: { active: route().page === "home-page" }'> <a class='navbar-brand' href='/' data-bind='css: { active: route().page === "home-page" }'>
<span class='glyphicon glyphicon-home'></span> Home <span class='glyphicon glyphicon-home'></span> Home
</a> </a>
</li> </li>
<li> <li>
<a class='navbar-brand' href='#counter' data-bind='css: { active: route().page === "counter-example" }'> <a class='navbar-brand' href='/counter' data-bind='css: { active: route().page === "counter-example" }'>
<span class='glyphicon glyphicon-education'></span> Counter <span class='glyphicon glyphicon-education'></span> Counter
</a> </a>
</li> </li>
<li> <li>
<a class='navbar-brand' href='#fetch-data' data-bind='css: { active: route().page === "fetch-data" }'> <a class='navbar-brand' href='/fetch-data' data-bind='css: { active: route().page === "fetch-data" }'>
<span class='glyphicon glyphicon-th-list'></span> Fetch data <span class='glyphicon glyphicon-th-list'></span> Fetch data
</a> </a>
</li> </li>

View File

@@ -1,7 +1,5 @@
import * as ko from 'knockout'; import * as ko from 'knockout';
import * as crossroads from 'crossroads'; import * as crossroads from 'crossroads';
import * as hasher from 'hasher';
import { routes } from './routes';
// This module configures crossroads.js, a routing library. If you prefer, you // This module configures crossroads.js, a routing library. If you prefer, you
// can use any other routing library (or none at all) as Knockout is designed to // can use any other routing library (or none at all) as Knockout is designed to
@@ -11,23 +9,41 @@ import { routes } from './routes';
// specifies a 'page', which is a Knockout component) - there's nothing built into // specifies a 'page', which is a Knockout component) - there's nothing built into
// Knockout that requires or even knows about this technique. It's just one of // Knockout that requires or even knows about this technique. It's just one of
// many possible ways of setting up client-side routes. // many possible ways of setting up client-side routes.
export class Router { export class Router {
public currentRoute = ko.observable<Route>({}); public currentRoute = ko.observable<Route>({});
private disposeHistory: () => void;
private clickEventListener: EventListener;
constructor(routes: Route[]) { constructor(history: HistoryModule.History, routes: Route[]) {
// Configure Crossroads route handlers // Reset and configure Crossroads so it matches routes and updates this.currentRoute
crossroads.removeAllRoutes();
crossroads.resetState();
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
routes.forEach(route => { routes.forEach(route => {
crossroads.addRoute(route.url, (requestParams) => { crossroads.addRoute(route.url, (requestParams) => {
this.currentRoute(ko.utils.extend(requestParams, route.params)); this.currentRoute(ko.utils.extend(requestParams, route.params));
}); });
}); });
// Activate Crossroads // Make history.js watch for navigation and notify Crossroads
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT; this.disposeHistory = history.listen(location => crossroads.parse(location.pathname));
hasher.initialized.add(hash => crossroads.parse(hash)); this.clickEventListener = evt => {
hasher.changed.add(hash => crossroads.parse(hash)); let target: any = evt.target;
hasher.init(); if (target && target.tagName === 'A') {
let href = target.getAttribute('href');
if (href && href.charAt(0) == '/') {
history.push(href);
evt.preventDefault();
}
}
};
document.addEventListener('click', this.clickEventListener);
}
public dispose() {
this.disposeHistory();
document.removeEventListener('click', this.clickEventListener);
} }
} }
@@ -35,10 +51,3 @@ export interface Route {
url?: string; url?: string;
params?: any; params?: any;
} }
export function instance() {
// Ensure there's only one instance. This is needed to support hot module replacement.
const windowOrDefault: any = typeof window === 'undefined' ? {} : window;
windowOrDefault._router = windowOrDefault._router || new Router(routes);
return windowOrDefault._router;
}

View File

@@ -1,18 +0,0 @@
import * as ko from 'knockout';
import { Route } from './router';
import navMenu from './components/nav-menu/nav-menu';
import homePage from './components/home-page/home-page';
import counterExample from './components/counter-example/counter-example';
import fetchData from './components/fetch-data/fetch-data';
ko.components.register('nav-menu', navMenu);
ko.components.register('home-page', homePage);
ko.components.register('counter-example', counterExample);
ko.components.register('fetch-data', fetchData);
export const routes: Route[] = [
{ url: '', params: { page: 'home-page' } },
{ url: 'counter', params: { page: 'counter-example' } },
{ url: 'fetch-data', params: { page: 'fetch-data' } }
];

View File

@@ -2,7 +2,7 @@
ViewData["Title"] = "Home Page"; ViewData["Title"] = "Home Page";
} }
<app-layout></app-layout> <app-root params="history: history"></app-root>
@section scripts { @section scripts {
<script src="~/dist/main.js" asp-append-version="true"></script> <script src="~/dist/main.js" asp-append-version="true"></script>

View File

@@ -22,7 +22,7 @@
"crossroads": "^0.12.2", "crossroads": "^0.12.2",
"domain-task": "^1.0.0", "domain-task": "^1.0.0",
"es6-promise": "^3.1.2", "es6-promise": "^3.1.2",
"hasher": "^1.2.0", "history": "^2.0.1",
"knockout": "^3.4.0" "knockout": "^3.4.0"
} }
} }

View File

@@ -8,15 +8,6 @@
"whatwg-fetch/whatwg-fetch.d.ts": { "whatwg-fetch/whatwg-fetch.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
}, },
"crossroads/crossroads.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"js-signals/js-signals.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"hasher/hasher.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"knockout/knockout.d.ts": { "knockout/knockout.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313" "commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
}, },
@@ -25,6 +16,18 @@
}, },
"es6-promise/es6-promise.d.ts": { "es6-promise/es6-promise.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313" "commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"history/history.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"react-router/history.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"crossroads/crossroads.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"js-signals/js-signals.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
} }
} }
} }

View File

@@ -1,116 +0,0 @@
// Type definitions for Hasher.js
// Project: https://github.com/millermedeiros/hasher/
// Definitions by: flyfishMT <https://github.com/flyfishMT/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/// <reference path="../js-signals/js-signals.d.ts" />
declare module HasherJs {
export interface HasherStatic {
// {string} hasher.appendHash
// String that should always be added to the end of Hash value.
appendHash(): string;
// default value: '';
// will be automatically removed from `hasher.getHash()`
// avoid conflicts with elements that contain ID equal to hash value;
// <static> {signals.Signal} hasher.changed
// Signal dispatched when hash value changes. - pass current hash as 1st parameter to listeners and previous hash value as 2nd parameter.
changed: Signal;
// <static> {signals.Signal} hasher.initialized
// Signal dispatched when hasher is initialized. - pass current hash as first parameter to listeners.
initialized: Signal;
// <static> {string} hasher.prependHash
// String that should always be added to the beginning of Hash value.
prependHash: string;
// default value: '/';
// will be automatically removed from `hasher.getHash()`
// avoid conflicts with elements that contain ID equal to hash value;
// <static> {string} hasher.separator
// String used to split hash paths; used by hasher.getHashAsArray() to split paths.
separator: string;
// default value: '/';
// <static> {signals.Signal} hasher.stopped
// Signal dispatched when hasher is stopped. - pass current hash as first parameter to listeners
stopped: Signal;
// <static> <constant> {string} hasher.VERSION
// hasher Version Number
VERSION: string;
// Method Detail
// <static> hasher.dispose()
// Removes all event listeners, stops hasher and destroy hasher object. - IMPORTANT: hasher won't work after calling this method, hasher Object will be deleted.
dispose(): void;
// <static> {string} hasher.getBaseURL()
// Returns:
// {string} Retrieve URL without query string and hash.
getBaseURL(): string;
// <static> {string} hasher.getHash()
// Returns:
// {string} Hash value without '#', `hasher.appendHash` and `hasher.prependHash`.
getHash(): string;
// <static> {Array.} hasher.getHashAsArray()
// Returns:
// {Array.} Hash value split into an Array.
getHashAsArray(): string[];
// <static> {string} hasher.getURL()
// Returns:
// {string} Full URL.
getURL(): string;
// <static> hasher.init()
// Start listening/dispatching changes in the hash/history.
init(): void;
// hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons before calling this method.
// <static> {boolean} hasher.isActive()
// Returns:
// {boolean} If hasher is listening to changes on the browser history and/or hash value.
isActive(): boolean;
// <static> hasher.replaceHash(path)
// Set Hash value without keeping previous hash on the history record. Similar to calling window.location.replace("#/hash") but will also work on IE6-7.
// hasher.replaceHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor'
// Parameters:
// {...string} path
// Hash value without '#'. Hasher will join path segments using `hasher.separator` and prepend/append hash value with `hasher.appendHash` and `hasher.prependHash`
replaceHash(...path: string[]): void;
// <static> hasher.setHash(path)
// Set Hash value, generating a new history record.
// hasher.setHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor'
// Parameters:
// {...string} path
// Hash value without '#'. Hasher will join path segments using `hasher.separator` and prepend/append hash value with `hasher.appendHash` and `hasher.prependHash`
setHash(...path: string[]): void;
// <static> hasher.stop()
// Stop listening/dispatching changes in the hash/history.
// hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons after calling this method, unless you call hasher.init() again.
// hasher will still dispatch changes made programatically by calling hasher.setHash();
stop(): void;
// <static> {string} hasher.toString()
// Returns:
// {string} A string representation of the object.
toString(): string;
}
}
declare var hasher: HasherJs.HasherStatic;
// AMD
declare module 'hasher'{
export = hasher;
}

View File

@@ -0,0 +1,61 @@
// Type definitions for History.js 1.8.0
// Project: https://github.com/browserstate/history.js
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, Gidon Junge <https://github.com/gjunge/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
interface HistoryAdapter {
bind(element: any, event: string, callback: () => void): void;
trigger(element: any, event: string): void;
onDomLoad(callback: () => void): void;
}
// Since History is defined in lib.d.ts as well
// the name for our interfaces was chosen to be Historyjs
// However at runtime you would need to do
// https://github.com/borisyankov/DefinitelyTyped/issues/277
// var Historyjs: Historyjs = <any>History;
interface Historyjs {
enabled: boolean;
pushState(data: any, title: string, url: string): void;
replaceState(data: any, title: string, url: string): void;
getState(): HistoryState;
getStateByIndex(index: number): HistoryState;
getCurrentIndex(): number;
getHash(): string;
Adapter: HistoryAdapter;
back(): void;
forward(): void;
go(x: Number): void;
log(...messages: any[]): void;
debug(...messages: any[]): void;
options: HistoryOptions;
}
interface HistoryState {
data?: any;
title?: string;
url: string;
}
interface HistoryOptions {
hashChangeInterval?: number;
safariPollInterval?: number;
doubleCheckInterval?: number;
disableSuid?: boolean;
storeInterval?: number;
busyDelay?: number;
debug?: boolean;
initialTitle?: string;
html4Mode?: boolean;
delayInit?: number;
}

View File

@@ -0,0 +1,233 @@
// Type definitions for history v2.0.0
// Project: https://github.com/rackt/history
// Definitions by: Sergey Buturlakin <https://github.com/sergey-buturlakin>, Nathan Brown <https://github.com/ngbrown>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
declare namespace HistoryModule {
// types based on https://github.com/rackt/history/blob/master/docs/Terms.md
type Action = string
type BeforeUnloadHook = () => string | boolean
type CreateHistory<T> = (options?: HistoryOptions) => T
type CreateHistoryEnhancer<T, E> = (createHistory: CreateHistory<T>) => CreateHistory<T & E>
interface History {
listenBefore(hook: TransitionHook): () => void
listen(listener: LocationListener): () => void
transitionTo(location: Location): void
push(path: LocationDescriptor): void
replace(path: LocationDescriptor): void
go(n: number): void
goBack(): void
goForward(): void
createKey(): LocationKey
createPath(path: LocationDescriptor): Path
createHref(path: LocationDescriptor): Href
createLocation(path?: LocationDescriptor, action?: Action, key?: LocationKey): Location
/** @deprecated use a location descriptor instead */
createLocation(path?: Path, state?: LocationState, action?: Action, key?: LocationKey): Location
/** @deprecated use location.key to save state instead */
pushState(state: LocationState, path: Path): void
/** @deprecated use location.key to save state instead */
replaceState(state: LocationState, path: Path): void
/** @deprecated use location.key to save state instead */
setState(state: LocationState): void
/** @deprecated use listenBefore instead */
registerTransitionHook(hook: TransitionHook): void
/** @deprecated use the callback returned from listenBefore instead */
unregisterTransitionHook(hook: TransitionHook): void
}
type HistoryOptions = {
getCurrentLocation?: () => Location
finishTransition?: (nextLocation: Location) => boolean
saveState?: (key: LocationKey, state: LocationState) => void
go?: (n: number) => void
getUserConfirmation?: (message: string, callback: (result: boolean) => void) => void
keyLength?: number
queryKey?: string | boolean
stringifyQuery?: (obj: any) => string
parseQueryString?: (str: string) => any
basename?: string
entries?: string | [any]
current?: number
}
type Href = string
type Location = {
pathname: Pathname
search: Search
query: Query
state: LocationState
action: Action
key: LocationKey
basename?: string
}
type LocationDescriptorObject = {
pathname?: Pathname
search?: Search
query?: Query
state?: LocationState
}
type LocationDescriptor = LocationDescriptorObject | Path
type LocationKey = string
type LocationListener = (location: Location) => void
type LocationState = Object
type Path = string // Pathname + QueryString
type Pathname = string
type Query = Object
type QueryString = string
type Search = string
type TransitionHook = (location: Location, callback: (result: any) => void) => any
interface HistoryBeforeUnload {
listenBeforeUnload(hook: BeforeUnloadHook): () => void
}
interface HistoryQueries {
pushState(state: LocationState, pathname: Pathname | Path, query?: Query): void
replaceState(state: LocationState, pathname: Pathname | Path, query?: Query): void
createPath(path: Path, query?: Query): Path
createHref(path: Path, query?: Query): Href
}
// Global usage, without modules, needs the small trick, because lib.d.ts
// already has `history` and `History` global definitions:
// var createHistory = ((window as any).History as HistoryModule.Module).createHistory;
interface Module {
createHistory: CreateHistory<History>
createHashHistory: CreateHistory<History>
createMemoryHistory: CreateHistory<History>
createLocation(path?: Path, state?: LocationState, action?: Action, key?: LocationKey): Location
useBasename<T>(createHistory: CreateHistory<T>): CreateHistory<T>
useBeforeUnload<T>(createHistory: CreateHistory<T>): CreateHistory<T & HistoryBeforeUnload>
useQueries<T>(createHistory: CreateHistory<T>): CreateHistory<T & HistoryQueries>
actions: {
PUSH: string
REPLACE: string
POP: string
}
}
}
declare module "history/lib/createBrowserHistory" {
export default function createBrowserHistory(options?: HistoryModule.HistoryOptions): HistoryModule.History
}
declare module "history/lib/createHashHistory" {
export default function createHashHistory(options?: HistoryModule.HistoryOptions): HistoryModule.History
}
declare module "history/lib/createMemoryHistory" {
export default function createMemoryHistory(options?: HistoryModule.HistoryOptions): HistoryModule.History
}
declare module "history/lib/createLocation" {
export default function createLocation(path?: HistoryModule.Path, state?: HistoryModule.LocationState, action?: HistoryModule.Action, key?: HistoryModule.LocationKey): HistoryModule.Location
}
declare module "history/lib/useBasename" {
export default function useBasename<T>(createHistory: HistoryModule.CreateHistory<T>): HistoryModule.CreateHistory<T>
}
declare module "history/lib/useBeforeUnload" {
export default function useBeforeUnload<T>(createHistory: HistoryModule.CreateHistory<T>): HistoryModule.CreateHistory<T & HistoryModule.HistoryBeforeUnload>
}
declare module "history/lib/useQueries" {
export default function useQueries<T>(createHistory: HistoryModule.CreateHistory<T>): HistoryModule.CreateHistory<T & HistoryModule.HistoryQueries>
}
declare module "history/lib/actions" {
export const PUSH: string
export const REPLACE: string
export const POP: string
export default {
PUSH,
REPLACE,
POP
}
}
declare module "history/lib/DOMUtils" {
export function addEventListener(node: EventTarget, event: string, listener: EventListenerOrEventListenerObject): void;
export function removeEventListener(node: EventTarget, event: string, listener: EventListenerOrEventListenerObject): void;
export function getHashPath(): string;
export function replaceHashPath(path: string): void;
export function getWindowPath(): string;
export function go(n: number): void;
export function getUserConfirmation(message: string, callback: (result: boolean) => void): void;
export function supportsHistory(): boolean;
export function supportsGoWithoutReloadUsingHash(): boolean;
}
declare module "history" {
export { default as createHistory } from "history/lib/createBrowserHistory"
export { default as createHashHistory } from "history/lib/createHashHistory"
export { default as createMemoryHistory } from "history/lib/createMemoryHistory"
export { default as createLocation } from "history/lib/createLocation"
export { default as useBasename } from "history/lib/useBasename"
export { default as useBeforeUnload } from "history/lib/useBeforeUnload"
export { default as useQueries } from "history/lib/useQueries"
import * as Actions from "history/lib/actions"
export { Actions }
}

View File

@@ -1,7 +1,8 @@
/// <reference path="crossroads/crossroads.d.ts" /> /// <reference path="es6-promise/es6-promise.d.ts" />
/// <reference path="hasher/hasher.d.ts" />
/// <reference path="js-signals/js-signals.d.ts" />
/// <reference path="knockout/knockout.d.ts" /> /// <reference path="knockout/knockout.d.ts" />
/// <reference path="requirejs/require.d.ts" /> /// <reference path="requirejs/require.d.ts" />
/// <reference path="whatwg-fetch/whatwg-fetch.d.ts" /> /// <reference path="whatwg-fetch/whatwg-fetch.d.ts" />
/// <reference path="es6-promise/es6-promise.d.ts" /> /// <reference path="history/history.d.ts" />
/// <reference path="react-router/history.d.ts" />
/// <reference path="crossroads/crossroads.d.ts" />
/// <reference path="js-signals/js-signals.d.ts" />