Skip to content

Commit f64aace

Browse files
feat(vanilla): Implement in-memory-only location api
test(resolve): Put beforeEach router init inside describe block test(stateService): re-implement url-based tests using in-memory location API
1 parent 0690917 commit f64aace

10 files changed

+264
-187
lines changed

src/vanilla/hashLocation.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import {services, isDefined} from '../common/module';
2-
import {LocationConfig, LocationServices} from '../common/coreservices';
3-
import {splitHash, splitQuery, trimHashVal, getParams} from './utils';
1+
import { isDefined } from '../common/module';
2+
import { LocationConfig, LocationServices } from '../common/coreservices';
3+
import { splitHash, splitQuery, trimHashVal, getParams, locationPluginFactory } from './utils';
4+
import { UIRouter } from '../router';
5+
import { LocationPlugin } from "./interface";
46

57
let hashPrefix: string = '';
68
let baseHref: string = '';
@@ -34,5 +36,11 @@ export const hashLocationService: LocationServices = {
3436
setUrl: (url: string, replace: boolean = true) => {
3537
if (url) location.hash = url;
3638
},
37-
onChange: (cb: EventListener) => window.addEventListener("hashchange", cb, false) as any
39+
onChange: (cb: EventListener) => {
40+
window.addEventListener('hashchange', cb, false);
41+
return () => window.removeEventListener('hashchange', cb);
42+
}
3843
};
44+
45+
export const hashLocationPlugin: (router: UIRouter) => LocationPlugin =
46+
locationPluginFactory('vanilla.hashBangLocation', hashLocationService, hashLocationConfig);

src/vanilla/index.ts

+7-20
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
*/ /** */
66
import { UIRouter } from "../router";
77

8-
import { services, LocationServices, LocationConfig } from "../common/coreservices";
9-
import { LocationPlugin, ServicesPlugin } from "./interface";
10-
import { extend } from "../common/common";
11-
import { hashLocationService, hashLocationConfig } from "./hashLocation";
12-
import { pushStateLocationService, pushStateLocationConfig } from "./pushStateLocation";
8+
import { services } from "../common/coreservices";
9+
import { ServicesPlugin } from "./interface";
1310
import { $q } from "./$q";
1411
import { $injector } from "./$injector";
1512

@@ -18,20 +15,10 @@ export { $q, $injector };
1815
export function servicesPlugin(router: UIRouter): ServicesPlugin {
1916
services.$injector = $injector;
2017
services.$q = $q;
21-
22-
return { name: "vanilla.services", $q, $injector };
23-
}
24-
25-
const locationPluginFactory = (name: string, service: LocationServices, configuration: LocationConfig) =>
26-
(router: UIRouter) => {
27-
extend(services.location, service);
28-
extend(services.locationConfig, configuration);
29-
return { name, service, configuration };
30-
};
3118

32-
export const hashLocationPlugin: (router: UIRouter) => LocationPlugin =
33-
locationPluginFactory("vanilla.hashBangLocation", hashLocationService, hashLocationConfig);
34-
35-
export const pushStateLocationPlugin: (router: UIRouter) => LocationPlugin =
36-
locationPluginFactory("vanilla.pushStateLocation", pushStateLocationService, pushStateLocationConfig);
19+
return { name: "vanilla.services", $q, $injector, dispose: () => null };
20+
}
3721

22+
export * from "./hashLocation";
23+
export * from "./memoryLocation";
24+
export * from "./pushStateLocation";

src/vanilla/memoryLocation.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { services, isDefined } from '../common/module';
2+
import { LocationConfig, LocationServices } from '../common/coreservices';
3+
import { splitQuery, trimHashVal, getParams, splitHash, locationPluginFactory } from './utils';
4+
import { removeFrom, unnestR } from "../common/common";
5+
import { UIRouter } from "../router";
6+
import { LocationPlugin } from "./interface";
7+
import { isArray } from "../common/predicates";
8+
9+
var mlc;
10+
export const memoryLocationConfig: LocationConfig = mlc = {
11+
_hashPrefix: '',
12+
_baseHref: '',
13+
_port: 80,
14+
_protocol: "http",
15+
_host: "localhost",
16+
17+
port: () => mlc._port,
18+
protocol: () => mlc._protocol,
19+
host: () => mlc._host,
20+
baseHref: () => mlc._baseHref,
21+
html5Mode: () => false,
22+
hashPrefix: (newprefix?: string): string => {
23+
if (isDefined(newprefix)) {
24+
mlc._hashPrefix = newprefix;
25+
}
26+
return mlc._hashPrefix;
27+
}
28+
};
29+
30+
var mls;
31+
export const memoryLocationService: LocationServices = mls = {
32+
_listeners: [],
33+
_url: {
34+
path: '',
35+
search: {},
36+
hash: ''
37+
},
38+
_changed: (newval, oldval) => {
39+
if (newval === oldval) return;
40+
let evt = new Event("locationchange");
41+
evt['url'] = newval;
42+
mls._listeners.forEach(cb => cb(evt));
43+
},
44+
45+
url: () => {
46+
let s = mls._url.search;
47+
let hash = mls._url.hash;
48+
let query = Object.keys(s).map(key => (isArray(s[key]) ? s[key] : [s[key]]) .map(val => key + "=" + val))
49+
.reduce(unnestR, [])
50+
.join("&");
51+
52+
return mls._url.path +
53+
(query ? "?" + query : "") +
54+
(hash ? "#" + hash : "");
55+
},
56+
hash: () => mls._url.hash,
57+
path: () => mls._url.path,
58+
search: () => mls._url.search,
59+
setUrl: (url: string, replace: boolean = false) => {
60+
if (isDefined(url)) {
61+
let path = splitHash(splitQuery(url)[0])[0];
62+
let hash = splitHash(url)[1];
63+
let search = getParams(splitQuery(splitHash(url)[0])[1]);
64+
65+
let oldval = mls.url();
66+
mls._url = { path, search, hash };
67+
let newval = mls.url();
68+
mls._changed(newval, oldval);
69+
}
70+
},
71+
onChange: (cb: EventListener) => (mls._listeners.push(cb), () => removeFrom(mls._listeners, cb))
72+
};
73+
74+
export const memoryLocationPlugin: (router: UIRouter) => LocationPlugin =
75+
locationPluginFactory("vanilla.memoryLocation", memoryLocationService, memoryLocationConfig);

src/vanilla/pushStateLocation.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { services, isDefined } from '../common/module';
22
import { LocationConfig, LocationServices } from '../common/coreservices';
3-
import { splitQuery, trimHashVal, getParams } from './utils';
3+
import { splitQuery, trimHashVal, getParams, locationPluginFactory } from './utils';
4+
import { LocationPlugin } from "./interface";
5+
import { UIRouter } from "../router";
46

57
let hashPrefix: string = '';
68
let baseHref: string = '';
@@ -42,5 +44,12 @@ export const pushStateLocationService: LocationServices = {
4244
else history.pushState(null, null, services.locationConfig.baseHref() + url);
4345
}
4446
},
45-
onChange: (cb: EventListener) => window.addEventListener("popstate", cb, false) as any
47+
onChange: (cb: EventListener) => {
48+
window.addEventListener("popstate", cb, false);
49+
return () => window.removeEventListener("popstate", cb);
50+
}
4651
};
52+
53+
export const pushStateLocationPlugin: (router: UIRouter) => LocationPlugin =
54+
locationPluginFactory("vanilla.pushStateLocation", pushStateLocationService, pushStateLocationConfig);
55+

src/vanilla/utils.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import {isArray} from "../common/module";
2+
import { LocationServices, LocationConfig, services } from "../common/coreservices";
3+
import { UIRouter } from "../router";
4+
import { extend, pushTo, removeFrom } from "../common/common";
25

36
const beforeAfterSubstr = (char: string) => (str: string): string[] => {
47
if (!str) return ["", ""];
@@ -24,4 +27,22 @@ export const keyValsToObjectR = (accum, [key, val]) => {
2427
};
2528

2629
export const getParams = (queryString: string): any =>
27-
queryString.split("&").map(splitEqual).reduce(keyValsToObjectR, {});
30+
queryString.split("&").map(splitEqual).reduce(keyValsToObjectR, {});
31+
32+
export function locationPluginFactory(name: string, service: LocationServices, configuration: LocationConfig) {
33+
let deregFns: Function[] = [];
34+
function dispose() {
35+
deregFns.forEach(fn => {
36+
typeof fn === 'function' && fn();
37+
removeFrom(deregFns, fn);
38+
});
39+
}
40+
41+
return function(router: UIRouter) {
42+
extend(services.locationConfig, configuration);
43+
extend(services.location, service);
44+
services.location.onChange = (cb: Function) =>
45+
pushTo(deregFns, service.onChange(cb));
46+
return { name, service, configuration, dispose };
47+
};
48+
}

test/_testUtils.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {pick, forEach, omit} from "../src/index";
2-
import {map} from "../src/common/common";
1+
import { pick, forEach, omit } from "../src/index";
2+
import { map } from "../src/common/common";
33

44
let stateProps = ["resolve", "resolvePolicy", "data", "template", "templateUrl", "url", "name", "params"];
55

@@ -59,4 +59,9 @@ export function PromiseResult(promise?) {
5959
}
6060
}
6161

62-
62+
export const awaitTransition = (router) => new Promise(resolve => {
63+
let dereg = router.transitionService.onSuccess({}, (trans) => {
64+
dereg();
65+
resolve(trans);
66+
});
67+
});

test/_testingPlugin.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { UIRouter } from "../src/router";
2+
import { UIRouterPluginBase } from "../src/interface";
3+
import * as vanilla from "../src/vanilla";
4+
5+
export class TestingPlugin extends UIRouterPluginBase {
6+
name: string = 'testing';
7+
errorsCount: number = 0;
8+
errorsThreshold: number = 1000;
9+
10+
constructor(public router: UIRouter) {
11+
super();
12+
router.plugin(vanilla.servicesPlugin);
13+
router.plugin(vanilla.memoryLocationPlugin);
14+
15+
this.addErrorLoopHandler();
16+
17+
this.startRouter();
18+
}
19+
20+
startRouter() {
21+
this.router.stateRegistry.stateQueue.autoFlush(this.router.stateService);
22+
this.router.urlRouter.listen();
23+
}
24+
25+
addErrorLoopHandler() {
26+
let $transitions = this.router.transitionService;
27+
$transitions.onCreate({}, trans => {
28+
trans.promise.catch(() => this.errorsCount++);
29+
if (this.errorsCount > this.errorsThreshold) {
30+
throw new Error(`Over ${this.errorsThreshold} failures; creation of new transitions disabled`);
31+
}
32+
});
33+
}
34+
}

test/pluginSpec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('plugin api', function () {
2626
class FancyPluginClass implements UIRouterPlugin {
2727
name = "fancypluginclass";
2828
constructor(public router: UIRouter) { }
29+
dispose() {}
2930
}
3031

3132
function FancyPluginConstructor(router: UIRouter, options: any) {

test/resolveSpec.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tree2Array } from "./_testUtils";
55
import { UIRouter } from "../src/router";
66

77
import Spy = jasmine.Spy;
8+
import { TestingPlugin } from "./_testingPlugin";
89

910
///////////////////////////////////////////////
1011

@@ -61,21 +62,6 @@ function getStates() {
6162
};
6263
}
6364

64-
beforeEach(function () {
65-
router = new UIRouter();
66-
router.plugin(vanilla.servicesPlugin);
67-
router.plugin(vanilla.hashLocationPlugin);
68-
router.stateRegistry.stateQueue.autoFlush(router.stateService);
69-
70-
counts = { _J: 0, _J2: 0, _K: 0, _L: 0, _M: 0, _Q: 0 };
71-
vals = { _Q: null };
72-
expectCounts = copy(counts);
73-
74-
tree2Array(getStates(), false).forEach(state => router.stateRegistry.register(state));
75-
statesMap = router.stateRegistry.get()
76-
.reduce((acc, state) => (acc[state.name] = state.$$state(), acc), statesMap);
77-
});
78-
7965
function makePath(names: string[]): PathNode[] {
8066
return names.map(name => new PathNode(statesMap[name]));
8167
}
@@ -86,8 +72,22 @@ function getResolvedData(pathContext: ResolveContext) {
8672
.reduce((acc, resolvable) => { acc[resolvable.token] = resolvable.data; return acc; }, {});
8773
}
8874

89-
9075
describe('Resolvables system:', function () {
76+
77+
afterEach(() => router.dispose());
78+
beforeEach(function () {
79+
router = new UIRouter();
80+
router.plugin(TestingPlugin);
81+
82+
counts = { _J: 0, _J2: 0, _K: 0, _L: 0, _M: 0, _Q: 0 };
83+
vals = { _Q: null };
84+
expectCounts = copy(counts);
85+
86+
tree2Array(getStates(), false).forEach(state => router.stateRegistry.register(state));
87+
statesMap = router.stateRegistry.get()
88+
.reduce((acc, state) => (acc[state.name] = state.$$state(), acc), statesMap);
89+
});
90+
9191
describe('Path.getResolvables', function () {
9292
it('should return Resolvables from the deepest element and all ancestors', () => {
9393
let path = makePath([ "A", "B", "C" ]);

0 commit comments

Comments
 (0)