feat(platform-server): Implement PlatformLocation for platformServer() (#14405)
This gives server-side apps a current URL including hash, but doesn't implement a state stack, so back-and-forward navigation isn't possible. PR Close #14405
This commit is contained in:
parent
db700dfc71
commit
9e28568a8f
81
modules/@angular/platform-server/src/location.ts
Normal file
81
modules/@angular/platform-server/src/location.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as url from 'url';
|
||||||
|
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common';
|
||||||
|
import {getDOM} from './private_import_platform-browser';
|
||||||
|
import {scheduleMicroTask} from './facade/lang';
|
||||||
|
|
||||||
|
import {Subject} from 'rxjs/Subject';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side implementation of URL state. Implements `pathname`, `search`, and `hash`
|
||||||
|
* but not the state stack.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServerPlatformLocation implements PlatformLocation {
|
||||||
|
private _path: string = '/';
|
||||||
|
private _search: string = '';
|
||||||
|
private _hash: string = '';
|
||||||
|
private _hashUpdate = new Subject<LocationChangeEvent>();
|
||||||
|
|
||||||
|
getBaseHrefFromDOM(): string {
|
||||||
|
return getDOM().getBaseHref();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPopState(fn: LocationChangeListener): void {
|
||||||
|
// No-op: a state stack is not implemented, so
|
||||||
|
// no events will ever come.
|
||||||
|
}
|
||||||
|
|
||||||
|
onHashChange(fn: LocationChangeListener): void {
|
||||||
|
this._hashUpdate.subscribe(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
get pathname(): string { return this._path; }
|
||||||
|
get search(): string { return this._search; }
|
||||||
|
get hash(): string { return this._hash; }
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return `${this.pathname}${this.search}${this.hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setHash(value: string, oldUrl: string) {
|
||||||
|
if (this._hash === value) {
|
||||||
|
// Don't fire events if the hash has not changed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._hash = value;
|
||||||
|
const newUrl = this.url;
|
||||||
|
scheduleMicroTask(() => this._hashUpdate.next(
|
||||||
|
{type: 'hashchange', oldUrl, newUrl} as LocationChangeEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceState(state: any, title: string, newUrl: string): void {
|
||||||
|
const oldUrl = this.url;
|
||||||
|
const parsedUrl = url.parse(newUrl, true);
|
||||||
|
this._path = parsedUrl.path;
|
||||||
|
this._search = parsedUrl.search;
|
||||||
|
this.setHash(parsedUrl.hash, oldUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
pushState(state: any, title: string, newUrl: string): void {
|
||||||
|
this.replaceState(state, title, newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
forward(): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
back(): void {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
@ -547,7 +547,15 @@ export class Parse5DomAdapter extends DomAdapter {
|
|||||||
return this.defaultDoc().body;
|
return this.defaultDoc().body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getBaseHref(): string { throw 'not implemented'; }
|
getBaseHref(): string {
|
||||||
|
const base = this.querySelector(this.defaultDoc(), 'base');
|
||||||
|
let href = '';
|
||||||
|
if (base) {
|
||||||
|
href = this.getHref(base);
|
||||||
|
}
|
||||||
|
// TODO(alxhub): Need relative path logic from BrowserDomAdapter here?
|
||||||
|
return isBlank(href) ? null : href;
|
||||||
|
}
|
||||||
resetBaseElement(): void { throw 'not implemented'; }
|
resetBaseElement(): void { throw 'not implemented'; }
|
||||||
getHistory(): History { throw 'not implemented'; }
|
getHistory(): History { throw 'not implemented'; }
|
||||||
getLocation(): Location { throw 'not implemented'; }
|
getLocation(): Location { throw 'not implemented'; }
|
||||||
|
@ -11,28 +11,17 @@ import {platformCoreDynamic} from '@angular/compiler';
|
|||||||
import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
|
import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import {ServerPlatformLocation} from './location';
|
||||||
import {Parse5DomAdapter} from './parse5_adapter';
|
import {Parse5DomAdapter} from './parse5_adapter';
|
||||||
import {DebugDomRootRenderer} from './private_import_core';
|
import {DebugDomRootRenderer} from './private_import_core';
|
||||||
import {SharedStylesHost} from './private_import_platform-browser';
|
import {DomAdapter, SharedStylesHost} from './private_import_platform-browser';
|
||||||
import {ServerRootRenderer} from './server_renderer';
|
import {ServerRootRenderer} from './server_renderer';
|
||||||
|
|
||||||
|
|
||||||
function notSupported(feature: string): Error {
|
function notSupported(feature: string): Error {
|
||||||
throw new Error(`platform-server does not support '${feature}'.`);
|
throw new Error(`platform-server does not support '${feature}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerPlatformLocation extends PlatformLocation {
|
|
||||||
getBaseHrefFromDOM(): string { throw notSupported('getBaseHrefFromDOM'); };
|
|
||||||
onPopState(fn: any): void { notSupported('onPopState'); };
|
|
||||||
onHashChange(fn: any): void { notSupported('onHashChange'); };
|
|
||||||
get pathname(): string { throw notSupported('pathname'); }
|
|
||||||
get search(): string { throw notSupported('search'); }
|
|
||||||
get hash(): string { throw notSupported('hash'); }
|
|
||||||
replaceState(state: any, title: string, url: string): void { notSupported('replaceState'); };
|
|
||||||
pushState(state: any, title: string, url: string): void { notSupported('pushState'); };
|
|
||||||
forward(): void { notSupported('forward'); };
|
|
||||||
back(): void { notSupported('back'); };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array<any /*Type | Provider | any[]*/> = [
|
export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array<any /*Type | Provider | any[]*/> = [
|
||||||
{provide: PLATFORM_INITIALIZER, useValue: initParse5Adapter, multi: true},
|
{provide: PLATFORM_INITIALIZER, useValue: initParse5Adapter, multi: true},
|
||||||
{provide: PlatformLocation, useClass: ServerPlatformLocation},
|
{provide: PlatformLocation, useClass: ServerPlatformLocation},
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component, NgModule, destroyPlatform} from '@angular/core';
|
import {Component, NgModule, destroyPlatform} from '@angular/core';
|
||||||
|
import {PlatformLocation} from '@angular/common';
|
||||||
import {async} from '@angular/core/testing';
|
import {async} from '@angular/core/testing';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
import {ServerModule, platformDynamicServer} from '@angular/platform-server';
|
import {ServerModule, platformDynamicServer} from '@angular/platform-server';
|
||||||
@ -31,7 +32,7 @@ class ExampleModule {
|
|||||||
export function main() {
|
export function main() {
|
||||||
if (getDOM().supportsDOMEvents()) return; // NODE only
|
if (getDOM().supportsDOMEvents()) return; // NODE only
|
||||||
|
|
||||||
describe('platform-server integration', () => {
|
fdescribe('platform-server integration', () => {
|
||||||
|
|
||||||
beforeEach(() => destroyPlatform());
|
beforeEach(() => destroyPlatform());
|
||||||
afterEach(() => destroyPlatform());
|
afterEach(() => destroyPlatform());
|
||||||
@ -42,5 +43,38 @@ export function main() {
|
|||||||
expect(getDOM().getText(body)).toEqual('Works!');
|
expect(getDOM().getText(body)).toEqual('Works!');
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('PlatformLocation', () => {
|
||||||
|
it('is injectable', () => {
|
||||||
|
const body = writeBody('<app></app>');
|
||||||
|
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => {
|
||||||
|
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||||
|
expect(location.pathname).toBe('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('pushState causes the URL to update', () => {
|
||||||
|
const body = writeBody('<app></app>');
|
||||||
|
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => {
|
||||||
|
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||||
|
location.pushState(null, 'Test', '/foo#bar');
|
||||||
|
expect(location.pathname).toBe('/foo');
|
||||||
|
expect(location.hash).toBe('#bar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('allows subscription to the hash state', done => {
|
||||||
|
const body = writeBody('<app></app>');
|
||||||
|
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => {
|
||||||
|
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||||
|
expect(location.pathname).toBe('/');
|
||||||
|
location.onHashChange((e: any) => {
|
||||||
|
expect(e.type).toBe('hashchange');
|
||||||
|
expect(e.oldUrl).toBe('/');
|
||||||
|
expect(e.newUrl).toBe('/foo#bar');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
location.pushState(null, 'Test', '/foo#bar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user