diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 42d44d2acc07c..6c1138d30e40d 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -110,7 +110,7 @@ export class DomEventsPlugin extends EventManagerPlugin { } private patchEvent() { - if (!Event || !Event.prototype) { + if (typeof Event === 'undefined' || !Event || !Event.prototype) { return; } if ((Event.prototype as any)[stopMethodSymbol]) { diff --git a/packages/platform-server/src/server.ts b/packages/platform-server/src/server.ts index 46dbd9707a73c..1b0febf54c81a 100644 --- a/packages/platform-server/src/server.ts +++ b/packages/platform-server/src/server.ts @@ -11,7 +11,7 @@ import {PlatformLocation, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@ang import {HttpClientModule} from '@angular/common/http'; import {Injectable, InjectionToken, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core'; import {HttpModule} from '@angular/http'; -import {BrowserModule, DOCUMENT, ɵSharedStylesHost as SharedStylesHost, ɵTRANSITION_ID, ɵgetDOM as getDOM} from '@angular/platform-browser'; +import {BrowserModule, DOCUMENT, EVENT_MANAGER_PLUGINS, ɵSharedStylesHost as SharedStylesHost, ɵTRANSITION_ID, ɵgetDOM as getDOM} from '@angular/platform-browser'; import {ɵplatformCoreDynamic as platformCoreDynamic} from '@angular/platform-browser-dynamic'; import {NoopAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations'; @@ -19,6 +19,7 @@ import {DominoAdapter, parseDocument} from './domino_adapter'; import {SERVER_HTTP_PROVIDERS} from './http'; import {ServerPlatformLocation} from './location'; import {PlatformState} from './platform_state'; +import {ServerEventManagerPlugin} from './server_events'; import {ServerRendererFactory2} from './server_renderer'; import {ServerStylesHost} from './styles_host'; import {INITIAL_CONFIG, PlatformConfig} from './tokens'; @@ -58,6 +59,7 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [ }, ServerStylesHost, {provide: SharedStylesHost, useExisting: ServerStylesHost}, + {provide: EVENT_MANAGER_PLUGINS, multi: true, useClass: ServerEventManagerPlugin}, ]; /** diff --git a/packages/platform-server/src/server_events.ts b/packages/platform-server/src/server_events.ts new file mode 100644 index 0000000000000..25598633a991f --- /dev/null +++ b/packages/platform-server/src/server_events.ts @@ -0,0 +1,30 @@ +/** + * @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 {Inject, Injectable} from '@angular/core'; +import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/platform-browser'; + +@Injectable() +export class ServerEventManagerPlugin /* extends EventManagerPlugin which is private */ { + constructor(@Inject(DOCUMENT) private doc: any) {} + + // Handle all events on the server. + supports(eventName: string) { return true; } + + addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + return getDOM().onAndCancel(element, eventName, handler); + } + + addGlobalEventListener(element: string, eventName: string, handler: Function): Function { + const target: HTMLElement = getDOM().getGlobalEventTarget(this.doc, element); + if (!target) { + throw new Error(`Unsupported event target ${target} for event ${eventName}`); + } + return this.addEventListener(target, eventName, handler); + } +} diff --git a/packages/platform-server/src/server_renderer.ts b/packages/platform-server/src/server_renderer.ts index 77bcc16ccfcff..67c48be90a761 100644 --- a/packages/platform-server/src/server_renderer.ts +++ b/packages/platform-server/src/server_renderer.ts @@ -8,7 +8,7 @@ import {DomElementSchemaRegistry} from '@angular/compiler'; import {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, RootRenderer, ViewEncapsulation, ɵstringify as stringify} from '@angular/core'; -import {DOCUMENT, ɵNAMESPACE_URIS as NAMESPACE_URIS, ɵSharedStylesHost as SharedStylesHost, ɵflattenStyles as flattenStyles, ɵgetDOM as getDOM, ɵshimContentAttribute as shimContentAttribute, ɵshimHostAttribute as shimHostAttribute} from '@angular/platform-browser'; +import {DOCUMENT, EventManager, ɵNAMESPACE_URIS as NAMESPACE_URIS, ɵSharedStylesHost as SharedStylesHost, ɵflattenStyles as flattenStyles, ɵgetDOM as getDOM, ɵshimContentAttribute as shimContentAttribute, ɵshimHostAttribute as shimHostAttribute} from '@angular/platform-browser'; const EMPTY_ARRAY: any[] = []; @@ -19,9 +19,9 @@ export class ServerRendererFactory2 implements RendererFactory2 { private schema = new DomElementSchemaRegistry(); constructor( - private ngZone: NgZone, @Inject(DOCUMENT) private document: any, - private sharedStylesHost: SharedStylesHost) { - this.defaultRenderer = new DefaultServerRenderer2(document, ngZone, this.schema); + private eventManager: EventManager, private ngZone: NgZone, + @Inject(DOCUMENT) private document: any, private sharedStylesHost: SharedStylesHost) { + this.defaultRenderer = new DefaultServerRenderer2(eventManager, document, ngZone, this.schema); } createRenderer(element: any, type: RendererType2|null): Renderer2 { @@ -34,7 +34,8 @@ export class ServerRendererFactory2 implements RendererFactory2 { let renderer = this.rendererByCompId.get(type.id); if (!renderer) { renderer = new EmulatedEncapsulationServerRenderer2( - this.document, this.ngZone, this.sharedStylesHost, this.schema, type); + this.eventManager, this.document, this.ngZone, this.sharedStylesHost, this.schema, + type); this.rendererByCompId.set(type.id, renderer); } (renderer).applyToHost(element); @@ -61,7 +62,8 @@ class DefaultServerRenderer2 implements Renderer2 { data: {[key: string]: any} = Object.create(null); constructor( - private document: any, private ngZone: NgZone, private schema: DomElementSchemaRegistry) {} + private eventManager: EventManager, protected document: any, private ngZone: NgZone, + private schema: DomElementSchemaRegistry) {} destroy(): void {} @@ -69,10 +71,10 @@ class DefaultServerRenderer2 implements Renderer2 { createElement(name: string, namespace?: string, debugInfo?: any): any { if (namespace) { - return getDOM().createElementNS(NAMESPACE_URIS[namespace], name); + return getDOM().createElementNS(NAMESPACE_URIS[namespace], name, this.document); } - return getDOM().createElement(name); + return getDOM().createElement(name, this.document); } createComment(value: string, debugInfo?: any): any { return getDOM().createComment(value); } @@ -166,14 +168,25 @@ class DefaultServerRenderer2 implements Renderer2 { listen( target: 'document'|'window'|'body'|any, eventName: string, callback: (event: any) => boolean): () => void { - // Note: We are not using the EventsPlugin here as this is not needed - // to run our tests. checkNoSyntheticProp(eventName, 'listener'); - const el = - typeof target === 'string' ? getDOM().getGlobalEventTarget(this.document, target) : target; - const outsideHandler = (event: any) => this.ngZone.runGuarded(() => callback(event)); - return this.ngZone.runOutsideAngular( - () => getDOM().onAndCancel(el, eventName, outsideHandler) as any); + if (typeof target === 'string') { + return <() => void>this.eventManager.addGlobalEventListener( + target, eventName, this.decoratePreventDefault(callback)); + } + return <() => void>this.eventManager.addEventListener( + target, eventName, this.decoratePreventDefault(callback)) as() => void; + } + + private decoratePreventDefault(eventHandler: Function): Function { + return (event: any) => { + // Run the event handler inside the ngZone because event handlers are not patched + // by Zone on the server. This is required only for tests. + const allowDefaultBehavior = this.ngZone.runGuarded(() => eventHandler(event)); + if (allowDefaultBehavior === false) { + event.preventDefault(); + event.returnValue = false; + } + }; } } @@ -190,9 +203,9 @@ class EmulatedEncapsulationServerRenderer2 extends DefaultServerRenderer2 { private hostAttr: string; constructor( - document: any, ngZone: NgZone, sharedStylesHost: SharedStylesHost, + eventManager: EventManager, document: any, ngZone: NgZone, sharedStylesHost: SharedStylesHost, schema: DomElementSchemaRegistry, private component: RendererType2) { - super(document, ngZone, schema); + super(eventManager, document, ngZone, schema); const styles = flattenStyles(component.id, component.styles, []); sharedStylesHost.addStyles(styles); @@ -203,7 +216,7 @@ class EmulatedEncapsulationServerRenderer2 extends DefaultServerRenderer2 { applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); } createElement(parent: any, name: string): Element { - const el = super.createElement(parent, name); + const el = super.createElement(parent, name, this.document); super.setAttribute(el, this.contentAttr, ''); return el; }