From 9e28568a8f7936ce165d359c21317fdcc0dd1669 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Thu, 9 Feb 2017 14:10:00 -0800 Subject: [PATCH] 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 --- .../@angular/platform-server/src/location.ts | 81 +++++++++++++++++++ .../platform-server/src/parse5_adapter.ts | 10 ++- .../@angular/platform-server/src/server.ts | 17 +--- .../platform-server/test/integration_spec.ts | 36 ++++++++- 4 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 modules/@angular/platform-server/src/location.ts diff --git a/modules/@angular/platform-server/src/location.ts b/modules/@angular/platform-server/src/location.ts new file mode 100644 index 0000000000..3aa2b81b5f --- /dev/null +++ b/modules/@angular/platform-server/src/location.ts @@ -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(); + + 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'); + } +} diff --git a/modules/@angular/platform-server/src/parse5_adapter.ts b/modules/@angular/platform-server/src/parse5_adapter.ts index 5515459244..473a180c49 100644 --- a/modules/@angular/platform-server/src/parse5_adapter.ts +++ b/modules/@angular/platform-server/src/parse5_adapter.ts @@ -547,7 +547,15 @@ export class Parse5DomAdapter extends DomAdapter { 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'; } getHistory(): History { throw 'not implemented'; } getLocation(): Location { throw 'not implemented'; } diff --git a/modules/@angular/platform-server/src/server.ts b/modules/@angular/platform-server/src/server.ts index 72db3a2512..db302546b6 100644 --- a/modules/@angular/platform-server/src/server.ts +++ b/modules/@angular/platform-server/src/server.ts @@ -11,28 +11,17 @@ import {platformCoreDynamic} from '@angular/compiler'; import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; +import {ServerPlatformLocation} from './location'; import {Parse5DomAdapter} from './parse5_adapter'; 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'; + function notSupported(feature: string): Error { 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 = [ {provide: PLATFORM_INITIALIZER, useValue: initParse5Adapter, multi: true}, {provide: PlatformLocation, useClass: ServerPlatformLocation}, diff --git a/modules/@angular/platform-server/test/integration_spec.ts b/modules/@angular/platform-server/test/integration_spec.ts index ff2ab242dd..9819ca6917 100644 --- a/modules/@angular/platform-server/test/integration_spec.ts +++ b/modules/@angular/platform-server/test/integration_spec.ts @@ -7,6 +7,7 @@ */ import {Component, NgModule, destroyPlatform} from '@angular/core'; +import {PlatformLocation} from '@angular/common'; import {async} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {ServerModule, platformDynamicServer} from '@angular/platform-server'; @@ -31,7 +32,7 @@ class ExampleModule { export function main() { if (getDOM().supportsDOMEvents()) return; // NODE only - describe('platform-server integration', () => { + fdescribe('platform-server integration', () => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); @@ -42,5 +43,38 @@ export function main() { expect(getDOM().getText(body)).toEqual('Works!'); }); })); + + describe('PlatformLocation', () => { + it('is injectable', () => { + const body = writeBody(''); + 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(''); + 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(''); + 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'); + }); + }); + }); }); }