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:
@ -30,7 +30,8 @@ import {DomSanitizer, DomSanitizerImpl} from './security/dom_sanitization_servic
|
||||
|
||||
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
|
||||
{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();
|
||||
}
|
||||
|
||||
export function meta(): Meta {
|
||||
return new Meta(getDOM());
|
||||
}
|
||||
|
||||
export function _document(): any {
|
||||
return getDOM().defaultDoc();
|
||||
return document;
|
||||
}
|
||||
|
||||
export function _resolveDefaultAnimationDriver(): AnimationDriver {
|
||||
@ -83,7 +80,6 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
|
||||
providers: [
|
||||
BROWSER_SANITIZATION_PROVIDERS,
|
||||
{provide: ErrorHandler, useFactory: errorHandler, deps: []},
|
||||
{provide: DOCUMENT, useFactory: _document, deps: []},
|
||||
{provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
|
||||
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
|
||||
{provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true},
|
||||
@ -92,11 +88,11 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver {
|
||||
{provide: RootRenderer, useExisting: DomRootRenderer},
|
||||
{provide: SharedStylesHost, useExisting: DomSharedStylesHost},
|
||||
{provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver},
|
||||
{provide: Meta, useFactory: meta},
|
||||
DomSharedStylesHost,
|
||||
Testability,
|
||||
EventManager,
|
||||
ELEMENT_PROBE_PROVIDERS,
|
||||
Meta,
|
||||
Title,
|
||||
],
|
||||
exports: [CommonModule, ApplicationModule]
|
||||
|
@ -107,10 +107,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
|
||||
get attrToPropMap(): any { return _attrToPropMap; }
|
||||
|
||||
query(selector: string): any { return document.querySelector(selector); }
|
||||
querySelector(el: Element, selector: string): HTMLElement {
|
||||
return el.querySelector(selector) as HTMLElement;
|
||||
}
|
||||
querySelector(el: Element, selector: string): any { return el.querySelector(selector); }
|
||||
querySelectorAll(el: any, selector: string): any[] { return el.querySelectorAll(selector); }
|
||||
on(el: Node, evt: any, listener: any) { el.addEventListener(evt, listener, false); }
|
||||
onAndCancel(el: Node, evt: any, listener: any): Function {
|
||||
@ -274,7 +271,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
createHtmlDocument(): HTMLDocument {
|
||||
return document.implementation.createHTMLDocument('fakeTitle');
|
||||
}
|
||||
defaultDoc(): HTMLDocument { return document; }
|
||||
getBoundingClientRect(el: Element): any {
|
||||
try {
|
||||
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};
|
||||
}
|
||||
}
|
||||
getTitle(): string { return document.title; }
|
||||
setTitle(newTitle: string) { document.title = newTitle || ''; }
|
||||
getTitle(doc: Document): string { return document.title; }
|
||||
setTitle(doc: Document, newTitle: string) { document.title = newTitle || ''; }
|
||||
elementMatches(n: any, selector: string): boolean {
|
||||
if (n instanceof HTMLElement) {
|
||||
return n.matches && n.matches(selector) ||
|
||||
@ -330,7 +326,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
|
||||
return _keyMap[key] || key;
|
||||
}
|
||||
getGlobalEventTarget(target: string): EventTarget {
|
||||
getGlobalEventTarget(doc: Document, target: string): EventTarget {
|
||||
if (target === 'window') {
|
||||
return window;
|
||||
}
|
||||
@ -343,7 +339,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
||||
}
|
||||
getHistory(): History { return window.history; }
|
||||
getLocation(): Location { return window.location; }
|
||||
getBaseHref(): string {
|
||||
getBaseHref(doc: Document): string {
|
||||
const href = getBaseElementHref();
|
||||
return isBlank(href) ? null : relativePath(href);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
|
||||
constructor() {
|
||||
super();
|
||||
try {
|
||||
const element = this.createElement('div', this.defaultDoc());
|
||||
const element = this.createElement('div', document);
|
||||
if (isPresent(this.getStyle(element, 'animationName'))) {
|
||||
this._animationPrefix = '';
|
||||
} else {
|
||||
@ -61,7 +61,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
|
||||
}
|
||||
supportsDOMEvents(): boolean { return true; }
|
||||
supportsNativeShadowDOM(): boolean {
|
||||
return typeof(<any>this.defaultDoc().body).createShadowRoot === 'function';
|
||||
return typeof(<any>document.body).createShadowRoot === 'function';
|
||||
}
|
||||
getAnimationPrefix(): string { return this._animationPrefix ? this._animationPrefix : ''; }
|
||||
getTransitionEnd(): string { return this._transitionEnd ? this._transitionEnd : ''; }
|
||||
|
@ -7,9 +7,10 @@
|
||||
*/
|
||||
|
||||
import {LocationChangeListener, PlatformLocation} from '@angular/common';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
import {getDOM} from '../../dom/dom_adapter';
|
||||
import {DOCUMENT} from '../../dom/dom_tokens';
|
||||
|
||||
import {supportsState} from './history';
|
||||
|
||||
@ -25,7 +26,7 @@ export class BrowserPlatformLocation extends PlatformLocation {
|
||||
private _location: Location;
|
||||
private _history: History;
|
||||
|
||||
constructor() {
|
||||
constructor(@Inject(DOCUMENT) private _doc: any) {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
@ -39,14 +40,14 @@ export class BrowserPlatformLocation extends PlatformLocation {
|
||||
|
||||
get location(): Location { return this._location; }
|
||||
|
||||
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); }
|
||||
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); }
|
||||
|
||||
onPopState(fn: LocationChangeListener): void {
|
||||
getDOM().getGlobalEventTarget('window').addEventListener('popstate', fn, false);
|
||||
getDOM().getGlobalEventTarget(this._doc, 'window').addEventListener('popstate', fn, false);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
@ -6,8 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {DomAdapter} from '../dom/dom_adapter';
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
import {DOCUMENT} from '../dom/dom_tokens';
|
||||
|
||||
|
||||
/**
|
||||
* Represents a meta element.
|
||||
@ -33,7 +36,8 @@ export type MetaDefinition = {
|
||||
*/
|
||||
@Injectable()
|
||||
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 {
|
||||
if (!tag) return null;
|
||||
@ -52,13 +56,12 @@ export class Meta {
|
||||
|
||||
getTag(attrSelector: string): HTMLMetaElement {
|
||||
if (!attrSelector) return null;
|
||||
return this._dom.query(`meta[${attrSelector}]`);
|
||||
return this._dom.querySelector(this._doc, `meta[${attrSelector}]`);
|
||||
}
|
||||
|
||||
getTags(attrSelector: string): HTMLMetaElement[] {
|
||||
if (!attrSelector) return [];
|
||||
const list /*NodeList*/ =
|
||||
this._dom.querySelectorAll(this._dom.defaultDoc(), `meta[${attrSelector}]`);
|
||||
const list /*NodeList*/ = this._dom.querySelectorAll(this._doc, `meta[${attrSelector}]`);
|
||||
return list ? [].slice.call(list) : [];
|
||||
}
|
||||
|
||||
@ -92,7 +95,7 @@ export class Meta {
|
||||
}
|
||||
const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement;
|
||||
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);
|
||||
return element;
|
||||
}
|
||||
|
@ -6,7 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
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.
|
||||
*
|
||||
@ -17,16 +22,18 @@ import {getDOM} from '../dom/dom_adapter';
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@Injectable()
|
||||
export class Title {
|
||||
constructor(@Inject(DOCUMENT) private _doc: any) {}
|
||||
/**
|
||||
* Get the title of the current HTML document.
|
||||
* @returns {string}
|
||||
*/
|
||||
getTitle(): string { return getDOM().getTitle(); }
|
||||
getTitle(): string { return getDOM().getTitle(this._doc); }
|
||||
|
||||
/**
|
||||
* Set the title of the current HTML document.
|
||||
* @param newTitle
|
||||
*/
|
||||
setTitle(newTitle: string) { getDOM().setTitle(newTitle); }
|
||||
setTitle(newTitle: string) { getDOM().setTitle(this._doc, newTitle); }
|
||||
}
|
||||
|
@ -53,8 +53,7 @@ export abstract class DomAdapter {
|
||||
_attrToPropMap: {[key: string]: string};
|
||||
|
||||
abstract parse(templateHtml: string): any /** TODO #9100 */;
|
||||
abstract query(selector: string): any;
|
||||
abstract querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement;
|
||||
abstract querySelector(el: any /** TODO #9100 */, selector: string): any;
|
||||
abstract querySelectorAll(el: any /** TODO #9100 */, selector: string): any[];
|
||||
abstract on(
|
||||
el: any /** TODO #9100 */, evt: any /** TODO #9100 */, listener: any /** TODO #9100 */): any
|
||||
@ -145,10 +144,9 @@ export abstract class DomAdapter {
|
||||
/** TODO #9100 */;
|
||||
abstract templateAwareRoot(el: any /** TODO #9100 */): any /** TODO #9100 */;
|
||||
abstract createHtmlDocument(): HTMLDocument;
|
||||
abstract defaultDoc(): HTMLDocument;
|
||||
abstract getBoundingClientRect(el: any /** TODO #9100 */): any /** TODO #9100 */;
|
||||
abstract getTitle(): string;
|
||||
abstract setTitle(newTitle: string): any /** TODO #9100 */;
|
||||
abstract getTitle(doc: Document): string;
|
||||
abstract setTitle(doc: Document, newTitle: string): any /** TODO #9100 */;
|
||||
abstract elementMatches(n: any /** TODO #9100 */, selector: string): boolean;
|
||||
abstract isTemplateElement(el: any): boolean;
|
||||
abstract isTextNode(node: any /** TODO #9100 */): boolean;
|
||||
@ -164,10 +162,10 @@ export abstract class DomAdapter {
|
||||
/** TODO #9100 */;
|
||||
abstract supportsDOMEvents(): boolean;
|
||||
abstract supportsNativeShadowDOM(): boolean;
|
||||
abstract getGlobalEventTarget(target: string): any;
|
||||
abstract getGlobalEventTarget(doc: Document, target: string): any;
|
||||
abstract getHistory(): History;
|
||||
abstract getLocation(): Location;
|
||||
abstract getBaseHref(): string;
|
||||
abstract getBaseHref(doc: Document): string;
|
||||
abstract resetBaseElement(): void;
|
||||
abstract getUserAgent(): string;
|
||||
abstract setData(element: any /** TODO #9100 */, name: string, value: string): any
|
||||
|
@ -6,11 +6,16 @@
|
||||
* 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';
|
||||
|
||||
@Injectable()
|
||||
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
|
||||
// events.
|
||||
supports(eventName: string): boolean { return true; }
|
||||
|
@ -10,7 +10,6 @@ import {Inject, Injectable, InjectionToken, NgZone} from '@angular/core';
|
||||
|
||||
import {getDOM} from '../dom_adapter';
|
||||
|
||||
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
@ -62,6 +61,8 @@ export class EventManager {
|
||||
}
|
||||
|
||||
export abstract class EventManagerPlugin {
|
||||
constructor(private _doc: any) {}
|
||||
|
||||
manager: EventManager;
|
||||
|
||||
abstract supports(eventName: string): boolean;
|
||||
@ -69,7 +70,7 @@ export abstract class EventManagerPlugin {
|
||||
abstract addEventListener(element: HTMLElement, 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) {
|
||||
throw new Error(`Unsupported event target ${target} for event ${eventName}`);
|
||||
}
|
||||
|
@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
|
||||
import {DOCUMENT} from '../dom_tokens';
|
||||
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
|
||||
const EVENT_NAMES = {
|
||||
@ -85,7 +88,11 @@ export class HammerGestureConfig {
|
||||
|
||||
@Injectable()
|
||||
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 {
|
||||
if (!EVENT_NAMES.hasOwnProperty(eventName.toLowerCase()) && !this.isCustomEvent(eventName)) {
|
||||
|
@ -6,8 +6,11 @@
|
||||
* 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 {DOCUMENT} from '../dom_tokens';
|
||||
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
|
||||
const MODIFIER_KEYS = ['alt', 'control', 'meta', 'shift'];
|
||||
@ -23,7 +26,7 @@ const MODIFIER_KEY_GETTERS: {[key: string]: (event: KeyboardEvent) => boolean} =
|
||||
*/
|
||||
@Injectable()
|
||||
export class KeyEventsPlugin extends EventManagerPlugin {
|
||||
constructor() { super(); }
|
||||
constructor(@Inject(DOCUMENT) doc: any) { super(doc); }
|
||||
|
||||
supports(eventName: string): boolean { return KeyEventsPlugin.parseEventName(eventName) != null; }
|
||||
|
||||
|
@ -6,7 +6,9 @@
|
||||
* 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 {sanitizeStyle} from './style_sanitizer';
|
||||
@ -15,6 +17,7 @@ import {sanitizeUrl} from './url_sanitizer';
|
||||
export {SecurityContext};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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()
|
||||
export class DomSanitizerImpl extends DomSanitizer {
|
||||
constructor(@Inject(DOCUMENT) private _doc: any) { super(); }
|
||||
|
||||
sanitize(ctx: SecurityContext, value: any): string {
|
||||
if (value == null) return null;
|
||||
switch (ctx) {
|
||||
@ -155,7 +160,7 @@ export class DomSanitizerImpl extends DomSanitizer {
|
||||
case SecurityContext.HTML:
|
||||
if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity;
|
||||
this.checkNotSafeValue(value, 'HTML');
|
||||
return sanitizeHtml(String(value));
|
||||
return sanitizeHtml(this._doc, String(value));
|
||||
case SecurityContext.STYLE:
|
||||
if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity;
|
||||
this.checkNotSafeValue(value, 'Style');
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {isDevMode} from '@angular/core';
|
||||
|
||||
import {DomAdapter, getDOM} from '../dom/dom_adapter';
|
||||
import {DOCUMENT} from '../dom/dom_tokens';
|
||||
|
||||
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
|
||||
* the DOM in a browser environment.
|
||||
*/
|
||||
export function sanitizeHtml(unsafeHtmlInput: string): string {
|
||||
export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
try {
|
||||
const containerEl = getInertElement();
|
||||
// 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;
|
||||
DOM.setInnerHTML(containerEl, unsafeHtml);
|
||||
if ((DOM.defaultDoc() as any).documentMode) {
|
||||
if (defaultDoc.documentMode) {
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
stripCustomNsAttrs(containerEl);
|
||||
}
|
||||
|
Reference in New Issue
Block a user