feat(platform-server): add API to render Module and ModuleFactory to string (#14381)

- PlatformState provides an interface to serialize the current Platform State as a string or Document.

- renderModule and renderModuleFactory are convenience methods to wait for Angular Application to stabilize and then render the state to a string.

- refactor code to remove defaultDoc from DomAdapter and inject DOCUMENT where it's needed.
This commit is contained in:
vikerman 2017-02-14 16:14:40 -08:00 committed by Igor Minar
parent 2f2b65bd38
commit b4d444a0a7
39 changed files with 462 additions and 187 deletions

View File

@ -223,7 +223,7 @@ function _extractToken(
if (!Array.isArray(metadata)) { if (!Array.isArray(metadata)) {
if (metadata instanceof Inject) { if (metadata instanceof Inject) {
return _createDependency(metadata.token, optional, null); return _createDependency(metadata['token'], optional, null);
} else { } else {
return _createDependency(metadata, optional, null); return _createDependency(metadata, optional, null);
} }
@ -238,7 +238,7 @@ function _extractToken(
token = paramMetadata; token = paramMetadata;
} else if (paramMetadata instanceof Inject) { } else if (paramMetadata instanceof Inject) {
token = paramMetadata.token; token = paramMetadata['token'];
} else if (paramMetadata instanceof Optional) { } else if (paramMetadata instanceof Optional) {
optional = true; optional = true;

View File

@ -12,6 +12,11 @@ import {el, stringifyElement} from '@angular/platform-browser/testing/browser_ut
export function main() { export function main() {
describe('dom adapter', () => { describe('dom adapter', () => {
let defaultDoc: any;
beforeEach(() => {
defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
});
it('should not coalesque text nodes', () => { it('should not coalesque text nodes', () => {
const el1 = el('<div>a</div>'); const el1 = el('<div>a</div>');
const el2 = el('<div>b</div>'); const el2 = el('<div>b</div>');
@ -68,15 +73,15 @@ export function main() {
beforeEach(() => getDOM().resetBaseElement()); beforeEach(() => getDOM().resetBaseElement());
it('should return null if base element is absent', it('should return null if base element is absent',
() => { expect(getDOM().getBaseHref()).toBeNull(); }); () => { expect(getDOM().getBaseHref(defaultDoc)).toBeNull(); });
it('should return the value of the base element', () => { it('should return the value of the base element', () => {
const baseEl = getDOM().createElement('base'); const baseEl = getDOM().createElement('base');
getDOM().setAttribute(baseEl, 'href', '/drop/bass/connon/'); getDOM().setAttribute(baseEl, 'href', '/drop/bass/connon/');
const headEl = getDOM().defaultDoc().head; const headEl = defaultDoc.head;
getDOM().appendChild(headEl, baseEl); getDOM().appendChild(headEl, baseEl);
const baseHref = getDOM().getBaseHref(); const baseHref = getDOM().getBaseHref(defaultDoc);
getDOM().removeChild(headEl, baseEl); getDOM().removeChild(headEl, baseEl);
getDOM().resetBaseElement(); getDOM().resetBaseElement();
@ -86,10 +91,10 @@ export function main() {
it('should return a relative url', () => { it('should return a relative url', () => {
const baseEl = getDOM().createElement('base'); const baseEl = getDOM().createElement('base');
getDOM().setAttribute(baseEl, 'href', 'base'); getDOM().setAttribute(baseEl, 'href', 'base');
const headEl = getDOM().defaultDoc().head; const headEl = defaultDoc.head;
getDOM().appendChild(headEl, baseEl); getDOM().appendChild(headEl, baseEl);
const baseHref = getDOM().getBaseHref(); const baseHref = getDOM().getBaseHref(defaultDoc);
getDOM().removeChild(headEl, baseEl); getDOM().removeChild(headEl, baseEl);
getDOM().resetBaseElement(); getDOM().resetBaseElement();

View File

@ -21,6 +21,7 @@ import {Attribute, Component, ContentChildren, Directive, HostBinding, HostListe
import {Renderer} from '@angular/core/src/render'; import {Renderer} from '@angular/core/src/render';
import {TestBed, async, fakeAsync, getTestBed, tick} from '@angular/core/testing'; import {TestBed, async, fakeAsync, getTestBed, tick} 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 {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent, el} from '@angular/platform-browser/testing/browser_util'; import {dispatchEvent, el} from '@angular/platform-browser/testing/browser_util';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
@ -838,19 +839,20 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
const template = '<div listener></div>'; const template = '<div listener></div>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
const doc = TestBed.get(DOCUMENT);
const tc = fixture.debugElement.children[0]; const tc = fixture.debugElement.children[0];
const listener = tc.injector.get(DirectiveListeningDomEvent); const listener = tc.injector.get(DirectiveListeningDomEvent);
dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
expect(listener.eventTypes).toEqual(['window_domEvent']); expect(listener.eventTypes).toEqual(['window_domEvent']);
listener.eventTypes = []; listener.eventTypes = [];
dispatchEvent(getDOM().getGlobalEventTarget('document'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent');
expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']); expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']);
fixture.destroy(); fixture.destroy();
listener.eventTypes = []; listener.eventTypes = [];
dispatchEvent(getDOM().getGlobalEventTarget('body'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent');
expect(listener.eventTypes).toEqual([]); expect(listener.eventTypes).toEqual([]);
}); });
@ -990,6 +992,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>'; const template = '<div *ngIf="ctxBoolProp" listener listenerother></div>';
TestBed.overrideComponent(MyComp, {set: {template}}); TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp); const fixture = TestBed.createComponent(MyComp);
const doc = TestBed.get(DOCUMENT);
globalCounter = 0; globalCounter = 0;
fixture.componentInstance.ctxBoolProp = true; fixture.componentInstance.ctxBoolProp = true;
@ -999,7 +1002,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
const listener = tc.injector.get(DirectiveListeningDomEvent); const listener = tc.injector.get(DirectiveListeningDomEvent);
const listenerother = tc.injector.get(DirectiveListeningDomEventOther); const listenerother = tc.injector.get(DirectiveListeningDomEventOther);
dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
expect(listener.eventTypes).toEqual(['window_domEvent']); expect(listener.eventTypes).toEqual(['window_domEvent']);
expect(listenerother.eventType).toEqual('other_domEvent'); expect(listenerother.eventType).toEqual('other_domEvent');
expect(globalCounter).toEqual(1); expect(globalCounter).toEqual(1);
@ -1007,12 +1010,12 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
fixture.componentInstance.ctxBoolProp = false; fixture.componentInstance.ctxBoolProp = false;
fixture.detectChanges(); fixture.detectChanges();
dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
expect(globalCounter).toEqual(1); expect(globalCounter).toEqual(1);
fixture.componentInstance.ctxBoolProp = true; fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges(); fixture.detectChanges();
dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent');
expect(globalCounter).toEqual(2); expect(globalCounter).toEqual(2);
// need to destroy to release all remaining global event listeners // need to destroy to release all remaining global event listeners

View File

@ -30,7 +30,8 @@ import {DomSanitizer, DomSanitizerImpl} from './security/dom_sanitization_servic
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [ export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
{provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true}, {provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true},
{provide: PlatformLocation, useClass: BrowserPlatformLocation} {provide: PlatformLocation, useClass: BrowserPlatformLocation},
{provide: DOCUMENT, useFactory: _document, deps: []},
]; ];
/** /**
@ -59,12 +60,8 @@ export function errorHandler(): ErrorHandler {
return new ErrorHandler(); return new ErrorHandler();
} }
export function meta(): Meta {
return new Meta(getDOM());
}
export function _document(): any { export function _document(): any {
return getDOM().defaultDoc(); return document;
} }
export function _resolveDefaultAnimationDriver(): AnimationDriver { export function _resolveDefaultAnimationDriver(): AnimationDriver {
@ -83,7 +80,6 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
providers: [ providers: [
BROWSER_SANITIZATION_PROVIDERS, BROWSER_SANITIZATION_PROVIDERS,
{provide: ErrorHandler, useFactory: errorHandler, deps: []}, {provide: ErrorHandler, useFactory: errorHandler, deps: []},
{provide: DOCUMENT, useFactory: _document, deps: []},
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
{provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true},
@ -92,11 +88,11 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
{provide: RootRenderer, useExisting: DomRootRenderer}, {provide: RootRenderer, useExisting: DomRootRenderer},
{provide: SharedStylesHost, useExisting: DomSharedStylesHost}, {provide: SharedStylesHost, useExisting: DomSharedStylesHost},
{provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver}, {provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver},
{provide: Meta, useFactory: meta},
DomSharedStylesHost, DomSharedStylesHost,
Testability, Testability,
EventManager, EventManager,
ELEMENT_PROBE_PROVIDERS, ELEMENT_PROBE_PROVIDERS,
Meta,
Title, Title,
], ],
exports: [CommonModule, ApplicationModule] exports: [CommonModule, ApplicationModule]

View File

@ -107,10 +107,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
get attrToPropMap(): any { return _attrToPropMap; } get attrToPropMap(): any { return _attrToPropMap; }
query(selector: string): any { return document.querySelector(selector); } querySelector(el: Element, selector: string): any { return el.querySelector(selector); }
querySelector(el: Element, selector: string): HTMLElement {
return el.querySelector(selector) as HTMLElement;
}
querySelectorAll(el: any, selector: string): any[] { return el.querySelectorAll(selector); } querySelectorAll(el: any, selector: string): any[] { return el.querySelectorAll(selector); }
on(el: Node, evt: any, listener: any) { el.addEventListener(evt, listener, false); } on(el: Node, evt: any, listener: any) { el.addEventListener(evt, listener, false); }
onAndCancel(el: Node, evt: any, listener: any): Function { onAndCancel(el: Node, evt: any, listener: any): Function {
@ -274,7 +271,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
createHtmlDocument(): HTMLDocument { createHtmlDocument(): HTMLDocument {
return document.implementation.createHTMLDocument('fakeTitle'); return document.implementation.createHTMLDocument('fakeTitle');
} }
defaultDoc(): HTMLDocument { return document; }
getBoundingClientRect(el: Element): any { getBoundingClientRect(el: Element): any {
try { try {
return el.getBoundingClientRect(); return el.getBoundingClientRect();
@ -282,8 +278,8 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
return {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0}; return {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0};
} }
} }
getTitle(): string { return document.title; } getTitle(doc: Document): string { return document.title; }
setTitle(newTitle: string) { document.title = newTitle || ''; } setTitle(doc: Document, newTitle: string) { document.title = newTitle || ''; }
elementMatches(n: any, selector: string): boolean { elementMatches(n: any, selector: string): boolean {
if (n instanceof HTMLElement) { if (n instanceof HTMLElement) {
return n.matches && n.matches(selector) || return n.matches && n.matches(selector) ||
@ -330,7 +326,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
return _keyMap[key] || key; return _keyMap[key] || key;
} }
getGlobalEventTarget(target: string): EventTarget { getGlobalEventTarget(doc: Document, target: string): EventTarget {
if (target === 'window') { if (target === 'window') {
return window; return window;
} }
@ -343,7 +339,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
} }
getHistory(): History { return window.history; } getHistory(): History { return window.history; }
getLocation(): Location { return window.location; } getLocation(): Location { return window.location; }
getBaseHref(): string { getBaseHref(doc: Document): string {
const href = getBaseElementHref(); const href = getBaseElementHref();
return isBlank(href) ? null : relativePath(href); return isBlank(href) ? null : relativePath(href);
} }

View File

@ -23,7 +23,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
constructor() { constructor() {
super(); super();
try { try {
const element = this.createElement('div', this.defaultDoc()); const element = this.createElement('div', document);
if (isPresent(this.getStyle(element, 'animationName'))) { if (isPresent(this.getStyle(element, 'animationName'))) {
this._animationPrefix = ''; this._animationPrefix = '';
} else { } else {
@ -61,7 +61,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
} }
supportsDOMEvents(): boolean { return true; } supportsDOMEvents(): boolean { return true; }
supportsNativeShadowDOM(): boolean { supportsNativeShadowDOM(): boolean {
return typeof(<any>this.defaultDoc().body).createShadowRoot === 'function'; return typeof(<any>document.body).createShadowRoot === 'function';
} }
getAnimationPrefix(): string { return this._animationPrefix ? this._animationPrefix : ''; } getAnimationPrefix(): string { return this._animationPrefix ? this._animationPrefix : ''; }
getTransitionEnd(): string { return this._transitionEnd ? this._transitionEnd : ''; } getTransitionEnd(): string { return this._transitionEnd ? this._transitionEnd : ''; }

View File

@ -7,9 +7,10 @@
*/ */
import {LocationChangeListener, PlatformLocation} from '@angular/common'; import {LocationChangeListener, PlatformLocation} from '@angular/common';
import {Injectable} from '@angular/core'; import {Inject, Injectable} from '@angular/core';
import {getDOM} from '../../dom/dom_adapter'; import {getDOM} from '../../dom/dom_adapter';
import {DOCUMENT} from '../../dom/dom_tokens';
import {supportsState} from './history'; import {supportsState} from './history';
@ -25,7 +26,7 @@ export class BrowserPlatformLocation extends PlatformLocation {
private _location: Location; private _location: Location;
private _history: History; private _history: History;
constructor() { constructor(@Inject(DOCUMENT) private _doc: any) {
super(); super();
this._init(); this._init();
} }
@ -39,14 +40,14 @@ export class BrowserPlatformLocation extends PlatformLocation {
get location(): Location { return this._location; } get location(): Location { return this._location; }
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); } getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); }
onPopState(fn: LocationChangeListener): void { onPopState(fn: LocationChangeListener): void {
getDOM().getGlobalEventTarget('window').addEventListener('popstate', fn, false); getDOM().getGlobalEventTarget(this._doc, 'window').addEventListener('popstate', fn, false);
} }
onHashChange(fn: LocationChangeListener): void { onHashChange(fn: LocationChangeListener): void {
getDOM().getGlobalEventTarget('window').addEventListener('hashchange', fn, false); getDOM().getGlobalEventTarget(this._doc, 'window').addEventListener('hashchange', fn, false);
} }
get pathname(): string { return this._location.pathname; } get pathname(): string { return this._location.pathname; }

View File

@ -6,8 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injectable} from '@angular/core'; import {Inject, Injectable} from '@angular/core';
import {DomAdapter} from '../dom/dom_adapter';
import {DomAdapter, getDOM} from '../dom/dom_adapter';
import {DOCUMENT} from '../dom/dom_tokens';
/** /**
* Represents a meta element. * Represents a meta element.
@ -33,7 +36,8 @@ export type MetaDefinition = {
*/ */
@Injectable() @Injectable()
export class Meta { export class Meta {
constructor(private _dom: DomAdapter) {} private _dom: DomAdapter;
constructor(@Inject(DOCUMENT) private _doc: any) { this._dom = getDOM(); }
addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement { addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement {
if (!tag) return null; if (!tag) return null;
@ -52,13 +56,12 @@ export class Meta {
getTag(attrSelector: string): HTMLMetaElement { getTag(attrSelector: string): HTMLMetaElement {
if (!attrSelector) return null; if (!attrSelector) return null;
return this._dom.query(`meta[${attrSelector}]`); return this._dom.querySelector(this._doc, `meta[${attrSelector}]`);
} }
getTags(attrSelector: string): HTMLMetaElement[] { getTags(attrSelector: string): HTMLMetaElement[] {
if (!attrSelector) return []; if (!attrSelector) return [];
const list /*NodeList*/ = const list /*NodeList*/ = this._dom.querySelectorAll(this._doc, `meta[${attrSelector}]`);
this._dom.querySelectorAll(this._dom.defaultDoc(), `meta[${attrSelector}]`);
return list ? [].slice.call(list) : []; return list ? [].slice.call(list) : [];
} }
@ -92,7 +95,7 @@ export class Meta {
} }
const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement; const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement;
this._setMetaElementAttributes(meta, element); this._setMetaElementAttributes(meta, element);
const head = this._dom.getElementsByTagName(this._dom.defaultDoc(), 'head')[0]; const head = this._dom.getElementsByTagName(this._doc, 'head')[0];
this._dom.appendChild(head, element); this._dom.appendChild(head, element);
return element; return element;
} }

View File

@ -6,7 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Inject, Injectable} from '@angular/core';
import {getDOM} from '../dom/dom_adapter'; import {getDOM} from '../dom/dom_adapter';
import {DOCUMENT} from '../dom/dom_tokens';
/** /**
* A service that can be used to get and set the title of a current HTML document. * A service that can be used to get and set the title of a current HTML document.
* *
@ -17,16 +22,18 @@ import {getDOM} from '../dom/dom_adapter';
* *
* @experimental * @experimental
*/ */
@Injectable()
export class Title { export class Title {
constructor(@Inject(DOCUMENT) private _doc: any) {}
/** /**
* Get the title of the current HTML document. * Get the title of the current HTML document.
* @returns {string} * @returns {string}
*/ */
getTitle(): string { return getDOM().getTitle(); } getTitle(): string { return getDOM().getTitle(this._doc); }
/** /**
* Set the title of the current HTML document. * Set the title of the current HTML document.
* @param newTitle * @param newTitle
*/ */
setTitle(newTitle: string) { getDOM().setTitle(newTitle); } setTitle(newTitle: string) { getDOM().setTitle(this._doc, newTitle); }
} }

View File

@ -53,8 +53,7 @@ export abstract class DomAdapter {
_attrToPropMap: {[key: string]: string}; _attrToPropMap: {[key: string]: string};
abstract parse(templateHtml: string): any /** TODO #9100 */; abstract parse(templateHtml: string): any /** TODO #9100 */;
abstract query(selector: string): any; abstract querySelector(el: any /** TODO #9100 */, selector: string): any;
abstract querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement;
abstract querySelectorAll(el: any /** TODO #9100 */, selector: string): any[]; abstract querySelectorAll(el: any /** TODO #9100 */, selector: string): any[];
abstract on( abstract on(
el: any /** TODO #9100 */, evt: any /** TODO #9100 */, listener: any /** TODO #9100 */): any el: any /** TODO #9100 */, evt: any /** TODO #9100 */, listener: any /** TODO #9100 */): any
@ -145,10 +144,9 @@ export abstract class DomAdapter {
/** TODO #9100 */; /** TODO #9100 */;
abstract templateAwareRoot(el: any /** TODO #9100 */): any /** TODO #9100 */; abstract templateAwareRoot(el: any /** TODO #9100 */): any /** TODO #9100 */;
abstract createHtmlDocument(): HTMLDocument; abstract createHtmlDocument(): HTMLDocument;
abstract defaultDoc(): HTMLDocument;
abstract getBoundingClientRect(el: any /** TODO #9100 */): any /** TODO #9100 */; abstract getBoundingClientRect(el: any /** TODO #9100 */): any /** TODO #9100 */;
abstract getTitle(): string; abstract getTitle(doc: Document): string;
abstract setTitle(newTitle: string): any /** TODO #9100 */; abstract setTitle(doc: Document, newTitle: string): any /** TODO #9100 */;
abstract elementMatches(n: any /** TODO #9100 */, selector: string): boolean; abstract elementMatches(n: any /** TODO #9100 */, selector: string): boolean;
abstract isTemplateElement(el: any): boolean; abstract isTemplateElement(el: any): boolean;
abstract isTextNode(node: any /** TODO #9100 */): boolean; abstract isTextNode(node: any /** TODO #9100 */): boolean;
@ -164,10 +162,10 @@ export abstract class DomAdapter {
/** TODO #9100 */; /** TODO #9100 */;
abstract supportsDOMEvents(): boolean; abstract supportsDOMEvents(): boolean;
abstract supportsNativeShadowDOM(): boolean; abstract supportsNativeShadowDOM(): boolean;
abstract getGlobalEventTarget(target: string): any; abstract getGlobalEventTarget(doc: Document, target: string): any;
abstract getHistory(): History; abstract getHistory(): History;
abstract getLocation(): Location; abstract getLocation(): Location;
abstract getBaseHref(): string; abstract getBaseHref(doc: Document): string;
abstract resetBaseElement(): void; abstract resetBaseElement(): void;
abstract getUserAgent(): string; abstract getUserAgent(): string;
abstract setData(element: any /** TODO #9100 */, name: string, value: string): any abstract setData(element: any /** TODO #9100 */, name: string, value: string): any

View File

@ -6,11 +6,16 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injectable} from '@angular/core'; import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '../dom_tokens';
import {EventManagerPlugin} from './event_manager'; import {EventManagerPlugin} from './event_manager';
@Injectable() @Injectable()
export class DomEventsPlugin extends EventManagerPlugin { export class DomEventsPlugin extends EventManagerPlugin {
constructor(@Inject(DOCUMENT) doc: any) { super(doc); }
// This plugin should come last in the list of plugins, because it accepts all // This plugin should come last in the list of plugins, because it accepts all
// events. // events.
supports(eventName: string): boolean { return true; } supports(eventName: string): boolean { return true; }

View File

@ -10,7 +10,6 @@ import {Inject, Injectable, InjectionToken, NgZone} from '@angular/core';
import {getDOM} from '../dom_adapter'; import {getDOM} from '../dom_adapter';
/** /**
* @stable * @stable
*/ */
@ -62,6 +61,8 @@ export class EventManager {
} }
export abstract class EventManagerPlugin { export abstract class EventManagerPlugin {
constructor(private _doc: any) {}
manager: EventManager; manager: EventManager;
abstract supports(eventName: string): boolean; abstract supports(eventName: string): boolean;
@ -69,7 +70,7 @@ export abstract class EventManagerPlugin {
abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
addGlobalEventListener(element: string, eventName: string, handler: Function): Function { addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
const target: HTMLElement = getDOM().getGlobalEventTarget(element); const target: HTMLElement = getDOM().getGlobalEventTarget(this._doc, element);
if (!target) { if (!target) {
throw new Error(`Unsupported event target ${target} for event ${eventName}`); throw new Error(`Unsupported event target ${target} for event ${eventName}`);
} }

View File

@ -7,6 +7,9 @@
*/ */
import {Inject, Injectable, InjectionToken} from '@angular/core'; import {Inject, Injectable, InjectionToken} from '@angular/core';
import {DOCUMENT} from '../dom_tokens';
import {EventManagerPlugin} from './event_manager'; import {EventManagerPlugin} from './event_manager';
const EVENT_NAMES = { const EVENT_NAMES = {
@ -85,7 +88,11 @@ export class HammerGestureConfig {
@Injectable() @Injectable()
export class HammerGesturesPlugin extends EventManagerPlugin { export class HammerGesturesPlugin extends EventManagerPlugin {
constructor(@Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig) { super(); } constructor(
@Inject(DOCUMENT) doc: any,
@Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig) {
super(doc);
}
supports(eventName: string): boolean { supports(eventName: string): boolean {
if (!EVENT_NAMES.hasOwnProperty(eventName.toLowerCase()) && !this.isCustomEvent(eventName)) { if (!EVENT_NAMES.hasOwnProperty(eventName.toLowerCase()) && !this.isCustomEvent(eventName)) {

View File

@ -6,8 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injectable, NgZone} from '@angular/core'; import {Inject, Injectable, NgZone} from '@angular/core';
import {getDOM} from '../dom_adapter'; import {getDOM} from '../dom_adapter';
import {DOCUMENT} from '../dom_tokens';
import {EventManagerPlugin} from './event_manager'; import {EventManagerPlugin} from './event_manager';
const MODIFIER_KEYS = ['alt', 'control', 'meta', 'shift']; const MODIFIER_KEYS = ['alt', 'control', 'meta', 'shift'];
@ -23,7 +26,7 @@ const MODIFIER_KEY_GETTERS: {[key: string]: (event: KeyboardEvent) => boolean} =
*/ */
@Injectable() @Injectable()
export class KeyEventsPlugin extends EventManagerPlugin { export class KeyEventsPlugin extends EventManagerPlugin {
constructor() { super(); } constructor(@Inject(DOCUMENT) doc: any) { super(doc); }
supports(eventName: string): boolean { return KeyEventsPlugin.parseEventName(eventName) != null; } supports(eventName: string): boolean { return KeyEventsPlugin.parseEventName(eventName) != null; }

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injectable, Sanitizer, SecurityContext} from '@angular/core'; import {Inject, Injectable, Sanitizer, SecurityContext} from '@angular/core';
import {DOCUMENT} from '../dom/dom_tokens';
import {sanitizeHtml} from './html_sanitizer'; import {sanitizeHtml} from './html_sanitizer';
import {sanitizeStyle} from './style_sanitizer'; import {sanitizeStyle} from './style_sanitizer';
@ -15,6 +17,7 @@ import {sanitizeUrl} from './url_sanitizer';
export {SecurityContext}; export {SecurityContext};
/** /**
* Marker interface for a value that's safe to use in a particular context. * Marker interface for a value that's safe to use in a particular context.
* *
@ -147,6 +150,8 @@ export abstract class DomSanitizer implements Sanitizer {
@Injectable() @Injectable()
export class DomSanitizerImpl extends DomSanitizer { export class DomSanitizerImpl extends DomSanitizer {
constructor(@Inject(DOCUMENT) private _doc: any) { super(); }
sanitize(ctx: SecurityContext, value: any): string { sanitize(ctx: SecurityContext, value: any): string {
if (value == null) return null; if (value == null) return null;
switch (ctx) { switch (ctx) {
@ -155,7 +160,7 @@ export class DomSanitizerImpl extends DomSanitizer {
case SecurityContext.HTML: case SecurityContext.HTML:
if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity; if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'HTML'); this.checkNotSafeValue(value, 'HTML');
return sanitizeHtml(String(value)); return sanitizeHtml(this._doc, String(value));
case SecurityContext.STYLE: case SecurityContext.STYLE:
if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity; if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'Style'); this.checkNotSafeValue(value, 'Style');

View File

@ -9,6 +9,7 @@
import {isDevMode} from '@angular/core'; import {isDevMode} from '@angular/core';
import {DomAdapter, getDOM} from '../dom/dom_adapter'; import {DomAdapter, getDOM} from '../dom/dom_adapter';
import {DOCUMENT} from '../dom/dom_tokens';
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer'; import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
@ -243,7 +244,7 @@ function stripCustomNsAttrs(el: Element) {
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to * Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
* the DOM in a browser environment. * the DOM in a browser environment.
*/ */
export function sanitizeHtml(unsafeHtmlInput: string): string { export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
try { try {
const containerEl = getInertElement(); const containerEl = getInertElement();
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime). // Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
@ -262,7 +263,7 @@ export function sanitizeHtml(unsafeHtmlInput: string): string {
unsafeHtml = parsedHtml; unsafeHtml = parsedHtml;
DOM.setInnerHTML(containerEl, unsafeHtml); DOM.setInnerHTML(containerEl, unsafeHtml);
if ((DOM.defaultDoc() as any).documentMode) { if (defaultDoc.documentMode) {
// strip custom-namespaced attributes on IE<=11 // strip custom-namespaced attributes on IE<=11
stripCustomNsAttrs(containerEl); stripCustomNsAttrs(containerEl);
} }

View File

@ -14,9 +14,8 @@ import {expect} from '@angular/platform-browser/testing/matchers';
export function main() { export function main() {
describe('Meta service', () => { describe('Meta service', () => {
const doc: HTMLDocument = getDOM().createHtmlDocument();
const metaService: Meta = new Meta(getDOM()); const metaService: Meta = new Meta(doc);
const doc: HTMLDocument = getDOM().defaultDoc();
let defaultMeta: HTMLMetaElement; let defaultMeta: HTMLMetaElement;
beforeEach(() => { beforeEach(() => {

View File

@ -14,23 +14,24 @@ import {expect} from '@angular/platform-browser/testing/matchers';
export function main() { export function main() {
describe('title service', () => { describe('title service', () => {
const initialTitle = getDOM().getTitle(); const doc = getDOM().createHtmlDocument();
const titleService = new Title(); const initialTitle = getDOM().getTitle(doc);
const titleService = new Title(doc);
afterEach(() => { getDOM().setTitle(initialTitle); }); afterEach(() => { getDOM().setTitle(doc, initialTitle); });
it('should allow reading initial title', it('should allow reading initial title',
() => { expect(titleService.getTitle()).toEqual(initialTitle); }); () => { expect(titleService.getTitle()).toEqual(initialTitle); });
it('should set a title on the injected document', () => { it('should set a title on the injected document', () => {
titleService.setTitle('test title'); titleService.setTitle('test title');
expect(getDOM().getTitle()).toEqual('test title'); expect(getDOM().getTitle(doc)).toEqual('test title');
expect(titleService.getTitle()).toEqual('test title'); expect(titleService.getTitle()).toEqual('test title');
}); });
it('should reset title to empty string if title not provided', () => { it('should reset title to empty string if title not provided', () => {
titleService.setTitle(null); titleService.setTitle(null);
expect(getDOM().getTitle()).toEqual(''); expect(getDOM().getTitle(doc)).toEqual('');
}); });
}); });

View File

@ -15,16 +15,20 @@ import {el} from '../../../testing/browser_util';
export function main() { export function main() {
let domEventPlugin: DomEventsPlugin; let domEventPlugin: DomEventsPlugin;
let doc: any;
describe('EventManager', () => { describe('EventManager', () => {
beforeEach(() => { domEventPlugin = new DomEventsPlugin(); }); beforeEach(() => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
domEventPlugin = new DomEventsPlugin(doc);
});
it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one', it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one',
() => { () => {
const element = el('<div></div>'); const element = el('<div></div>');
const handler = (e: any /** TODO #9100 */) => e; const handler = (e: any /** TODO #9100 */) => e;
const plugin = new FakeEventManagerPlugin(['click']); const plugin = new FakeEventManagerPlugin(doc, ['click']);
const manager = new EventManager([domEventPlugin, plugin], new FakeNgZone()); const manager = new EventManager([domEventPlugin, plugin], new FakeNgZone());
manager.addEventListener(element, 'click', handler); manager.addEventListener(element, 'click', handler);
expect(plugin.eventHandler['click']).toBe(handler); expect(plugin.eventHandler['click']).toBe(handler);
@ -34,8 +38,8 @@ export function main() {
const element = el('<div></div>'); const element = el('<div></div>');
const clickHandler = (e: any /** TODO #9100 */) => e; const clickHandler = (e: any /** TODO #9100 */) => e;
const dblClickHandler = (e: any /** TODO #9100 */) => e; const dblClickHandler = (e: any /** TODO #9100 */) => e;
const plugin1 = new FakeEventManagerPlugin(['dblclick']); const plugin1 = new FakeEventManagerPlugin(doc, ['dblclick']);
const plugin2 = new FakeEventManagerPlugin(['click', 'dblclick']); const plugin2 = new FakeEventManagerPlugin(doc, ['click', 'dblclick']);
const manager = new EventManager([plugin2, plugin1], new FakeNgZone()); const manager = new EventManager([plugin2, plugin1], new FakeNgZone());
manager.addEventListener(element, 'click', clickHandler); manager.addEventListener(element, 'click', clickHandler);
manager.addEventListener(element, 'dblclick', dblClickHandler); manager.addEventListener(element, 'dblclick', dblClickHandler);
@ -45,7 +49,7 @@ export function main() {
it('should throw when no plugin can handle the event', () => { it('should throw when no plugin can handle the event', () => {
const element = el('<div></div>'); const element = el('<div></div>');
const plugin = new FakeEventManagerPlugin(['dblclick']); const plugin = new FakeEventManagerPlugin(doc, ['dblclick']);
const manager = new EventManager([plugin], new FakeNgZone()); const manager = new EventManager([plugin], new FakeNgZone());
expect(() => manager.addEventListener(element, 'click', null)) expect(() => manager.addEventListener(element, 'click', null))
.toThrowError('No event manager plugin found for event click'); .toThrowError('No event manager plugin found for event click');
@ -54,7 +58,7 @@ export function main() {
it('events are caught when fired from a child', () => { it('events are caught when fired from a child', () => {
const element = el('<div><div></div></div>'); const element = el('<div><div></div></div>');
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=122755 // Workaround for https://bugs.webkit.org/show_bug.cgi?id=122755
getDOM().appendChild(getDOM().defaultDoc().body, element); getDOM().appendChild(doc.body, element);
const child = getDOM().firstChild(element); const child = getDOM().firstChild(element);
const dispatchedEvent = getDOM().createMouseEvent('click'); const dispatchedEvent = getDOM().createMouseEvent('click');
@ -69,7 +73,7 @@ export function main() {
it('should add and remove global event listeners', () => { it('should add and remove global event listeners', () => {
const element = el('<div><div></div></div>'); const element = el('<div><div></div></div>');
getDOM().appendChild(getDOM().defaultDoc().body, element); getDOM().appendChild(doc.body, element);
const dispatchedEvent = getDOM().createMouseEvent('click'); const dispatchedEvent = getDOM().createMouseEvent('click');
let receivedEvent: any /** TODO #9100 */ = null; let receivedEvent: any /** TODO #9100 */ = null;
const handler = (e: any /** TODO #9100 */) => { receivedEvent = e; }; const handler = (e: any /** TODO #9100 */) => { receivedEvent = e; };
@ -91,7 +95,7 @@ export function main() {
class FakeEventManagerPlugin extends EventManagerPlugin { class FakeEventManagerPlugin extends EventManagerPlugin {
eventHandler: {[event: string]: Function} = {}; eventHandler: {[event: string]: Function} = {};
constructor(public supportedEvents: string[]) { super(); } constructor(doc: any, public supportedEvents: string[]) { super(doc); }
supports(eventName: string): boolean { return this.supportedEvents.indexOf(eventName) > -1; } supports(eventName: string): boolean { return this.supportedEvents.indexOf(eventName) > -1; }

View File

@ -12,7 +12,7 @@ export function main() {
describe('HammerGesturesPlugin', () => { describe('HammerGesturesPlugin', () => {
it('should implement addGlobalEventListener', () => { it('should implement addGlobalEventListener', () => {
const plugin = new HammerGesturesPlugin(new HammerGestureConfig()); const plugin = new HammerGesturesPlugin(document, new HammerGestureConfig());
spyOn(plugin, 'addEventListener').and.callFake(() => {}); spyOn(plugin, 'addEventListener').and.callFake(() => {});

View File

@ -59,7 +59,7 @@ export function main() {
}); });
it('should implement addGlobalEventListener', () => { it('should implement addGlobalEventListener', () => {
const plugin = new KeyEventsPlugin(); const plugin = new KeyEventsPlugin(document);
spyOn(plugin, 'addEventListener').and.callFake(() => {}); spyOn(plugin, 'addEventListener').and.callFake(() => {});

View File

@ -14,7 +14,7 @@ import {DomSanitizerImpl} from '../../src/security/dom_sanitization_service';
export function main() { export function main() {
t.describe('DOM Sanitization Service', () => { t.describe('DOM Sanitization Service', () => {
t.it('accepts resource URL values for resource contexts', () => { t.it('accepts resource URL values for resource contexts', () => {
const svc = new DomSanitizerImpl(); const svc = new DomSanitizerImpl(null);
const resourceUrl = svc.bypassSecurityTrustResourceUrl('http://hello/world'); const resourceUrl = svc.bypassSecurityTrustResourceUrl('http://hello/world');
t.expect(svc.sanitize(SecurityContext.URL, resourceUrl)).toBe('http://hello/world'); t.expect(svc.sanitize(SecurityContext.URL, resourceUrl)).toBe('http://hello/world');
}); });

View File

@ -14,10 +14,12 @@ import {sanitizeHtml} from '../../src/security/html_sanitizer';
export function main() { export function main() {
t.describe('HTML sanitizer', () => { t.describe('HTML sanitizer', () => {
let defaultDoc: any;
let originalLog: (msg: any) => any = null; let originalLog: (msg: any) => any = null;
let logMsgs: string[]; let logMsgs: string[];
t.beforeEach(() => { t.beforeEach(() => {
defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
logMsgs = []; logMsgs = [];
originalLog = getDOM().log; // Monkey patch DOM.log. originalLog = getDOM().log; // Monkey patch DOM.log.
getDOM().log = (msg) => logMsgs.push(msg); getDOM().log = (msg) => logMsgs.push(msg);
@ -25,52 +27,55 @@ export function main() {
t.afterEach(() => { getDOM().log = originalLog; }); t.afterEach(() => { getDOM().log = originalLog; });
t.it('serializes nested structures', () => { t.it('serializes nested structures', () => {
t.expect(sanitizeHtml('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>')) t.expect(sanitizeHtml(defaultDoc, '<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'))
.toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'); .toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>');
t.expect(logMsgs).toEqual([]); t.expect(logMsgs).toEqual([]);
}); });
t.it('serializes self closing elements', () => { t.it('serializes self closing elements', () => {
t.expect(sanitizeHtml('<p>Hello <br> World</p>')).toEqual('<p>Hello <br> World</p>'); t.expect(sanitizeHtml(defaultDoc, '<p>Hello <br> World</p>'))
.toEqual('<p>Hello <br> World</p>');
}); });
t.it('supports namespaced elements', () => { t.it('supports namespaced elements', () => {
t.expect(sanitizeHtml('a<my:hr/><my:div>b</my:div>c')).toEqual('abc'); t.expect(sanitizeHtml(defaultDoc, 'a<my:hr/><my:div>b</my:div>c')).toEqual('abc');
}); });
t.it('supports namespaced attributes', () => { t.it('supports namespaced attributes', () => {
t.expect(sanitizeHtml('<a xlink:href="something">t</a>')) t.expect(sanitizeHtml(defaultDoc, '<a xlink:href="something">t</a>'))
.toEqual('<a xlink:href="something">t</a>'); .toEqual('<a xlink:href="something">t</a>');
t.expect(sanitizeHtml('<a xlink:evil="something">t</a>')).toEqual('<a>t</a>'); t.expect(sanitizeHtml(defaultDoc, '<a xlink:evil="something">t</a>')).toEqual('<a>t</a>');
t.expect(sanitizeHtml('<a xlink:href="javascript:foo()">t</a>')) t.expect(sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>'))
.toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>'); .toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>');
}); });
t.it('supports HTML5 elements', () => { t.it('supports HTML5 elements', () => {
t.expect(sanitizeHtml('<main><summary>Works</summary></main>')) t.expect(sanitizeHtml(defaultDoc, '<main><summary>Works</summary></main>'))
.toEqual('<main><summary>Works</summary></main>'); .toEqual('<main><summary>Works</summary></main>');
}); });
t.it('sanitizes srcset attributes', () => { t.it('sanitizes srcset attributes', () => {
t.expect(sanitizeHtml('<img srcset="/foo.png 400px, javascript:evil() 23px">')) t.expect(sanitizeHtml(defaultDoc, '<img srcset="/foo.png 400px, javascript:evil() 23px">'))
.toEqual('<img srcset="/foo.png 400px, unsafe:javascript:evil() 23px">'); .toEqual('<img srcset="/foo.png 400px, unsafe:javascript:evil() 23px">');
}); });
t.it('supports sanitizing plain text', () => { t.it('supports sanitizing plain text', () => {
t.expect(sanitizeHtml('Hello, World')).toEqual('Hello, World'); t.expect(sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World');
}); });
t.it('ignores non-element, non-attribute nodes', () => { t.it('ignores non-element, non-attribute nodes', () => {
t.expect(sanitizeHtml('<!-- comments? -->no.')).toEqual('no.'); t.expect(sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
t.expect(sanitizeHtml('<?pi nodes?>no.')).toEqual('no.'); t.expect(sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
t.expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/); t.expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
}); });
t.it('supports sanitizing escaped entities', () => { t.it('supports sanitizing escaped entities', () => {
t.expect(sanitizeHtml('&#128640;')).toEqual('&#128640;'); t.expect(sanitizeHtml(defaultDoc, '&#128640;')).toEqual('&#128640;');
t.expect(logMsgs).toEqual([]); t.expect(logMsgs).toEqual([]);
}); });
t.it('does not warn when just re-encoding text', () => { t.it('does not warn when just re-encoding text', () => {
t.expect(sanitizeHtml('<p>Hellö Wörld</p>')).toEqual('<p>Hell&#246; W&#246;rld</p>'); t.expect(sanitizeHtml(defaultDoc, '<p>Hellö Wörld</p>'))
.toEqual('<p>Hell&#246; W&#246;rld</p>');
t.expect(logMsgs).toEqual([]); t.expect(logMsgs).toEqual([]);
}); });
t.it('escapes entities', () => { t.it('escapes entities', () => {
t.expect(sanitizeHtml('<p>Hello &lt; World</p>')).toEqual('<p>Hello &lt; World</p>'); t.expect(sanitizeHtml(defaultDoc, '<p>Hello &lt; World</p>'))
t.expect(sanitizeHtml('<p>Hello < World</p>')).toEqual('<p>Hello &lt; World</p>'); .toEqual('<p>Hello &lt; World</p>');
t.expect(sanitizeHtml('<p alt="% &amp; &quot; !">Hello</p>')) t.expect(sanitizeHtml(defaultDoc, '<p>Hello < World</p>')).toEqual('<p>Hello &lt; World</p>');
t.expect(sanitizeHtml(defaultDoc, '<p alt="% &amp; &quot; !">Hello</p>'))
.toEqual('<p alt="% &amp; &#34; !">Hello</p>'); // NB: quote encoded as ASCII &#34;. .toEqual('<p alt="% &amp; &#34; !">Hello</p>'); // NB: quote encoded as ASCII &#34;.
}); });
t.describe('should strip dangerous elements', () => { t.describe('should strip dangerous elements', () => {
@ -80,11 +85,12 @@ export function main() {
]; ];
for (const tag of dangerousTags) { for (const tag of dangerousTags) {
t.it( t.it(`${tag}`, () => {
`${tag}`, () => { t.expect(sanitizeHtml(`<${tag}>evil!</${tag}>`)).toEqual('evil!'); }); t.expect(sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!');
});
} }
t.it(`swallows frame entirely`, () => { t.it(`swallows frame entirely`, () => {
t.expect(sanitizeHtml(`<frame>evil!</frame>`)).not.toContain('<frame>'); t.expect(sanitizeHtml(defaultDoc, `<frame>evil!</frame>`)).not.toContain('<frame>');
}); });
}); });
t.describe('should strip dangerous attributes', () => { t.describe('should strip dangerous attributes', () => {
@ -92,14 +98,14 @@ export function main() {
for (const attr of dangerousAttrs) { for (const attr of dangerousAttrs) {
t.it(`${attr}`, () => { t.it(`${attr}`, () => {
t.expect(sanitizeHtml(`<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>'); t.expect(sanitizeHtml(defaultDoc, `<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>');
}); });
} }
}); });
if (browserDetection.isWebkit) { if (browserDetection.isWebkit) {
t.it('should prevent mXSS attacks', function() { t.it('should prevent mXSS attacks', function() {
t.expect(sanitizeHtml('<a href="&#x3000;javascript:alert(1)">CLICKME</a>')) t.expect(sanitizeHtml(defaultDoc, '<a href="&#x3000;javascript:alert(1)">CLICKME</a>'))
.toEqual('<a href="unsafe:javascript:alert(1)">CLICKME</a>'); .toEqual('<a href="unsafe:javascript:alert(1)">CLICKME</a>');
}); });
} }

View File

@ -7,7 +7,8 @@
*/ */
import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common'; import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common';
import {Injectable} from '@angular/core'; import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {Subject} from 'rxjs/Subject'; import {Subject} from 'rxjs/Subject';
import * as url from 'url'; import * as url from 'url';
@ -27,7 +28,9 @@ export class ServerPlatformLocation implements PlatformLocation {
private _hash: string = ''; private _hash: string = '';
private _hashUpdate = new Subject<LocationChangeEvent>(); private _hashUpdate = new Subject<LocationChangeEvent>();
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); } constructor(@Inject(DOCUMENT) private _doc: any) {}
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); }
onPopState(fn: LocationChangeListener): void { onPopState(fn: LocationChangeListener): void {
// No-op: a state stack is not implemented, so // No-op: a state stack is not implemented, so

View File

@ -22,14 +22,20 @@ const _attrToPropMap: {[key: string]: string} = {
'tabindex': 'tabIndex', 'tabindex': 'tabIndex',
}; };
let defDoc: any = null;
const mapProps = ['attribs', 'x-attribsNamespace', 'x-attribsPrefix']; const mapProps = ['attribs', 'x-attribsNamespace', 'x-attribsPrefix'];
function _notImplemented(methodName: string) { function _notImplemented(methodName: string) {
return new Error('This method is not implemented in Parse5DomAdapter: ' + methodName); return new Error('This method is not implemented in Parse5DomAdapter: ' + methodName);
} }
/**
* Parses a document string to a Document object.
*/
export function parseDocument(html: string) {
return parse5.parse(html, {treeAdapter: parse5.treeAdapters.htmlparser2});
}
/* tslint:disable:requireParameterType */ /* tslint:disable:requireParameterType */
/** /**
* A `DomAdapter` powered by the `parse5` NodeJS module. * A `DomAdapter` powered by the `parse5` NodeJS module.
@ -72,7 +78,6 @@ export class Parse5DomAdapter extends DomAdapter {
get attrToPropMap() { return _attrToPropMap; } get attrToPropMap() { return _attrToPropMap; }
query(selector: any) { throw _notImplemented('query'); }
querySelector(el: any, selector: string): any { return this.querySelectorAll(el, selector)[0]; } querySelector(el: any, selector: string): any { return this.querySelectorAll(el, selector)[0]; }
querySelectorAll(el: any, selector: string): any[] { querySelectorAll(el: any, selector: string): any[] {
const res: any[] = []; const res: any[] = [];
@ -468,7 +473,7 @@ export class Parse5DomAdapter extends DomAdapter {
} }
createHtmlDocument(): Document { createHtmlDocument(): Document {
const newDoc = treeAdapter.createDocument(); const newDoc = treeAdapter.createDocument();
newDoc.title = 'fake title'; newDoc.title = 'fakeTitle';
const head = treeAdapter.createElement('head', null, []); const head = treeAdapter.createElement('head', null, []);
const body = treeAdapter.createElement('body', 'http://www.w3.org/1999/xhtml', []); const body = treeAdapter.createElement('body', 'http://www.w3.org/1999/xhtml', []);
this.appendChild(newDoc, head); this.appendChild(newDoc, head);
@ -478,10 +483,9 @@ export class Parse5DomAdapter extends DomAdapter {
newDoc['_window'] = {}; newDoc['_window'] = {};
return newDoc; return newDoc;
} }
defaultDoc(): Document { return defDoc = defDoc || this.createHtmlDocument(); }
getBoundingClientRect(el: any): any { return {left: 0, top: 0, width: 0, height: 0}; } getBoundingClientRect(el: any): any { return {left: 0, top: 0, width: 0, height: 0}; }
getTitle(): string { return this.defaultDoc().title || ''; } getTitle(doc: Document): string { return doc.title || ''; }
setTitle(newTitle: string) { this.defaultDoc().title = newTitle; } setTitle(doc: Document, newTitle: string) { doc.title = newTitle; }
isTemplateElement(el: any): boolean { isTemplateElement(el: any): boolean {
return this.isElementNode(el) && this.tagName(el) === 'template'; return this.isElementNode(el) && this.tagName(el) === 'template';
} }
@ -538,17 +542,17 @@ export class Parse5DomAdapter extends DomAdapter {
} }
supportsDOMEvents(): boolean { return false; } supportsDOMEvents(): boolean { return false; }
supportsNativeShadowDOM(): boolean { return false; } supportsNativeShadowDOM(): boolean { return false; }
getGlobalEventTarget(target: string): any { getGlobalEventTarget(doc: Document, target: string): any {
if (target == 'window') { if (target == 'window') {
return (<any>this.defaultDoc())._window; return (<any>doc)._window;
} else if (target == 'document') { } else if (target == 'document') {
return this.defaultDoc(); return doc;
} else if (target == 'body') { } else if (target == 'body') {
return this.defaultDoc().body; return doc.body;
} }
} }
getBaseHref(): string { getBaseHref(doc: Document): string {
const base = this.querySelector(this.defaultDoc(), 'base'); const base = this.querySelector(doc, 'base');
let href = ''; let href = '';
if (base) { if (base) {
href = this.getHref(base); href = this.getHref(base);

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {ServerModule, platformDynamicServer, platformServer} from './server'; export {PlatformState} from './platform_state';
export {INITIAL_CONFIG, ServerModule, platformDynamicServer, platformServer} from './server';
export {renderModule, renderModuleFactory} from './utils';
export * from './private_export'; export * from './private_export';
export {VERSION} from './version'; export {VERSION} from './version';

View File

@ -0,0 +1,34 @@
/**
* @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
*/
const parse5 = require('parse5');
import {Injectable, Inject} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {getDOM} from './private_import_platform-browser';
/**
* Representation of the current platform state.
*
* @experimental
*/
@Injectable()
export class PlatformState {
constructor(@Inject(DOCUMENT) private _doc: any) {}
/**
* Renders the current state of the platform to string.
*/
renderToString(): string { return getDOM().getInnerHTML(this._doc); }
/**
* Returns the current DOM state.
*/
getDocument(): any { return this._doc; }
}

View File

@ -8,13 +8,14 @@
import {PlatformLocation} from '@angular/common'; import {PlatformLocation} from '@angular/common';
import {platformCoreDynamic} from '@angular/compiler'; import {platformCoreDynamic} from '@angular/compiler';
import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {Injectable, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule, DOCUMENT} from '@angular/platform-browser';
import {ServerPlatformLocation} from './location'; import {ServerPlatformLocation} from './location';
import {Parse5DomAdapter} from './parse5_adapter'; import {Parse5DomAdapter, parseDocument} from './parse5_adapter';
import {PlatformState} from './platform_state';
import {DebugDomRootRenderer} from './private_import_core'; import {DebugDomRootRenderer} from './private_import_core';
import {DomAdapter, SharedStylesHost} from './private_import_platform-browser'; import {SharedStylesHost, getDOM} from './private_import_platform-browser';
import {ServerRootRenderer} from './server_renderer'; import {ServerRootRenderer} from './server_renderer';
@ -23,15 +24,16 @@ function notSupported(feature: string): Error {
} }
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: DOCUMENT, useFactory: _document, deps: [Injector]},
{provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]},
{provide: PlatformLocation, useClass: ServerPlatformLocation}, {provide: PlatformLocation, useClass: ServerPlatformLocation},
PlatformState,
]; ];
function initParse5Adapter() { function initParse5Adapter(injector: Injector) {
Parse5DomAdapter.makeCurrent(); return () => { Parse5DomAdapter.makeCurrent(); };
} }
export function _createConditionalRootRenderer(rootRenderer: any) { export function _createConditionalRootRenderer(rootRenderer: any) {
if (isDevMode()) { if (isDevMode()) {
return new DebugDomRootRenderer(rootRenderer); return new DebugDomRootRenderer(rootRenderer);
@ -46,15 +48,46 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [
SharedStylesHost SharedStylesHost
]; ];
/**
* Config object passed to initialize the platform.
*
* @experimental
*/
export interface PlatformConfig {
document?: string;
url?: string;
}
/**
* The DI token for setting the initial config for the platform.
*
* @experimental
*/
export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL_CONFIG');
/** /**
* The ng module for the server. * The ng module for the server.
* *
* @experimental * @experimental
*/ */
@NgModule({exports: [BrowserModule], providers: SERVER_RENDER_PROVIDERS}) @NgModule({
exports: [BrowserModule],
providers: [
SERVER_RENDER_PROVIDERS,
]
})
export class ServerModule { export class ServerModule {
} }
function _document(injector: Injector) {
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
if (config && config.document) {
return parseDocument(config.document);
} else {
return getDOM().createHtmlDocument();
}
}
/** /**
* @experimental * @experimental
*/ */

View File

@ -136,7 +136,7 @@ export class ServerRenderer implements Renderer {
} }
listenGlobal(target: string, name: string, callback: Function): Function { listenGlobal(target: string, name: string, callback: Function): Function {
const renderElement = getDOM().getGlobalEventTarget(target); const renderElement = getDOM().getGlobalEventTarget(this._rootRenderer.document, target);
return this.listen(renderElement, name, callback); return this.listen(renderElement, name, callback);
} }

View File

@ -0,0 +1,71 @@
/**
* @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 {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type, destroyPlatform} from '@angular/core';
import {filter} from 'rxjs/operator/filter';
import {first} from 'rxjs/operator/first';
import {toPromise} from 'rxjs/operator/toPromise';
import {PlatformState} from './platform_state';
import {INITIAL_CONFIG, platformDynamicServer, platformServer} from './server';
const parse5 = require('parse5');
export interface PlatformOptions {
document?: string;
url?: string;
extraProviders?: Provider[];
}
function _getPlatform(
platformFactory: (extraProviders: Provider[]) => PlatformRef,
options: PlatformOptions): PlatformRef {
const extraProviders = options.extraProviders ? options.extraProviders : [];
return platformFactory([
{provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
extraProviders
]);
}
function _render<T>(
platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<string> {
return moduleRefPromise.then((moduleRef) => {
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return toPromise
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
.then(() => {
const output = platform.injector.get(PlatformState).renderToString();
destroyPlatform();
return output;
});
});
}
/**
* Renders a Module to string.
*
* Do not use this in a production server environment. Use pre-compiled {@link NgModuleFactory} with
* {link renderModuleFactory} instead.
*
* @experimental
*/
export function renderModule<T>(module: Type<T>, options: PlatformOptions): Promise<string> {
const platform = _getPlatform(platformDynamicServer, options);
return _render(platform, platform.bootstrapModule(module));
}
/**
* Renders a {@link NgModuleFactory} to string.
*
* @experimental
*/
export function renderModuleFactory<T>(
moduleFactory: NgModuleFactory<T>, options: PlatformOptions): Promise<string> {
const platform = _getPlatform(platformServer, options);
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
}

View File

@ -7,19 +7,15 @@
*/ */
import {PlatformLocation} from '@angular/common'; import {PlatformLocation} from '@angular/common';
import {Component, NgModule, destroyPlatform} from '@angular/core'; import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
import {async} from '@angular/core/testing'; import {async, inject} from '@angular/core/testing';
import {DOCUMENT} from '@angular/platform-browser';
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 {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
import {Subscription} from 'rxjs/Subscription';
function writeBody(html: string): any { import {filter} from 'rxjs/operator/filter';
const dom = getDOM(); import {first} from 'rxjs/operator/first';
const doc = dom.defaultDoc(); import {toPromise} from 'rxjs/operator/toPromise';
const body = dom.querySelector(doc, 'body');
dom.setInnerHTML(body, html);
return body;
}
@Component({selector: 'app', template: `Works!`}) @Component({selector: 'app', template: `Works!`})
class MyServerApp { class MyServerApp {
@ -38,23 +34,27 @@ export function main() {
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
it('should bootstrap', async(() => { it('should bootstrap', async(() => {
const body = writeBody('<app></app>'); platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}])
platformDynamicServer().bootstrapModule(ExampleModule).then(() => { .bootstrapModule(ExampleModule)
expect(getDOM().getText(body)).toEqual('Works!'); .then((moduleRef) => {
const doc = moduleRef.injector.get(DOCUMENT);
expect(getDOM().getText(doc)).toEqual('Works!');
}); });
})); }));
describe('PlatformLocation', () => { describe('PlatformLocation', () => {
it('is injectable', () => { it('is injectable', () => {
const body = writeBody('<app></app>'); platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}])
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { .bootstrapModule(ExampleModule)
.then(appRef => {
const location: PlatformLocation = appRef.injector.get(PlatformLocation); const location: PlatformLocation = appRef.injector.get(PlatformLocation);
expect(location.pathname).toBe('/'); expect(location.pathname).toBe('/');
}); });
}); });
it('pushState causes the URL to update', () => { it('pushState causes the URL to update', () => {
const body = writeBody('<app></app>'); platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}])
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { .bootstrapModule(ExampleModule)
.then(appRef => {
const location: PlatformLocation = appRef.injector.get(PlatformLocation); const location: PlatformLocation = appRef.injector.get(PlatformLocation);
location.pushState(null, 'Test', '/foo#bar'); location.pushState(null, 'Test', '/foo#bar');
expect(location.pathname).toBe('/foo'); expect(location.pathname).toBe('/foo');
@ -62,8 +62,9 @@ export function main() {
}); });
}); });
it('allows subscription to the hash state', done => { it('allows subscription to the hash state', done => {
const body = writeBody('<app></app>'); platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}])
platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { .bootstrapModule(ExampleModule)
.then(appRef => {
const location: PlatformLocation = appRef.injector.get(PlatformLocation); const location: PlatformLocation = appRef.injector.get(PlatformLocation);
expect(location.pathname).toBe('/'); expect(location.pathname).toBe('/');
location.onHashChange((e: any) => { location.onHashChange((e: any) => {
@ -77,4 +78,73 @@ export function main() {
}); });
}); });
}); });
describe('Platform Server', () => {
@Component({selector: 'app', template: '{{text}}'})
class MyAsyncServerApp {
text = '';
ngOnInit() {
Promise.resolve(null).then(() => setTimeout(() => { this.text = 'Works!'; }, 10));
}
}
@NgModule(
{declarations: [MyAsyncServerApp], imports: [ServerModule], bootstrap: [MyAsyncServerApp]})
class AsyncServerModule {
}
let doc: string;
let called: boolean;
let expectedOutput =
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>';
beforeEach(() => {
destroyPlatform();
// PlatformConfig takes in a parsed document so that it can be cached across requests.
doc = '<html><head></head><body><app></app></body></html>';
called = false;
});
afterEach(() => {
expect(called).toBe(true);
// Platform should have been destroyed at the end of rendering.
expect(getPlatform()).toBeNull();
});
it('PlatformState should render to string (Long form rendering)', async(() => {
const platform =
platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: doc}}]);
platform.bootstrapModule(AsyncServerModule)
.then((moduleRef) => {
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return toPromise.call(first.call(
filter.call(applicationRef.isStable, (isStable: boolean) => isStable)));
})
.then((b) => {
expect(platform.injector.get(PlatformState).renderToString()).toBe(expectedOutput);
destroyPlatform();
called = true;
});
}));
it('renderModule should render to string (short form rendering)', async(() => {
renderModule(AsyncServerModule, {document: doc}).then(output => {
expect(output).toBe(expectedOutput);
called = true;
});
}));
it('renderModuleFactory should render to string (short form rendering)',
async(inject([PlatformRef], (defaultPlatform: PlatformRef) => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory =
compilerFactory.createCompiler().compileModuleSync(AsyncServerModule);
renderModuleFactory(moduleFactory, {document: doc}).then(output => {
expect(output).toBe(expectedOutput);
called = true;
});
})));
});
} }

View File

@ -55,7 +55,6 @@ export class WorkerDomAdapter extends DomAdapter {
set attrToPropMap(value: {[key: string]: string}) { throw 'not implemented'; } set attrToPropMap(value: {[key: string]: string}) { throw 'not implemented'; }
parse(templateHtml: string) { throw 'not implemented'; } parse(templateHtml: string) { throw 'not implemented'; }
query(selector: string): any { throw 'not implemented'; }
querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement { querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement {
throw 'not implemented'; throw 'not implemented';
} }
@ -169,10 +168,9 @@ export class WorkerDomAdapter extends DomAdapter {
} }
templateAwareRoot(el: any /** TODO #9100 */) { throw 'not implemented'; } templateAwareRoot(el: any /** TODO #9100 */) { throw 'not implemented'; }
createHtmlDocument(): HTMLDocument { throw 'not implemented'; } createHtmlDocument(): HTMLDocument { throw 'not implemented'; }
defaultDoc(): HTMLDocument { throw 'not implemented'; }
getBoundingClientRect(el: any /** TODO #9100 */) { throw 'not implemented'; } getBoundingClientRect(el: any /** TODO #9100 */) { throw 'not implemented'; }
getTitle(): string { throw 'not implemented'; } getTitle(doc: Document): string { throw 'not implemented'; }
setTitle(newTitle: string) { throw 'not implemented'; } setTitle(doc: Document, newTitle: string) { throw 'not implemented'; }
elementMatches(n: any /** TODO #9100 */, selector: string): boolean { throw 'not implemented'; } elementMatches(n: any /** TODO #9100 */, selector: string): boolean { throw 'not implemented'; }
isTemplateElement(el: any): boolean { throw 'not implemented'; } isTemplateElement(el: any): boolean { throw 'not implemented'; }
isTextNode(node: any /** TODO #9100 */): boolean { throw 'not implemented'; } isTextNode(node: any /** TODO #9100 */): boolean { throw 'not implemented'; }
@ -189,10 +187,10 @@ export class WorkerDomAdapter extends DomAdapter {
} }
supportsDOMEvents(): boolean { throw 'not implemented'; } supportsDOMEvents(): boolean { throw 'not implemented'; }
supportsNativeShadowDOM(): boolean { throw 'not implemented'; } supportsNativeShadowDOM(): boolean { throw 'not implemented'; }
getGlobalEventTarget(target: string): any { throw 'not implemented'; } getGlobalEventTarget(doc: Document, target: string): any { throw 'not implemented'; }
getHistory(): History { throw 'not implemented'; } getHistory(): History { throw 'not implemented'; }
getLocation(): Location { throw 'not implemented'; } getLocation(): Location { throw 'not implemented'; }
getBaseHref(): string { throw 'not implemented'; } getBaseHref(doc: Document): string { throw 'not implemented'; }
resetBaseElement(): void { throw 'not implemented'; } resetBaseElement(): void { throw 'not implemented'; }
getUserAgent(): string { throw 'not implemented'; } getUserAgent(): string { throw 'not implemented'; }
setData(element: any /** TODO #9100 */, name: string, value: string) { throw 'not implemented'; } setData(element: any /** TODO #9100 */, name: string, value: string) { throw 'not implemented'; }

View File

@ -8,6 +8,7 @@
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PlatformRef, Provider, RootRenderer, createPlatformFactory, platformCore} from '@angular/core'; import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PlatformRef, Provider, RootRenderer, createPlatformFactory, platformCore} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {BROWSER_SANITIZATION_PROVIDERS} from './private_import_platform-browser'; import {BROWSER_SANITIZATION_PROVIDERS} from './private_import_platform-browser';
import {ON_WEB_WORKER} from './web_workers/shared/api'; import {ON_WEB_WORKER} from './web_workers/shared/api';
@ -60,7 +61,7 @@ export function setupWebWorker(): void {
*/ */
@NgModule({ @NgModule({
providers: [ providers: [
BROWSER_SANITIZATION_PROVIDERS, Serializer, BROWSER_SANITIZATION_PROVIDERS, Serializer, {provide: DOCUMENT, useValue: null},
{provide: ClientMessageBrokerFactory, useClass: ClientMessageBrokerFactory_}, {provide: ClientMessageBrokerFactory, useClass: ClientMessageBrokerFactory_},
{provide: ServiceMessageBrokerFactory, useClass: ServiceMessageBrokerFactory_}, {provide: ServiceMessageBrokerFactory, useClass: ServiceMessageBrokerFactory_},
WebWorkerRootRenderer, {provide: RootRenderer, useExisting: WebWorkerRootRenderer}, WebWorkerRootRenderer, {provide: RootRenderer, useExisting: WebWorkerRootRenderer},

View File

@ -135,7 +135,7 @@ function _exceptionHandler(): ErrorHandler {
} }
function _document(): any { function _document(): any {
return getDOM().defaultDoc(); return document;
} }
function createNgZone(): NgZone { function createNgZone(): NgZone {

View File

@ -120,8 +120,8 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
get _EVENT_MANAGER_PLUGINS_15(): any[] { get _EVENT_MANAGER_PLUGINS_15(): any[] {
if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) { if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) {
(this.__EVENT_MANAGER_PLUGINS_15 = [ (this.__EVENT_MANAGER_PLUGINS_15 = [
new import20.DomEventsPlugin(), new import21.KeyEventsPlugin(), new import20.DomEventsPlugin(document), new import21.KeyEventsPlugin(document),
new import10.HammerGesturesPlugin(this._HAMMER_GESTURE_CONFIG_14) new import10.HammerGesturesPlugin(document, this._HAMMER_GESTURE_CONFIG_14)
]); ]);
} }
return this.__EVENT_MANAGER_PLUGINS_15; return this.__EVENT_MANAGER_PLUGINS_15;
@ -163,7 +163,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
} }
get _DomSanitizer_21(): import14.DomSanitizerImpl { get _DomSanitizer_21(): import14.DomSanitizerImpl {
if ((this.__DomSanitizer_21 == (null as any))) { if ((this.__DomSanitizer_21 == (null as any))) {
(this.__DomSanitizer_21 = new import14.DomSanitizerImpl()); (this.__DomSanitizer_21 = new import14.DomSanitizerImpl(document));
} }
return this.__DomSanitizer_21; return this.__DomSanitizer_21;
} }
@ -206,7 +206,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
} }
get _Title_27(): import16.Title { get _Title_27(): import16.Title {
if ((this.__Title_27 == (null as any))) { if ((this.__Title_27 == (null as any))) {
(this.__Title_27 = new import16.Title()); (this.__Title_27 = new import16.Title(document));
} }
return this.__Title_27; return this.__Title_27;
} }

View File

@ -92,7 +92,7 @@ export class AppModule implements Injector {
constructor() { constructor() {
initServicesIfNeeded(); initServicesIfNeeded();
this.sanitizer = new DomSanitizerImpl(); this.sanitizer = new DomSanitizerImpl(document);
trustedEmptyColor = this.sanitizer.bypassSecurityTrustStyle(''); trustedEmptyColor = this.sanitizer.bypassSecurityTrustStyle('');
trustedGreyColor = this.sanitizer.bypassSecurityTrustStyle('grey'); trustedGreyColor = this.sanitizer.bypassSecurityTrustStyle('grey');
this.componentFactory = createComponentFactory('#root', TreeComponent, TreeComponent_Host); this.componentFactory = createComponentFactory('#root', TreeComponent, TreeComponent_Host);

View File

@ -120,8 +120,8 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
get _EVENT_MANAGER_PLUGINS_15(): any[] { get _EVENT_MANAGER_PLUGINS_15(): any[] {
if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) { if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) {
(this.__EVENT_MANAGER_PLUGINS_15 = [ (this.__EVENT_MANAGER_PLUGINS_15 = [
new import20.DomEventsPlugin(), new import21.KeyEventsPlugin(), new import20.DomEventsPlugin(document), new import21.KeyEventsPlugin(document),
new import10.HammerGesturesPlugin(this._HAMMER_GESTURE_CONFIG_14) new import10.HammerGesturesPlugin(document, this._HAMMER_GESTURE_CONFIG_14)
]); ]);
} }
return this.__EVENT_MANAGER_PLUGINS_15; return this.__EVENT_MANAGER_PLUGINS_15;
@ -163,7 +163,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
} }
get _DomSanitizer_21(): import14.DomSanitizerImpl { get _DomSanitizer_21(): import14.DomSanitizerImpl {
if ((this.__DomSanitizer_21 == (null as any))) { if ((this.__DomSanitizer_21 == (null as any))) {
(this.__DomSanitizer_21 = new import14.DomSanitizerImpl()); (this.__DomSanitizer_21 = new import14.DomSanitizerImpl(document));
} }
return this.__DomSanitizer_21; return this.__DomSanitizer_21;
} }
@ -206,7 +206,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
} }
get _Title_27(): import16.Title { get _Title_27(): import16.Title {
if ((this.__Title_27 == (null as any))) { if ((this.__Title_27 == (null as any))) {
(this.__Title_27 = new import16.Title()); (this.__Title_27 = new import16.Title(document));
} }
return this.__Title_27; return this.__Title_27;
} }

View File

@ -60,7 +60,7 @@ export declare class HammerGestureConfig {
/** @experimental */ /** @experimental */
export declare class Meta { export declare class Meta {
constructor(_dom: DomAdapter); constructor(_doc: any);
addTag(tag: MetaDefinition, forceCreation?: boolean): HTMLMetaElement; addTag(tag: MetaDefinition, forceCreation?: boolean): HTMLMetaElement;
addTags(tags: MetaDefinition[], forceCreation?: boolean): HTMLMetaElement[]; addTags(tags: MetaDefinition[], forceCreation?: boolean): HTMLMetaElement[];
getTag(attrSelector: string): HTMLMetaElement; getTag(attrSelector: string): HTMLMetaElement;
@ -117,6 +117,7 @@ export interface SafeUrl extends SafeValue {
/** @experimental */ /** @experimental */
export declare class Title { export declare class Title {
constructor(_doc: any);
getTitle(): string; getTitle(): string;
setTitle(newTitle: string): void; setTitle(newTitle: string): void;
} }

View File

@ -1,9 +1,25 @@
/** @experimental */
export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>;
/** @experimental */ /** @experimental */
export declare const platformDynamicServer: (extraProviders?: Provider[]) => PlatformRef; export declare const platformDynamicServer: (extraProviders?: Provider[]) => PlatformRef;
/** @experimental */ /** @experimental */
export declare const platformServer: (extraProviders?: Provider[]) => PlatformRef; export declare const platformServer: (extraProviders?: Provider[]) => PlatformRef;
/** @experimental */
export declare class PlatformState {
constructor(_doc: any);
getDocument(): any;
renderToString(): string;
}
/** @experimental */
export declare function renderModule<T>(module: Type<T>, options: PlatformOptions): Promise<string>;
/** @experimental */
export declare function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: PlatformOptions): Promise<string>;
/** @experimental */ /** @experimental */
export declare class ServerModule { export declare class ServerModule {
} }