Skip to content

Commit 4277600

Browse files
jasonadenbenlesh
authored andcommittedApr 24, 2019
feat(common): provide replacement for AngularJS $location service (#30055)
This commit provides a replacement for `$location`. The new service is written in Angular, and can be consumed into existing applications by using the downgraded version of the provider. Prior to this addition, applications upgrading from AngularJS to Angular could get into a situation where AngularJS wanted to control the URL, and would often parse or se rialize the URL in a different way than Angular. Additionally, AngularJS was alerted to URL changes only through the `$digest` cycle. This provided a buggy feedback loop from Angular to AngularJS. With this new `LocationUpgradeProvider`, the `$location` methods and events are provided in Angular, and use Angular APIs to make updates to the URL. Additionally, change s to the URL made by other parts of the Angular framework (such as the Router) will be listened for and will cause events to fire in AngularJS, but will no longer attempt to update the URL (since it was already updated by the Angular framework). This centralizes URL reads and writes to Angular and should help provide an easier path to upgrading AngularJS applications to Angular. PR Close #30055
1 parent f185ff3 commit 4277600

19 files changed

+1777
-137
lines changed
 

‎packages/common/src/location/location.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export class Location {
5757
_platformStrategy: LocationStrategy;
5858
/** @internal */
5959
_platformLocation: PlatformLocation;
60-
private urlChangeListeners: any[] = [];
60+
/** @internal */
61+
_urlChangeListeners: ((url: string, state: unknown) => void)[] = [];
6162

6263
constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
6364
this._platformStrategy = platformStrategy;
@@ -147,7 +148,7 @@ export class Location {
147148
*/
148149
go(path: string, query: string = '', state: any = null): void {
149150
this._platformStrategy.pushState(state, '', path, query);
150-
this.notifyUrlChangeListeners(
151+
this._notifyUrlChangeListeners(
151152
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
152153
}
153154

@@ -161,7 +162,7 @@ export class Location {
161162
*/
162163
replaceState(path: string, query: string = '', state: any = null): void {
163164
this._platformStrategy.replaceState(state, '', path, query);
164-
this.notifyUrlChangeListeners(
165+
this._notifyUrlChangeListeners(
165166
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
166167
}
167168

@@ -180,13 +181,13 @@ export class Location {
180181
* framework. These are not detectible through "popstate" or "hashchange" events.
181182
*/
182183
onUrlChange(fn: (url: string, state: unknown) => void) {
183-
this.urlChangeListeners.push(fn);
184-
this.subscribe(v => { this.notifyUrlChangeListeners(v.url, v.state); });
184+
this._urlChangeListeners.push(fn);
185+
this.subscribe(v => { this._notifyUrlChangeListeners(v.url, v.state); });
185186
}
186187

187-
188-
private notifyUrlChangeListeners(url: string = '', state: unknown) {
189-
this.urlChangeListeners.forEach(fn => fn(url, state));
188+
/** @internal */
189+
_notifyUrlChangeListeners(url: string = '', state: unknown) {
190+
this._urlChangeListeners.forEach(fn => fn(url, state));
190191
}
191192

192193
/**

‎packages/common/test/location/location_spec.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common';
10-
import {PathLocationStrategy} from '@angular/common/src/common';
9+
import {CommonModule, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
1110
import {MockPlatformLocation} from '@angular/common/testing';
1211
import {TestBed, inject} from '@angular/core/testing';
1312

@@ -91,23 +90,22 @@ describe('Location Class', () => {
9190
});
9291

9392
it('should have onUrlChange method', inject([Location], (location: Location) => {
94-
expect(typeof location.onUrlChange).toBe('function');
95-
}));
93+
expect(typeof location.onUrlChange).toBe('function');
94+
}));
95+
96+
it('should add registered functions to urlChangeListeners',
97+
inject([Location], (location: Location) => {
9698

97-
it('should add registered functions to urlChangeListeners', inject([Location], (location: Location) => {
99+
function changeListener(url: string, state: unknown) { return undefined; }
98100

99-
function changeListener(url: string, state: unknown) {
100-
return undefined;
101-
}
101+
expect((location as any)._urlChangeListeners.length).toBe(0);
102102

103-
expect((location as any).urlChangeListeners.length).toBe(0);
103+
location.onUrlChange(changeListener);
104104

105-
location.onUrlChange(changeListener);
105+
expect((location as any)._urlChangeListeners.length).toBe(1);
106+
expect((location as any)._urlChangeListeners[0]).toEqual(changeListener);
106107

107-
expect((location as any).urlChangeListeners.length).toBe(1);
108-
expect((location as any).urlChangeListeners[0]).toEqual(changeListener);
109-
110-
}));
108+
}));
111109

112110
});
113111
});

‎packages/common/testing/src/location_mock.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@ import {Location, LocationStrategy, PlatformLocation} from '@angular/common';
1010
import {EventEmitter, Injectable} from '@angular/core';
1111
import {SubscriptionLike} from 'rxjs';
1212

13-
14-
const urlChangeListeners: ((url: string, state: unknown) => void)[] = [];
15-
function notifyUrlChangeListeners(url: string = '', state: unknown) {
16-
urlChangeListeners.forEach(fn => fn(url, state));
17-
}
18-
1913
/**
2014
* A spy for {@link Location} that allows tests to fire simulated location events.
2115
*
2216
* @publicApi
2317
*/
2418
@Injectable()
25-
export class SpyLocation extends Location {
19+
export class SpyLocation implements Location {
2620
urlChanges: string[] = [];
2721
private _history: LocationState[] = [new LocationState('', '', null)];
2822
private _historyIndex: number = 0;
@@ -34,6 +28,8 @@ export class SpyLocation extends Location {
3428
_platformStrategy: LocationStrategy = null !;
3529
/** @internal */
3630
_platformLocation: PlatformLocation = null !;
31+
/** @internal */
32+
_urlChangeListeners: ((url: string, state: unknown) => void)[] = [];
3733

3834
setInitialPath(url: string) { this._history[this._historyIndex].path = url; }
3935

@@ -118,8 +114,13 @@ export class SpyLocation extends Location {
118114
}
119115
}
120116
onUrlChange(fn: (url: string, state: unknown) => void) {
121-
urlChangeListeners.push(fn);
122-
this.subscribe(v => { notifyUrlChangeListeners(v.url, v.state); });
117+
this._urlChangeListeners.push(fn);
118+
this.subscribe(v => { this._notifyUrlChangeListeners(v.url, v.state); });
119+
}
120+
121+
/** @internal */
122+
_notifyUrlChangeListeners(url: string = '', state: unknown) {
123+
this._urlChangeListeners.forEach(fn => fn(url, state));
123124
}
124125

125126
subscribe(

‎packages/common/testing/src/mock_platform_location.ts

+18-21
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Subject} from 'rxjs';
1212

1313
function parseUrl(urlStr: string, baseHref: string) {
1414
const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
15-
let serverBase;
15+
let serverBase: string|undefined;
1616

1717
// URL class requires full URL. If the URL string doesn't start with protocol, we need to add
1818
// an arbitrary base URL which can be removed afterward.
@@ -42,6 +42,8 @@ export const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_L
4242

4343
/**
4444
* Mock implementation of URL state.
45+
*
46+
* @publicApi
4547
*/
4648
@Injectable()
4749
export class MockPlatformLocation implements PlatformLocation {
@@ -87,49 +89,44 @@ export class MockPlatformLocation implements PlatformLocation {
8789

8890
get href(): string {
8991
let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
90-
url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`
92+
url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
9193
return url;
9294
}
9395

9496
get url(): string { return `${this.pathname}${this.search}${this.hash}`; }
9597

96-
private setHash(value: string, oldUrl: string) {
97-
if (this.hash === value) {
98-
// Don't fire events if the hash has not changed.
99-
return;
100-
}
101-
(this as{hash: string}).hash = value;
102-
const newUrl = this.url;
103-
scheduleMicroTask(() => this.hashUpdate.next({
104-
type: 'hashchange', state: null, oldUrl, newUrl
105-
} as LocationChangeEvent));
106-
}
107-
10898
private parseChanges(state: unknown, url: string, baseHref: string = '') {
10999
// When the `history.state` value is stored, it is always copied.
110100
state = JSON.parse(JSON.stringify(state));
111101
return {...parseUrl(url, baseHref), state};
112102
}
113103

114104
replaceState(state: any, title: string, newUrl: string): void {
115-
const oldUrl = this.url;
116-
117105
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
118106

119-
this.urlChanges[0] = {...this.urlChanges[0], pathname, search, state: parsedState};
120-
this.setHash(hash, oldUrl);
107+
this.urlChanges[0] = {...this.urlChanges[0], pathname, search, hash, state: parsedState};
121108
}
122109

123110
pushState(state: any, title: string, newUrl: string): void {
124111
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
125-
this.urlChanges.unshift({...this.urlChanges[0], pathname, search, state: parsedState});
112+
this.urlChanges.unshift({...this.urlChanges[0], pathname, search, hash, state: parsedState});
126113
}
127114

128115
forward(): void { throw new Error('Not implemented'); }
129116

130-
back(): void { this.urlChanges.shift(); }
117+
back(): void {
118+
const oldUrl = this.url;
119+
const oldHash = this.hash;
120+
this.urlChanges.shift();
121+
const newHash = this.hash;
122+
123+
if (oldHash !== newHash) {
124+
scheduleMicroTask(() => this.hashUpdate.next({
125+
type: 'hashchange', state: null, oldUrl, newUrl: this.url
126+
} as LocationChangeEvent));
127+
}
128+
}
131129

132-
// History API isn't available on server, therefore return undefined
133130
getState(): unknown { return this.state; }
134131
}
135132

‎packages/common/upgrade/rollup.config.js

-30
This file was deleted.

‎packages/common/upgrade/src/$location.ts

+694
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {downgradeInjectable} from '@angular/upgrade/static';
10+
import {LocationUpgradeProvider} from './$location';
11+
12+
/**
13+
* Name of AngularJS module under which $location upgrade services are exported.
14+
*
15+
* @publicApi
16+
*/
17+
export const LOCATION_UPGRADE_MODULE = 'LOCATION_UPGRADE_MODULE';
18+
19+
/**
20+
* Downgraded $location provider. API should match AngularJS $location and should be a drop-in
21+
* replacement.
22+
*
23+
* @publicApi
24+
*/
25+
export const $locationProvider = downgradeInjectable(LocationUpgradeProvider);

‎packages/common/upgrade/src/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
export * from './location';
10-
export * from './location_module';
9+
export * from './location_upgrade_module';
10+
export * from './angular_js_module';
11+
export * from './$location';
12+
export * from './params';

‎packages/common/upgrade/src/location.ts

-18
This file was deleted.

‎packages/common/upgrade/src/location_module.ts

-17
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {APP_BASE_HREF, CommonModule, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
10+
import {Inject, InjectionToken, ModuleWithProviders, NgModule, Optional} from '@angular/core';
11+
import {UpgradeModule} from '@angular/upgrade/static';
12+
13+
import {LocationUpgradeProvider, LocationUpgradeService} from './$location';
14+
import {AngularJSUrlCodec, UrlCodec} from './params';
15+
16+
/**
17+
* Configuration options for LocationUpgrade.
18+
*
19+
* @publicApi
20+
*/
21+
export interface LocationUpgradeConfig {
22+
useHash?: boolean;
23+
hashPrefix?: string;
24+
urlCodec?: typeof UrlCodec;
25+
serverBaseHref?: string;
26+
appBaseHref?: string;
27+
}
28+
29+
/**
30+
* Is used in DI to configure the location upgrade package.
31+
*
32+
* @publicApi
33+
*/
34+
export const LOCATION_UPGRADE_CONFIGURATION =
35+
new InjectionToken<LocationUpgradeConfig>('LOCATION_UPGRADE_CONFIGURATION');
36+
37+
const APP_BASE_HREF_RESOLVED = new InjectionToken<string>('APP_BASE_HREF_RESOLVED');
38+
39+
/**
40+
* Module used for configuring Angular's LocationUpgradeService.
41+
*
42+
* @publicApi
43+
*/
44+
@NgModule({imports: [CommonModule]})
45+
export class LocationUpgradeModule {
46+
static config(config?: LocationUpgradeConfig): ModuleWithProviders<LocationUpgradeModule> {
47+
return {
48+
ngModule: LocationUpgradeModule,
49+
providers: [
50+
Location,
51+
{
52+
provide: LocationUpgradeService,
53+
useFactory: provide$location,
54+
deps: [UpgradeModule, Location, PlatformLocation, UrlCodec, LocationStrategy]
55+
},
56+
{provide: LOCATION_UPGRADE_CONFIGURATION, useValue: config ? config : {}},
57+
{provide: UrlCodec, useFactory: provideUrlCodec, deps: [LOCATION_UPGRADE_CONFIGURATION]},
58+
{
59+
provide: APP_BASE_HREF_RESOLVED,
60+
useFactory: provideAppBaseHref,
61+
deps: [LOCATION_UPGRADE_CONFIGURATION, [new Inject(APP_BASE_HREF), new Optional()]]
62+
},
63+
{
64+
provide: LocationStrategy,
65+
useFactory: provideLocationStrategy,
66+
deps: [
67+
PlatformLocation,
68+
APP_BASE_HREF_RESOLVED,
69+
LOCATION_UPGRADE_CONFIGURATION,
70+
]
71+
},
72+
],
73+
};
74+
}
75+
}
76+
77+
/** @internal */
78+
export function provideAppBaseHref(config: LocationUpgradeConfig, appBaseHref?: string) {
79+
if (config && config.appBaseHref != null) {
80+
return config.appBaseHref;
81+
} else if (appBaseHref != null) {
82+
return appBaseHref;
83+
}
84+
return '';
85+
}
86+
87+
/** @internal */
88+
export function provideUrlCodec(config: LocationUpgradeConfig) {
89+
const codec = config && config.urlCodec || AngularJSUrlCodec;
90+
return new (codec as any)();
91+
}
92+
93+
/** @internal */
94+
export function provideLocationStrategy(
95+
platformLocation: PlatformLocation, baseHref: string, options: LocationUpgradeConfig = {}) {
96+
return options.useHash ? new HashLocationStrategy(platformLocation, baseHref) :
97+
new PathLocationStrategy(platformLocation, baseHref);
98+
}
99+
100+
/** @internal */
101+
export function provide$location(
102+
ngUpgrade: UpgradeModule, location: Location, platformLocation: PlatformLocation,
103+
urlCodec: UrlCodec, locationStrategy: LocationStrategy) {
104+
const $locationProvider = new LocationUpgradeProvider(
105+
ngUpgrade, location, platformLocation, urlCodec, locationStrategy);
106+
107+
return $locationProvider.$get();
108+
}

‎packages/common/upgrade/src/params.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export abstract class UrlCodec {
4646
* @publicApi
4747
*/
4848
export class AngularJSUrlCodec implements UrlCodec {
49+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L15
4950
encodePath(path: string): string {
5051
const segments = path.split('/');
5152
let i = segments.length;
@@ -59,6 +60,7 @@ export class AngularJSUrlCodec implements UrlCodec {
5960
return _stripIndexHtml((path && path[0] !== '/' && '/' || '') + path);
6061
}
6162

63+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L42
6264
encodeSearch(search: string|{[k: string]: unknown}): string {
6365
if (typeof search === 'string') {
6466
search = parseKeyValue(search);
@@ -68,11 +70,13 @@ export class AngularJSUrlCodec implements UrlCodec {
6870
return search ? '?' + search : '';
6971
}
7072

73+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L44
7174
encodeHash(hash: string) {
7275
hash = encodeUriSegment(hash);
7376
return hash ? '#' + hash : '';
7477
}
7578

79+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L27
7680
decodePath(path: string, html5Mode = true): string {
7781
const segments = path.split('/');
7882
let i = segments.length;
@@ -88,13 +92,17 @@ export class AngularJSUrlCodec implements UrlCodec {
8892
return segments.join('/');
8993
}
9094

95+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L72
9196
decodeSearch(search: string) { return parseKeyValue(search); }
9297

98+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L73
9399
decodeHash(hash: string) {
94100
hash = decodeURIComponent(hash);
95101
return hash[0] === '#' ? hash.substring(1) : hash;
96102
}
97103

104+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L149
105+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L42
98106
normalize(href: string): string;
99107
normalize(path: string, search: {[k: string]: unknown}, hash: string, baseUrl?: string): string;
100108
normalize(pathOrHref: string, search?: {[k: string]: unknown}, hash?: string, baseUrl?: string):
@@ -128,6 +136,7 @@ export class AngularJSUrlCodec implements UrlCodec {
128136

129137
areEqual(a: string, b: string) { return this.normalize(a) === this.normalize(b); }
130138

139+
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/urlUtils.js#L60
131140
parse(url: string, base?: string) {
132141
try {
133142
const parsed = new URL(url, base);
@@ -170,7 +179,8 @@ function tryDecodeURIComponent(value: string) {
170179

171180

172181
/**
173-
* Parses an escaped url query string into key-value pairs.
182+
* Parses an escaped url query string into key-value pairs. Logic taken from
183+
* https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1382
174184
* @returns {Object.<string,boolean|Array>}
175185
*/
176186
function parseKeyValue(keyValue: string): {[k: string]: unknown} {
@@ -200,6 +210,10 @@ function parseKeyValue(keyValue: string): {[k: string]: unknown} {
200210
return obj;
201211
}
202212

213+
/**
214+
* Serializes into key-value pairs. Logic taken from
215+
* https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1409
216+
*/
203217
function toKeyValue(obj: {[k: string]: unknown}) {
204218
const parts: unknown[] = [];
205219
for (const key in obj) {
@@ -230,6 +244,8 @@ function toKeyValue(obj: {[k: string]: unknown}) {
230244
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
231245
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
232246
* / "*" / "+" / "," / ";" / "="
247+
*
248+
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1437
233249
*/
234250
function encodeUriSegment(val: string) {
235251
return encodeUriQuery(val, true)
@@ -249,6 +265,8 @@ function encodeUriSegment(val: string) {
249265
* pct-encoded = "%" HEXDIG HEXDIG
250266
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
251267
* / "*" / "+" / "," / ";" / "="
268+
*
269+
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1456
252270
*/
253271
function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) {
254272
return encodeURIComponent(val)

‎packages/common/upgrade/src/utils.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export function stripPrefix(val: string, prefix: string): string {
10+
return val.startsWith(prefix) ? val.substring(prefix.length) : val;
11+
}
12+
13+
export function deepEqual(a: any, b: any): boolean {
14+
if (a === b) {
15+
return true;
16+
} else if (!a || !b) {
17+
return false;
18+
} else {
19+
try {
20+
if ((a.prototype !== b.prototype) || (Array.isArray(a) && Array.isArray(b))) {
21+
return false;
22+
}
23+
return JSON.stringify(a) === JSON.stringify(b);
24+
} catch (e) {
25+
return false;
26+
}
27+
}
28+
}
29+
30+
export function isAnchor(el: (Node & ParentNode) | Element | null): el is HTMLAnchorElement {
31+
return (<HTMLAnchorElement>el).href !== undefined;
32+
}
33+
34+
export function isPromise(obj: any): obj is Promise<any> {
35+
// allow any Promise/A+ compliant thenable.
36+
// It's up to the caller to ensure that obj.then conforms to the spec
37+
return !!obj && typeof obj.then === 'function';
38+
}

‎packages/common/upgrade/test/BUILD.bazel

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ ts_library(
66
srcs = glob(["**/*.ts"]),
77
deps = [
88
"//packages/common",
9-
"//packages/common/upgrade",
109
"//packages/common/testing",
10+
"//packages/common/upgrade",
11+
"//packages/core",
1112
"//packages/core/testing",
1213
"//packages/upgrade/static",
1314
],

‎packages/common/upgrade/test/upgrade.spec.ts

+613-11
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {APP_BASE_HREF, CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common';
10+
import {MockPlatformLocation} from '@angular/common/testing';
11+
import {Inject, InjectionToken, ModuleWithProviders, NgModule, Optional} from '@angular/core';
12+
import {UpgradeModule} from '@angular/upgrade/static';
13+
14+
import {LocationUpgradeProvider, LocationUpgradeService} from '../src/$location';
15+
import {LocationUpgradeModule} from '../src/location_upgrade_module';
16+
import {UrlCodec} from '../src/params';
17+
18+
export interface LocationUpgradeTestingConfig {
19+
useHash?: boolean;
20+
hashPrefix?: string;
21+
urlCodec?: typeof UrlCodec;
22+
startUrl?: string;
23+
appBaseHref?: string;
24+
}
25+
26+
/**
27+
* @description
28+
*
29+
* Is used in DI to configure the router.
30+
*
31+
* @publicApi
32+
*/
33+
export const LOC_UPGRADE_TEST_CONFIG =
34+
new InjectionToken<LocationUpgradeTestingConfig>('LOC_UPGRADE_TEST_CONFIG');
35+
36+
37+
export const APP_BASE_HREF_RESOLVED = new InjectionToken<string>('APP_BASE_HREF_RESOLVED');
38+
39+
/**
40+
* Module used for configuring Angular's LocationUpgradeService.
41+
*/
42+
@NgModule({imports: [CommonModule]})
43+
export class LocationUpgradeTestModule {
44+
static config(config?: LocationUpgradeTestingConfig):
45+
ModuleWithProviders<LocationUpgradeTestModule> {
46+
return {
47+
ngModule: LocationUpgradeTestModule,
48+
providers: [
49+
{provide: LOC_UPGRADE_TEST_CONFIG, useValue: config || {}}, {
50+
provide: PlatformLocation,
51+
useFactory: (appBaseHref?: string) => {
52+
if (config && config.appBaseHref != null) {
53+
appBaseHref = config.appBaseHref;
54+
} else if (appBaseHref == null) {
55+
appBaseHref = '';
56+
}
57+
return new MockPlatformLocation(
58+
{startUrl: config && config.startUrl, appBaseHref: appBaseHref});
59+
},
60+
deps: [[new Inject(APP_BASE_HREF), new Optional()]]
61+
},
62+
{
63+
provide: LocationUpgradeService,
64+
useFactory: provide$location,
65+
deps: [
66+
UpgradeModule, Location, PlatformLocation, UrlCodec, LocationStrategy,
67+
LOC_UPGRADE_TEST_CONFIG
68+
]
69+
},
70+
LocationUpgradeModule
71+
.config({
72+
appBaseHref: config && config.appBaseHref,
73+
useHash: config && config.useHash || false
74+
})
75+
.providers !
76+
],
77+
};
78+
}
79+
}
80+
81+
export function provide$location(
82+
ngUpgrade: UpgradeModule, location: Location, platformLocation: PlatformLocation,
83+
urlCodec: UrlCodec, locationStrategy: LocationStrategy, config?: LocationUpgradeTestingConfig) {
84+
const $locationProvider = new LocationUpgradeProvider(
85+
ngUpgrade, location, platformLocation, urlCodec, locationStrategy);
86+
87+
$locationProvider.hashPrefix(config && config.hashPrefix);
88+
$locationProvider.html5Mode(config && !config.useHash);
89+
90+
return $locationProvider.$get();
91+
}

‎tools/public_api_guard/common/common.d.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ export declare class HashLocationStrategy extends LocationStrategy {
116116
back(): void;
117117
forward(): void;
118118
getBaseHref(): string;
119-
getState(): unknown;
120119
onPopState(fn: LocationChangeListener): void;
121120
path(includeHash?: boolean): string;
122121
prepareExternalUrl(internal: string): string;
@@ -167,13 +166,14 @@ export declare class KeyValuePipe implements PipeTransform {
167166
}
168167

169168
export declare class Location {
170-
constructor(platformStrategy: LocationStrategy);
169+
constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation);
171170
back(): void;
172171
forward(): void;
173172
getState(): unknown;
174173
go(path: string, query?: string, state?: any): void;
175174
isCurrentPathEqualTo(path: string, query?: string): boolean;
176175
normalize(url: string): string;
176+
onUrlChange(fn: (url: string, state: unknown) => void): void;
177177
path(includeHash?: boolean): string;
178178
prepareExternalUrl(url: string): string;
179179
replaceState(path: string, query?: string, state?: any): void;
@@ -198,7 +198,6 @@ export declare abstract class LocationStrategy {
198198
abstract back(): void;
199199
abstract forward(): void;
200200
abstract getBaseHref(): string;
201-
abstract getState(): unknown;
202201
abstract onPopState(fn: LocationChangeListener): void;
203202
abstract path(includeHash?: boolean): string;
204203
abstract prepareExternalUrl(internal: string): string;
@@ -362,7 +361,6 @@ export declare class PathLocationStrategy extends LocationStrategy {
362361
back(): void;
363362
forward(): void;
364363
getBaseHref(): string;
365-
getState(): unknown;
366364
onPopState(fn: LocationChangeListener): void;
367365
path(includeHash?: boolean): string;
368366
prepareExternalUrl(internal: string): string;
@@ -378,6 +376,7 @@ export declare class PercentPipe implements PipeTransform {
378376
export declare abstract class PlatformLocation {
379377
abstract readonly hash: string;
380378
abstract readonly hostname: string;
379+
abstract readonly href: string;
381380
abstract readonly pathname: string;
382381
abstract readonly port: string;
383382
abstract readonly protocol: string;

‎tools/public_api_guard/common/testing.d.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,36 @@ export declare class MockLocationStrategy extends LocationStrategy {
1616
simulatePopState(url: string): void;
1717
}
1818

19-
export declare class SpyLocation implements Location {
19+
export declare class MockPlatformLocation implements PlatformLocation {
20+
readonly hash: string;
21+
readonly hostname: string;
22+
readonly href: string;
23+
readonly pathname: string;
24+
readonly port: string;
25+
readonly protocol: string;
26+
readonly search: string;
27+
readonly state: unknown;
28+
readonly url: string;
29+
constructor(config?: MockPlatformLocationConfig);
30+
back(): void;
31+
forward(): void;
32+
getBaseHrefFromDOM(): string;
33+
getState(): unknown;
34+
onHashChange(fn: LocationChangeListener): void;
35+
onPopState(fn: LocationChangeListener): void;
36+
pushState(state: any, title: string, newUrl: string): void;
37+
replaceState(state: any, title: string, newUrl: string): void;
38+
}
39+
40+
export declare class SpyLocation extends Location {
2041
urlChanges: string[];
2142
back(): void;
2243
forward(): void;
2344
getState(): unknown;
2445
go(path: string, query?: string, state?: any): void;
2546
isCurrentPathEqualTo(path: string, query?: string): boolean;
2647
normalize(url: string): string;
48+
onUrlChange(fn: (url: string, state: unknown) => void): void;
2749
path(): string;
2850
prepareExternalUrl(url: string): string;
2951
replaceState(path: string, query?: string, state?: any): void;
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
export declare const $locationProvider: Function;
2+
3+
export declare class AngularJSUrlCodec implements UrlCodec {
4+
areEqual(a: string, b: string): boolean;
5+
decodeHash(hash: string): string;
6+
decodePath(path: string, html5Mode?: boolean): string;
7+
decodeSearch(search: string): {
8+
[k: string]: unknown;
9+
};
10+
encodeHash(hash: string): string;
11+
encodePath(path: string): string;
12+
encodeSearch(search: string | {
13+
[k: string]: unknown;
14+
}): string;
15+
normalize(href: string): string;
16+
normalize(path: string, search: {
17+
[k: string]: unknown;
18+
}, hash: string, baseUrl?: string): string;
19+
parse(url: string, base?: string): {
20+
href: string;
21+
protocol: string;
22+
host: string;
23+
search: string;
24+
hash: string;
25+
hostname: string;
26+
port: string;
27+
pathname: string;
28+
};
29+
}
30+
31+
export declare const LOCATION_UPGRADE_CONFIGURATION: InjectionToken<LocationUpgradeConfig>;
32+
33+
export declare const LOCATION_UPGRADE_MODULE = "LOCATION_UPGRADE_MODULE";
34+
35+
export interface LocationUpgradeConfig {
36+
appBaseHref?: string;
37+
hashPrefix?: string;
38+
serverBaseHref?: string;
39+
urlCodec?: typeof UrlCodec;
40+
useHash?: boolean;
41+
}
42+
43+
export declare class LocationUpgradeModule {
44+
static config(config?: LocationUpgradeConfig): ModuleWithProviders<LocationUpgradeModule>;
45+
}
46+
47+
export declare class LocationUpgradeProvider {
48+
constructor(ngUpgrade: UpgradeModule, location: Location, platformLocation: PlatformLocation, urlCodec: UrlCodec, locationStrategy: LocationStrategy);
49+
$get(): LocationUpgradeService;
50+
hashPrefix(prefix?: string): void;
51+
html5Mode(mode?: any): void;
52+
}
53+
54+
export declare class LocationUpgradeService {
55+
constructor($injector: any, location: Location, platformLocation: PlatformLocation, urlCodec: UrlCodec, locationStrategy: LocationStrategy);
56+
$$parse(url: string): void;
57+
$$parseLinkUrl(url: string, relHref?: string | null): boolean;
58+
absUrl(): string;
59+
hash(hash: string | number | null): this;
60+
hash(): string;
61+
host(): string;
62+
path(): string;
63+
path(path: string | number | null): this;
64+
port(): number | null;
65+
protocol(): string;
66+
replace(): this;
67+
search(): {
68+
[key: string]: unknown;
69+
};
70+
search(search: string | number | {
71+
[key: string]: unknown;
72+
}): this;
73+
search(search: string | number | {
74+
[key: string]: unknown;
75+
}, paramValue: null | undefined | string | number | boolean | string[]): this;
76+
state(state: unknown): this;
77+
state(): unknown;
78+
url(): string;
79+
url(url: string): this;
80+
}
81+
82+
export declare abstract class UrlCodec {
83+
abstract areEqual(a: string, b: string): boolean;
84+
abstract decodeHash(hash: string): string;
85+
abstract decodePath(path: string): string;
86+
abstract decodeSearch(search: string): {
87+
[k: string]: unknown;
88+
};
89+
abstract encodeHash(hash: string): string;
90+
abstract encodePath(path: string): string;
91+
abstract encodeSearch(search: string | {
92+
[k: string]: unknown;
93+
}): string;
94+
abstract normalize(href: string): string;
95+
abstract normalize(path: string, search: {
96+
[k: string]: unknown;
97+
}, hash: string, baseUrl?: string): string;
98+
abstract parse(url: string, base?: string): {
99+
href: string;
100+
protocol: string;
101+
host: string;
102+
search: string;
103+
hash: string;
104+
hostname: string;
105+
port: string;
106+
pathname: string;
107+
};
108+
}

0 commit comments

Comments
 (0)
Please sign in to comment.