Last active
September 22, 2016 17:38
-
-
Save cevek/28a2ab1eb79bc3e658793ad3f581ad57 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Resolve<T> = (value?: T | PromiseLike<T> | undefined) => void; | |
type Reject = (reason?: any) => void; | |
type AbortFn = ((onCancel: () => void) => void) | undefined; | |
type Executor<T> = (resolve: Resolve<T>, reject: Reject, onCancel?: AbortFn) => void | |
export class CancellablePromise<T> implements Promise<T> { | |
cancelled = false; | |
children?: CancellablePromise<{}>[] = undefined; | |
promise: Promise<T>; | |
onCancel?: ()=>void; | |
parent: CancellablePromise<{}>; | |
onfulfilled: any; | |
constructor(private executor: Executor<T>) { | |
this.children = []; | |
this.promise = new Promise((resolve, reject) => { | |
let abortFn: AbortFn = undefined; | |
if (executor.length >= 2) { | |
abortFn = onCancel => this.onCancel = onCancel; | |
} | |
executor(data => { | |
if (data instanceof CancellablePromise) { | |
let topPromise = this.findTopPromise(data); | |
this.addChild(topPromise); | |
if (this.cancelled) { | |
topPromise.cancel(); | |
} | |
} | |
resolve(data); | |
}, reject, abortFn); | |
}); | |
} | |
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): CancellablePromise<TResult> { | |
this.onfulfilled = onfulfilled; | |
let resolve: Resolve<TResult>; | |
let reject: Reject; | |
let newPromise = new CancellablePromise((_resolve, _reject) => { | |
resolve = _resolve; | |
reject = _reject; | |
}); | |
this.addChild(newPromise); | |
let _didFulfill = (data: T) => resolve(!this.cancelled ? (onfulfilled ? onfulfilled(data) : data as {} as TResult) : undefined); | |
let _didReject = (err: any) => onrejected ? resolve(!this.cancelled ? onrejected(err) : undefined) : reject(err); | |
this.promise.then(_didFulfill!, _didReject!); | |
return newPromise; | |
} | |
// todo: doesn't work | |
catch<TResult>(onrejected?: (reason: any) => TResult | PromiseLike<TResult>): CancellablePromise<TResult> { | |
return this.then(undefined, onrejected); | |
} | |
protected findTopPromise(promise: CancellablePromise<{}>) { | |
let top = promise; | |
while (top.parent) { | |
top = top.parent; | |
} | |
return top; | |
} | |
protected addChild(promise: CancellablePromise<{}>) { | |
if (!this.children) { | |
this.children = []; | |
} | |
promise.parent = this; | |
this.children.push(promise); | |
} | |
cancelAll() { | |
this.findTopPromise(this).cancel(); | |
} | |
cancel() { | |
if (this.cancelled) { | |
return; | |
} | |
this.cancelled = true; | |
if (this.onCancel) { | |
this.onCancel(); | |
} | |
if (this.children) { | |
for (let i = 0; i < this.children.length; i++) { | |
this.children[i].cancel(); | |
} | |
} | |
} | |
static resolve<T>(value?: T | PromiseLike<T>): CancellablePromise<T> { | |
return new CancellablePromise(resolve => resolve(value as any)); | |
} | |
static reject<T>(reason: T): CancellablePromise<T> { | |
return new CancellablePromise((resolve, reject) => reject(reason)); | |
} | |
static all<TAll>(array: (TAll | PromiseLike<TAll>)[]): CancellablePromise<TAll[]> { | |
return new CancellablePromise((resolve, reject) => { | |
let done = 0; | |
let newArr = new Array(array.length); | |
array.forEach((promise, i) => { | |
let p = promise as PromiseLike<TAll>; | |
if (p && p.then) { | |
done++; | |
p.then(val => { | |
newArr[i] = val; | |
done--; | |
if (done == 0) { | |
resolve(newArr); | |
} | |
}, reject); | |
} else { | |
newArr[i] = promise as TAll; | |
} | |
}); | |
if (!array.length) { | |
resolve(newArr); | |
} | |
}); | |
} | |
static race(array: PromiseLike<{}>[]) { | |
return new CancellablePromise((resolve, reject) => { | |
for (let i = 0; i < array.length; i++) { | |
let promise = array[i]; | |
if (promise && promise.then) { | |
promise.then(resolve, reject); | |
} else { | |
resolve(promise); | |
} | |
} | |
if (!array.length) { | |
resolve(undefined); | |
} | |
}); | |
} | |
static waterfall(values: PromiseLike<{}>[]) { | |
let promise = CancellablePromise.resolve(); | |
for (let i = 0; i < values.length; i++) { | |
const p = values[i]; | |
promise = promise.then(val => p); | |
} | |
} | |
} | |
CancellablePromise.resolve(2).then(null, a => a).then(a => console.log('XXXXXXXXXX', a)); | |
window.CancellablePromise = CancellablePromise; | |
const enum PromiseState{ | |
PENDING, | |
RESOLVED, | |
REJECTED, | |
CANCELLED | |
} | |
type This = {} | undefined; | |
type PDef = P<{} | undefined>; | |
type Callback<R, T> = <R>(val: T | undefined) => R | P<R>; | |
class P<T> { | |
value: T | undefined; | |
state = PromiseState.PENDING; | |
children: PDef[]; | |
callback: Callback<{}, T>; | |
thisArg: This; | |
rejectCallback: Callback<{}, T>; | |
rejectThisArg: This; | |
inner: boolean; | |
resolve(value: T | undefined | PromiseLike<T>) { | |
if (!this.inner) { | |
if (this.state !== PromiseState.PENDING) { | |
return this; | |
} | |
const newValue = this.callback ? (this.thisArg ? this.callback.call(this.thisArg, value) : this.callback(value as T)) : value; | |
if (newValue instanceof P) { | |
this.processResultPromise(newValue); | |
return this; | |
} | |
this.value = newValue; | |
this.state = PromiseState.RESOLVED; | |
} | |
this.runChildren(false); | |
return this; | |
} | |
reject(reason: T | Error) { | |
if (!this.inner) { | |
if (this.state !== PromiseState.PENDING) { | |
return this; | |
} | |
this.value = this.rejectCallback ? (this.rejectThisArg ? this.rejectCallback.call(this.rejectThisArg, reason) : this.rejectCallback(reason as T)) : reason; | |
} | |
this.state = PromiseState.REJECTED; | |
this.runChildren(!this.rejectCallback); | |
return this; | |
} | |
protected processResultPromise(promise: PDef) { | |
const state = promise.state; | |
this.inner = true; | |
this.value = promise.value as T; | |
this.state = promise.state; | |
if (promise.children) { | |
this.children = this.children ? promise.children.concat(this.children) : promise.children.slice(); | |
} | |
this.runChildren(state == PromiseState.REJECTED); | |
} | |
protected runChildren(doReject: boolean) { | |
if (this.children) { | |
if (doReject) { | |
for (let i = 0; i < this.children.length; i++) { | |
const child = this.children[i]; | |
child.reject(this.value); | |
} | |
} else { | |
for (let i = 0; i < this.children.length; i++) { | |
const child = this.children[i]; | |
child.resolve(this.value); | |
} | |
} | |
} | |
} | |
then<TResult>(callback: (val: T) => (TResult | P<TResult>), thisArg?: This): P<TResult> { | |
const p = new P<TResult>(); | |
p.callback = callback as {} as Callback<T, TResult>; | |
p.thisArg = thisArg; | |
if (!this.children) { | |
this.children = []; | |
} | |
this.children.push(p); | |
return p; | |
} | |
catch<TResult>(callback: (val: T) => (TResult | P<TResult>), thisArg?: This): P<TResult> { | |
const p = new P<TResult>(); | |
p.rejectCallback = callback as {} as Callback<T, TResult>; | |
p.rejectThisArg = thisArg; | |
if (!this.children) { | |
this.children = []; | |
} | |
this.children.push(p); | |
return p; | |
} | |
} | |
// window.P = P; | |
function check(val: any, expected: any) { | |
for (let i = 0; i < Math.max(val.length, expected.length); i++) { | |
if (val[i] !== expected[i]) { | |
console.error('Test failed', i, val, expected, val[i], expected[i]); | |
} | |
} | |
} | |
function test1() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [1]); | |
} | |
function test2() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => calls.push(val)); | |
p.reject(1); | |
check(calls, []); | |
} | |
function test3() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.catch(val => 2) | |
.then(val => calls.push(val)); | |
p.reject(1); | |
check(calls, [2]); | |
} | |
function test4() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(() => 2) | |
.catch(val => 3) | |
.then(val => calls.push(val)); | |
p.reject(1); | |
check(calls, [3]); | |
} | |
function test5() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => new P().resolve(val + 2)) | |
.catch(val => 30) | |
.then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [3]); | |
} | |
function test6() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => new P().reject(val + 10)) | |
.catch((val: number) => { | |
calls.push(val); | |
return 7 | |
}) | |
.then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [11, 7]); | |
} | |
function test7() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => calls.push(val)); | |
p.then(val => { | |
calls.push(val + 1); | |
return val + 1 | |
}) | |
.then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [1, 2, 2]); | |
} | |
function test8() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
const pp = p.then(val => { | |
const r = new P<number>().resolve(val + 1); | |
r.catch(a => calls.push(a + 100)); | |
r.then(a => calls.push(a + 1)); | |
return r; | |
}); | |
pp.then(val => calls.push(val + 5)); | |
pp.then(val => { | |
calls.push(val + 2); | |
return val + 1 | |
}) | |
.then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [3, 7, 4, 3]); | |
} | |
function test9() { | |
const calls: number[] = []; | |
const p = new P<number>(); | |
p.then(val => { | |
const r = new P<number>(); | |
const rr = r.then(val => new P<number>().resolve(val + 1)); | |
r.resolve(val + 1); | |
return rr; | |
}).then(val => calls.push(val)); | |
p.resolve(1); | |
check(calls, [3]); | |
} | |
test1(); | |
test2(); | |
test3(); | |
test4(); | |
test5(); | |
test6(); | |
test7(); | |
test8(); | |
test9(); | |
// p.reject(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface AppProps { | |
search: { | |
y: number; | |
} | |
app: { | |
} | |
} | |
class App extends React.Component<AppProps, {}> { | |
static resolve(params: AppProps) { | |
return new CancellablePromise((resolve) => { | |
params.app = {}; | |
setTimeout(() => { | |
console.log("App enter done", params); | |
resolve() | |
}, 100); | |
}) | |
} | |
static leave(nextUrl: Url) { | |
return new CancellablePromise((resolve) => { | |
setTimeout(() => { | |
console.log("App leave", nextUrl); | |
resolve() | |
}, 300); | |
}) | |
} | |
render() { | |
console.log("Render App", this.props); | |
return <div className="foo"> | |
Main | |
<div><ActionButton onClick={()=>indexRoute.goto({})}>Index</ActionButton></div> | |
<div><ActionButton onClick={()=>indexRoute.profile.settings.goto({})}>Settings</ActionButton></div> | |
{this.props.children} | |
</div> | |
} | |
} | |
interface ProfileProps { | |
profile: { | |
} | |
} | |
class Profile extends React.Component<ProfileProps, {}> { | |
static resolve(params: ProfileProps) { | |
return new CancellablePromise((resolve) => { | |
params.profile = {}; | |
setTimeout(() => { | |
console.log("Profile enter done", params); | |
// indexRoute.goto({}, undefined, true); | |
resolve() | |
}, 200); | |
}) | |
} | |
static leave(nextUrl: Url) { | |
return new CancellablePromise((resolve) => { | |
setTimeout(() => { | |
console.log("Profile leave", nextUrl); | |
resolve() | |
}, 200); | |
}) | |
} | |
render() { | |
console.log("Render Profile", this.props); | |
return <div className="profile"> | |
Profile | |
{this.props.children} | |
</div> | |
} | |
} | |
interface ProfileSettingsProps { | |
settings: { | |
foo: number; | |
} | |
} | |
class ProfileSettings extends React.Component<ProfileSettingsProps, {}> { | |
static resolve(params: ProfileSettingsProps) { | |
return new CancellablePromise((resolve) => { | |
params.settings = {foo: 123}; | |
setTimeout(() => { | |
console.log("ProfileSettings enter done", params); | |
resolve() | |
}, 300); | |
}) | |
} | |
static leave(nextUrl: Url) { | |
return new CancellablePromise((resolve) => { | |
setTimeout(() => { | |
console.log("ProfileSettings leave", nextUrl); | |
resolve() | |
}, 1000); | |
}) | |
} | |
render() { | |
console.log("Render ProfileSettings", this.props); | |
return <div className="profile-settings"> | |
Settings | |
{this.props.children} | |
</div> | |
} | |
} | |
interface NewsProps { | |
settings: { | |
foo: number; | |
} | |
} | |
class News extends React.Component<NewsProps, {}> { | |
static resolve(params: NewsProps) { | |
return new CancellablePromise((resolve) => { | |
params.settings = {foo: 123}; | |
setTimeout(() => { | |
console.log("ProfileSettings enter done", params); | |
resolve() | |
}, 300); | |
}) | |
} | |
static leave(nextUrl: Url) { | |
return new CancellablePromise((resolve) => { | |
setTimeout(() => { | |
console.log("news leave", nextUrl); | |
resolve() | |
}, 100); | |
}) | |
} | |
render() { | |
console.log("Render news", this.props); | |
return <div className="news"> | |
News | |
{this.props.children} | |
</div> | |
} | |
} | |
const indexRoute = route('/', App, { | |
index: () => <div>Index</div>, | |
any: () => <div>Index Not Found</div>, | |
profile: route('profile', Profile, { | |
index: () => <div>Index Profile</div>, | |
any: () => <div>Profile Not Found</div>, | |
settings: route('settings', ProfileSettings, { | |
index: () => <div>Settings index</div>, | |
any: () => <div>Settings not found</div> | |
}) | |
}), | |
news: route('news', News, { | |
index: () => <div>News index</div>, | |
any: () => <div>News not found</div> | |
}) | |
}); | |
interface ActionButtonProps { | |
onClick: () => CancellablePromise<{}> | |
} | |
class ActionButton extends React.Component<ActionButtonProps, {}> { | |
disabled = false; | |
onClick = () => { | |
const promise = this.props.onClick(); | |
promise.then(() => { | |
this.disabled = false; | |
this.forceUpdate(); | |
}); | |
this.disabled = true; | |
this.forceUpdate(); | |
}; | |
render() { | |
return <button disabled={this.disabled} onClick={this.onClick}>{this.props.children}</button> | |
} | |
} | |
const urlHistory = new BrowserHistory(); | |
(window as any).start = (initialData?: {}) => { | |
// history.pushState(null, null, '/profile/'); | |
// history.pushState(null, null, '/profile/settings/'); | |
// history.pushState(null, null, '/profile/'); | |
// history.pushState(null, null, '/'); | |
document.onclick = () => { | |
// indexRoute.profile.settings.goto({}, {x: 1}); | |
}; | |
ReactDOM.render(<RouterView history={urlHistory} indexRoute={indexRoute}/>, document.body); | |
// ReactDOM.render(<RouterView initialData={initialData}/>, document.body); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {CancellablePromise} from "./CancellablePromise"; | |
interface ComponentCls<P> { | |
resolve?(params: P): CancellablePromise<{}>; | |
leave?(nextUrl: Url): CancellablePromise<{}>; | |
} | |
type ComponentClass<P> = (React.ComponentClass<P> | React.StatelessComponent<P>) & ComponentCls<P>; | |
type RouteDef = Route<{}>; | |
export interface RouteProps { | |
params: {}; | |
search: {}, | |
url: Url; | |
} | |
interface RouteParams { | |
url: string; | |
component: ComponentClass<{}>; | |
} | |
interface Children { | |
index?: ComponentClass<{}> | |
any?: ComponentClass<{}> | |
} | |
interface PublicRoute { | |
goto(props: {}, search?: {}, replace?: boolean): Promise<{}>; | |
} | |
interface InnerRoute { | |
_route: Route<{}>; | |
} | |
export function route<C extends Children>(url: string, component: ComponentClass<{}>, children = {} as C): C & PublicRoute { | |
const route = new Route({ | |
url: url, | |
component: component, | |
}); | |
(children as {} as InnerRoute)._route = route; | |
if (children) { | |
const keys = Object.keys(children); | |
if (children.index) { | |
route.addChild(new Route({url: '^', component: children.index})); | |
} | |
if (children.any) { | |
route.addChild(new Route({url: '*', component: children.any})); | |
} | |
for (let i = 0; i < keys.length; i++) { | |
const key = keys[i]; | |
const value = children[key] as InnerRoute; | |
if (value._route) { | |
route.addChild(value._route); | |
} | |
} | |
} | |
const ret = children as C & PublicRoute; | |
ret.goto = (props: {}, search?: {}, replace = false) => route.goto(props, search, replace); | |
return ret; | |
} | |
export class Route<Props> { | |
router: Router; | |
parent: RouteDef; | |
children: RouteDef[] = []; | |
regexp: RegExp; | |
names: string[] = []; | |
regexpNames: RegExp[] = []; | |
selfUrl: string; | |
url: string; | |
component: ComponentClass<RouteProps>; | |
onEnter?: (params: RouteProps) => CancellablePromise<{}>; | |
onLeave?: (nextUrl: Url)=>CancellablePromise<{}>; | |
constructor(params: RouteParams) { | |
this.selfUrl = params.url; | |
this.component = params.component as ComponentClass<RouteProps>; | |
this.onEnter = params.component.resolve; | |
this.onLeave = params.component.leave; | |
} | |
init() { | |
this.makeRegexp(); | |
} | |
makeRegexp() { | |
let url = '/' + this.selfUrl.replace(/(^\/+|\/+$)/g, ''); | |
url = url === '/' ? url : url + '/'; | |
if (this.parent) { | |
url = this.parent.url + url.substr(1); | |
} | |
const reg = /:([^\/]+)/g; | |
while (true) { | |
const v = reg.exec(url); | |
if (!v) { | |
break; | |
} | |
this.names.push(v[1]); | |
this.regexpNames.push(new RegExp(':' + v[1] + '(/|$)')); | |
} | |
this.url = url; | |
this.regexp = new RegExp('^' + url.replace(/(:([^\/]+))/g, '([^\/]+)').replace(/\*\//g, '.+').replace(/\^\//g, '') + '?$'); | |
} | |
goto(params: Props, search?: {}, replace = false) { | |
return this.router.changeUrl(this.toUrl(params, search), false, replace); | |
} | |
enter(enterData: RouteProps) { | |
if (this.onEnter) { | |
return this.onEnter(enterData).then(() => enterData); | |
} | |
return CancellablePromise.resolve(enterData); | |
} | |
leave(nextUrl: Url) { | |
if (this.onLeave) { | |
return this.onLeave(nextUrl); | |
} | |
return CancellablePromise.resolve(); | |
} | |
getParams(url: Url) { | |
const m = this.regexp.exec(url.pathName); | |
if (m) { | |
const params = {} as Props; | |
for (let j = 0; j < this.names.length; j++) { | |
params[this.names[j]] = m[j + 1]; | |
} | |
return params; | |
} | |
return {} as Props; | |
} | |
toUrl(params: Props, search?: {}) { | |
let url = this.url; | |
for (let i = 0; i < this.names.length; i++) { | |
const name = this.names[i]; | |
const regexp = this.regexpNames[i]; | |
url = url.replace(regexp, params[name]); | |
} | |
return new Url({url: url, search: search}); | |
} | |
getParents() { | |
let route = this as RouteDef; | |
const parents: RouteDef[] = []; | |
while (route) { | |
parents.unshift(route); | |
route = route.parent; | |
} | |
return parents; | |
} | |
addChild(route: RouteDef) { | |
route.parent = this; | |
this.children.push(route); | |
return route; | |
} | |
check(url: Url) { | |
return this.regexp.test(url.pathName); | |
} | |
} | |
export class Router { | |
history: UrlHistory; | |
activePromise:CancellablePromise<{} | void> = CancellablePromise.resolve(); | |
routeStack: RouteDef[] = []; | |
routeStackEnterData: RouteProps[] = []; | |
registeredRoutes: RouteDef[]; | |
url = new Url({}); | |
activeRoute: RouteDef; | |
onChangeCallbacks: (()=>void)[] = []; | |
publicPromise: Promise<{}> | null; | |
publicPromiseResolve: (()=>void) | null; | |
publicPromiseReject: (()=>void) | null; | |
constructor(route: PublicRoute, urlHistory: UrlHistory) { | |
this.history = urlHistory; | |
this.setRootRoute((route as {} as InnerRoute)._route); | |
console.log(this.registeredRoutes.map(r => r.url)); | |
} | |
addRoute(route: RouteDef) { | |
this.registeredRoutes.push(route); | |
route.init(); | |
route.router = this; | |
for (let i = 0; i < route.children.length; i++) { | |
this.addRoute(route.children[i]); | |
} | |
} | |
setRootRoute(route: RouteDef) { | |
this.registeredRoutes = []; | |
this.addRoute(route); | |
this.registeredRoutes.sort((a, b) => a.url < b.url ? -1 : 1); | |
} | |
changeUrl<T>(url: Url, urlHasChanged: boolean, replace: boolean) { | |
console.log('changeUrl', url, this.url, this.url.href, url.href, this.url.state, url.state); | |
this.activePromise.cancelAll(); | |
if (!this.publicPromise) { | |
this.publicPromise = new Promise((resolve, reject) => { | |
this.publicPromiseResolve = resolve; | |
this.publicPromiseReject = reject; | |
}); | |
} | |
if (this.url.href === url.href && this.url.state === url.state) { | |
console.log("skip"); | |
// restore old url | |
this.history.replace(this.url); | |
this.activePromise = CancellablePromise.resolve().then(()=>this.resolvePublicPromise()); | |
} else { | |
const route = this.findRouteByUrl(url); | |
if (route) { | |
const routeWithParents = route.getParents(); | |
const pos = this.getChangedRoutesPos(routeWithParents); | |
const unMountRoutes = this.routeStack.slice(pos) as RouteDef[]; | |
const toMountRoutes = routeWithParents.slice(pos) as RouteDef[]; | |
console.log({ | |
router: this, | |
routeStack: this.routeStack, | |
unMountRoutes, | |
toMountRoutes, | |
pos, | |
route, | |
routeWithParents, | |
url | |
}); | |
let promise = CancellablePromise.resolve(); | |
// leave | |
promise = unMountRoutes.reverse().reduce((promise, route) => promise.then(() => route.leave(url)), promise); | |
// enter | |
const enterData = {params: route.getParams(url), search: url.search, url}; | |
const stackData: RouteProps[] = []; | |
promise = promise.then(() => enterData); | |
promise = toMountRoutes.reduce((promise, route, i) => | |
promise.then(val => { | |
stackData[i] = val as RouteProps; | |
return route.enter(Object.create(val) as RouteProps); | |
}), promise); | |
// action | |
promise/*.catch(err => {console.error(err)})*/.then(() => { | |
console.log("SDASASFDASFA"); | |
this.routeStack = this.routeStack.slice(0, pos).concat(toMountRoutes); | |
this.routeStackEnterData = stackData; | |
this.url.apply(url); | |
if (!urlHasChanged) { | |
if (replace) { | |
this.history.replace(url); | |
} else { | |
this.history.push(url); | |
} | |
} | |
this.resolvePublicPromise(); | |
this.activeRoute = route; | |
this.callListeners(); | |
}); | |
this.activePromise = promise; | |
} else { | |
this.activePromise = CancellablePromise.resolve(); | |
} | |
} | |
return this.publicPromise; | |
} | |
resolvePublicPromise() { | |
if (this.publicPromiseResolve) { | |
this.publicPromiseResolve(); | |
} | |
this.publicPromise = null; | |
this.publicPromiseResolve = null; | |
this.publicPromiseReject = null; | |
} | |
getChangedRoutesPos(newRoutes: RouteDef[]) { | |
for (let i = 0; i < this.routeStack.length; i++) { | |
const route = this.routeStack[i]; | |
const newRoute = newRoutes[i]; | |
if (route !== newRoute) { | |
return i; | |
} | |
} | |
return this.routeStack.length; | |
} | |
findRouteByUrl(url: Url): RouteDef | undefined { | |
return this.registeredRoutes.filter(route => route.check(url)).pop(); | |
} | |
onPopState = () => { | |
this.changeUrl(this.history.getCurrentUrl(), true, false); | |
}; | |
init() { | |
this.history.addListener(this.onPopState); | |
this.onPopState(); | |
} | |
addListener(onChange: ()=>void) { | |
this.onChangeCallbacks.push(onChange); | |
} | |
removeListener(onChange: ()=>void) { | |
const pos = this.onChangeCallbacks.indexOf(onChange); | |
if (pos > -1) { | |
this.onChangeCallbacks.splice(pos, 1); | |
} | |
} | |
private callListeners() { | |
for (let i = 0; i < this.onChangeCallbacks.length; i++) { | |
const callback = this.onChangeCallbacks[i]; | |
callback(); | |
} | |
} | |
} | |
import * as React from "react"; | |
interface ReactViewProps { | |
history: UrlHistory; | |
indexRoute: PublicRoute; | |
} | |
export class RouterView extends React.Component<ReactViewProps, {}> { | |
router: Router; | |
update = () => { | |
this.forceUpdate(); | |
}; | |
componentWillMount() { | |
this.router = new Router(this.props.indexRoute, this.props.history); | |
this.router.init(); | |
this.router.addListener(this.update); | |
} | |
render() { | |
const routes = this.router.routeStack; | |
let Component: React.ReactElement<{}> | null = null; | |
for (let i = routes.length - 1; i >= 0; i--) { | |
const route = routes[i]; | |
const enterData = this.router.routeStackEnterData[i]; | |
Component = React.createElement(route.component, enterData, Component!) | |
} | |
console.log('render', Component, routes); | |
return Component!; | |
} | |
} | |
abstract class UrlHistory { | |
abstract history: History; | |
abstract getCurrentUrl(): Url; | |
constructor() { | |
this.listen(); | |
} | |
abstract listen(): void; | |
private onChangeCallbacks: (()=>void)[] = []; | |
protected onPopState = (event: PopStateEvent) => { | |
for (let i = 0; i < this.onChangeCallbacks.length; i++) { | |
const callback = this.onChangeCallbacks[i]; | |
callback(); | |
} | |
}; | |
get length() { | |
return this.history.length; | |
} | |
push(url: Url) { | |
this.history.pushState(url.state, undefined, url.href); | |
} | |
replace(url: Url) { | |
this.history.replaceState(url.state, undefined, url.href); | |
} | |
back() { | |
this.history.back(); | |
} | |
forward() { | |
this.history.forward(); | |
} | |
addListener(onChange: ()=>void) { | |
this.onChangeCallbacks.push(onChange); | |
} | |
removeListener(onChange: ()=>void) { | |
const pos = this.onChangeCallbacks.indexOf(onChange); | |
if (pos > -1) { | |
this.onChangeCallbacks.splice(pos, 1); | |
} | |
} | |
} | |
export class BrowserHistory extends UrlHistory { | |
history = window.history; | |
getCurrentUrl() { | |
return new Url({url: window.location.pathname + window.location.search, state: history.state}) | |
} | |
listen() { | |
window.addEventListener('popstate', this.onPopState); | |
} | |
} | |
interface IURL { | |
url?: string; | |
search?: Search; | |
searchParts?: Search; | |
state?: State; | |
} | |
type State = {} | null; | |
type Search = {}; | |
export class Url { | |
pathName: string = ''; | |
state: State; | |
search: Search; | |
href: string = ''; | |
constructor(url: IURL) { | |
this.setParams(url); | |
} | |
private setHref() { | |
const searchParams = this.search; | |
let search = Object.keys(searchParams).filter(k => k && searchParams[k]).map(k => `${k}=${searchParams[k]}`).join('&'); | |
this.href = this.pathName + (search ? ('?' + search) : ''); | |
} | |
private parseHref(url: string) { | |
const m = url.match(/^(.*?)(\?(.*))?$/); | |
if (!m) { | |
throw new Error('Incorrect url: ' + url); | |
} | |
this.pathName = m[1]; | |
if (m[3]) { | |
this.parseSearch(m[3]); | |
} | |
} | |
setParams(url: IURL) { | |
this.state = typeof url.state == 'undefined' ? null : url.state; | |
if (url.url) { | |
this.parseHref(url.url); | |
} | |
if (url.search) { | |
this.search = url.search; | |
} else if (url.searchParts) { | |
const parts = url.searchParts; | |
for (const prop in parts) { | |
const part = parts[prop]; | |
if (part) { | |
this.search[prop] = part; | |
} | |
} | |
} | |
if (!this.search || typeof this.search !== 'object') { | |
this.search = {}; | |
} | |
this.setHref(); | |
return this; | |
} | |
private parseSearch(search: string) { | |
const params: Search = {}; | |
const parts = search.split('&'); | |
for (let i = 0; i < parts.length; i++) { | |
const part = parts[i]; | |
if (part) { | |
const [key, value] = part.split('='); | |
params[key] = value; | |
} | |
} | |
this.search = params; | |
} | |
apply(url: Url) { | |
this.pathName = url.pathName; | |
this.search = url.search; | |
this.state = url.state; | |
this.href = url.href; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment