diff --git a/modules/@angular/core/test/application_ref_spec.ts b/modules/@angular/core/test/application_ref_spec.ts index be4911ee16..ea0376e761 100644 --- a/modules/@angular/core/test/application_ref_spec.ts +++ b/modules/@angular/core/test/application_ref_spec.ts @@ -14,6 +14,7 @@ import {BrowserModule} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {expect} from '@angular/platform-browser/testing/matchers'; +import {ServerModule} from '@angular/platform-server'; import {TestBed, async, inject, withModule} from '../testing'; @@ -49,12 +50,14 @@ export function main() { const errorHandler = new ErrorHandler(false); errorHandler._console = mockConsole as any; + const platformModule = getDOM().supportsDOMEvents() ? BrowserModule : ServerModule; + @NgModule({ providers: [ {provide: ErrorHandler, useValue: errorHandler}, {provide: DOCUMENT, useValue: fakeDoc}, options.providers || [] ], - imports: [BrowserModule], + imports: [platformModule], declarations: [SomeComponent], entryComponents: [SomeComponent], bootstrap: options.bootstrap || [] diff --git a/modules/@angular/platform-browser/src/dom/dom_renderer.ts b/modules/@angular/platform-browser/src/dom/dom_renderer.ts index 7397be4f2f..1c0d440750 100644 --- a/modules/@angular/platform-browser/src/dom/dom_renderer.ts +++ b/modules/@angular/platform-browser/src/dom/dom_renderer.ts @@ -12,13 +12,12 @@ import {isBlank, isPresent, stringify} from '../facade/lang'; import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from '../private_import_core'; import {AnimationDriver} from './animation_driver'; -import {getDOM} from './dom_adapter'; import {DOCUMENT} from './dom_tokens'; import {EventManager} from './events/event_manager'; import {DomSharedStylesHost} from './shared_styles_host'; import {camelCaseToDashCase} from './util'; -const NAMESPACE_URIS = { +export const NAMESPACE_URIS: {[ns: string]: string} = { 'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg', 'xhtml': 'http://www.w3.org/1999/xhtml' @@ -30,12 +29,12 @@ export abstract class DomRootRenderer implements RootRenderer { protected registeredComponents: Map = new Map(); constructor( - public document: any, public eventManager: EventManager, + public document: Document, public eventManager: EventManager, public sharedStylesHost: DomSharedStylesHost, public animationDriver: AnimationDriver, public appId: string) {} renderComponent(componentProto: RenderComponentType): Renderer { - var renderer = this.registeredComponents.get(componentProto.id); + let renderer = this.registeredComponents.get(componentProto.id); if (!renderer) { renderer = new DomRenderer( this, componentProto, this.animationDriver, `${this.appId}-${componentProto.id}`); @@ -63,97 +62,106 @@ export class DomRenderer implements Renderer { constructor( private _rootRenderer: DomRootRenderer, private componentProto: RenderComponentType, private _animationDriver: AnimationDriver, styleShimId: string) { - this._styles = _flattenStyles(styleShimId, componentProto.styles, []); + this._styles = flattenStyles(styleShimId, componentProto.styles, []); if (componentProto.encapsulation !== ViewEncapsulation.Native) { this._rootRenderer.sharedStylesHost.addStyles(this._styles); } if (this.componentProto.encapsulation === ViewEncapsulation.Emulated) { - this._contentAttr = _shimContentAttribute(styleShimId); - this._hostAttr = _shimHostAttribute(styleShimId); + this._contentAttr = shimContentAttribute(styleShimId); + this._hostAttr = shimHostAttribute(styleShimId); } else { this._contentAttr = null; this._hostAttr = null; } } - selectRootElement(selectorOrNode: string|any, debugInfo: RenderDebugInfo): Element { - var el: any /** TODO #9100 */; + selectRootElement(selectorOrNode: string|Element, debugInfo: RenderDebugInfo): Element { + let el: Element; if (typeof selectorOrNode === 'string') { - el = getDOM().querySelector(this._rootRenderer.document, selectorOrNode); - if (isBlank(el)) { + el = this._rootRenderer.document.querySelector(selectorOrNode); + if (!el) { throw new Error(`The selector "${selectorOrNode}" did not match any elements`); } } else { el = selectorOrNode; } - getDOM().clearNodes(el); - return el; - } - - createElement(parent: Element, name: string, debugInfo: RenderDebugInfo): Node { - var nsAndName = splitNamespace(name); - var el = isPresent(nsAndName[0]) ? - getDOM().createElementNS( - (NAMESPACE_URIS as any /** TODO #9100 */)[nsAndName[0]], nsAndName[1]) : - getDOM().createElement(nsAndName[1]); - if (isPresent(this._contentAttr)) { - getDOM().setAttribute(el, this._contentAttr, ''); - } - if (isPresent(parent)) { - getDOM().appendChild(parent, el); + while (el.firstChild) { + el.removeChild(el.firstChild); } return el; } - createViewRoot(hostElement: any): any { - var nodesParent: any /** TODO #9100 */; + createElement(parent: Element|DocumentFragment, name: string, debugInfo: RenderDebugInfo): + Element { + let el: Element; + if (isNamespaced(name)) { + const nsAndName = splitNamespace(name); + el = document.createElementNS((NAMESPACE_URIS)[nsAndName[0]], nsAndName[1]); + } else { + el = document.createElement(name); + } + if (this._contentAttr) { + el.setAttribute(this._contentAttr, ''); + } + if (parent) { + parent.appendChild(el); + } + return el; + } + + createViewRoot(hostElement: Element): Element|DocumentFragment { + let nodesParent: Element|DocumentFragment; if (this.componentProto.encapsulation === ViewEncapsulation.Native) { - nodesParent = getDOM().createShadowRoot(hostElement); + nodesParent = (hostElement as any).createShadowRoot(); this._rootRenderer.sharedStylesHost.addHost(nodesParent); - for (var i = 0; i < this._styles.length; i++) { - getDOM().appendChild(nodesParent, getDOM().createStyleElement(this._styles[i])); + for (let i = 0; i < this._styles.length; i++) { + const styleEl = document.createElement('style'); + styleEl.textContent = this._styles[i]; + nodesParent.appendChild(styleEl); } } else { - if (isPresent(this._hostAttr)) { - getDOM().setAttribute(hostElement, this._hostAttr, ''); + if (this._hostAttr) { + hostElement.setAttribute(this._hostAttr, ''); } nodesParent = hostElement; } return nodesParent; } - createTemplateAnchor(parentElement: any, debugInfo: RenderDebugInfo): any { - var comment = getDOM().createComment(TEMPLATE_COMMENT_TEXT); - if (isPresent(parentElement)) { - getDOM().appendChild(parentElement, comment); + createTemplateAnchor(parentElement: Element|DocumentFragment, debugInfo: RenderDebugInfo): + Comment { + const comment = document.createComment(TEMPLATE_COMMENT_TEXT); + if (parentElement) { + parentElement.appendChild(comment); } return comment; } - createText(parentElement: any, value: string, debugInfo: RenderDebugInfo): any { - var node = getDOM().createTextNode(value); - if (isPresent(parentElement)) { - getDOM().appendChild(parentElement, node); + createText(parentElement: Element|DocumentFragment, value: string, debugInfo: RenderDebugInfo): + any { + const node = document.createTextNode(value); + if (parentElement) { + parentElement.appendChild(node); } return node; } - projectNodes(parentElement: any, nodes: any[]) { - if (isBlank(parentElement)) return; + projectNodes(parentElement: Element|DocumentFragment, nodes: Node[]) { + if (!parentElement) return; appendNodes(parentElement, nodes); } - attachViewAfter(node: any, viewRootNodes: any[]) { moveNodesAfterSibling(node, viewRootNodes); } + attachViewAfter(node: Node, viewRootNodes: Node[]) { moveNodesAfterSibling(node, viewRootNodes); } - detachView(viewRootNodes: any[]) { - for (var i = 0; i < viewRootNodes.length; i++) { - getDOM().remove(viewRootNodes[i]); + detachView(viewRootNodes: (Element|Text|Comment)[]) { + for (let i = 0; i < viewRootNodes.length; i++) { + viewRootNodes[i].remove(); } } - destroyView(hostElement: any, viewAllNodes: any[]) { - if (this.componentProto.encapsulation === ViewEncapsulation.Native && isPresent(hostElement)) { - this._rootRenderer.sharedStylesHost.removeHost(getDOM().getShadowRoot(hostElement)); + destroyView(hostElement: Element|DocumentFragment, viewAllNodes: Node[]) { + if (this.componentProto.encapsulation === ViewEncapsulation.Native && hostElement) { + this._rootRenderer.sharedStylesHost.removeHost((hostElement as any).shadowRoot); } } @@ -167,68 +175,71 @@ export class DomRenderer implements Renderer { target, name, decoratePreventDefault(callback)); } - setElementProperty(renderElement: any, propertyName: string, propertyValue: any): void { - getDOM().setProperty(renderElement, propertyName, propertyValue); + setElementProperty( + renderElement: Element|DocumentFragment, propertyName: string, propertyValue: any): void { + (renderElement as any)[propertyName] = propertyValue; } - setElementAttribute(renderElement: any, attributeName: string, attributeValue: string): void { - var attrNs: any /** TODO #9100 */; - var nsAndName = splitNamespace(attributeName); - if (isPresent(nsAndName[0])) { + setElementAttribute(renderElement: Element, attributeName: string, attributeValue: string): void { + let attrNs: string; + let attrNameWithoutNs = attributeName; + if (isNamespaced(attributeName)) { + const nsAndName = splitNamespace(attributeName); + attrNameWithoutNs = nsAndName[1]; attributeName = nsAndName[0] + ':' + nsAndName[1]; - attrNs = (NAMESPACE_URIS as any /** TODO #9100 */)[nsAndName[0]]; + attrNs = NAMESPACE_URIS[nsAndName[0]]; } if (isPresent(attributeValue)) { - if (isPresent(attrNs)) { - getDOM().setAttributeNS(renderElement, attrNs, attributeName, attributeValue); + if (attrNs) { + renderElement.setAttributeNS(attrNs, attributeName, attributeValue); } else { - getDOM().setAttribute(renderElement, attributeName, attributeValue); + renderElement.setAttribute(attributeName, attributeValue); } } else { if (isPresent(attrNs)) { - getDOM().removeAttributeNS(renderElement, attrNs, nsAndName[1]); + renderElement.removeAttributeNS(attrNs, attrNameWithoutNs); } else { - getDOM().removeAttribute(renderElement, attributeName); + renderElement.removeAttribute(attributeName); } } } - setBindingDebugInfo(renderElement: any, propertyName: string, propertyValue: string): void { - var dashCasedPropertyName = camelCaseToDashCase(propertyName); - if (getDOM().isCommentNode(renderElement)) { + setBindingDebugInfo(renderElement: Element, propertyName: string, propertyValue: string): void { + if (renderElement.nodeType === Node.COMMENT_NODE) { const existingBindings = - getDOM().getText(renderElement).replace(/\n/g, '').match(TEMPLATE_BINDINGS_EXP); - var parsedBindings = JSON.parse(existingBindings[1]); - (parsedBindings as any /** TODO #9100 */)[dashCasedPropertyName] = propertyValue; - getDOM().setText( - renderElement, - TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(parsedBindings, null, 2))); + renderElement.nodeValue.replace(/\n/g, '').match(TEMPLATE_BINDINGS_EXP); + const parsedBindings = JSON.parse(existingBindings[1]); + parsedBindings[propertyName] = propertyValue; + renderElement.nodeValue = + TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(parsedBindings, null, 2)); } else { this.setElementAttribute(renderElement, propertyName, propertyValue); } } - setElementClass(renderElement: any, className: string, isAdd: boolean): void { + setElementClass(renderElement: Element, className: string, isAdd: boolean): void { if (isAdd) { - getDOM().addClass(renderElement, className); + renderElement.classList.add(className); } else { - getDOM().removeClass(renderElement, className); + renderElement.classList.remove(className); } } - setElementStyle(renderElement: any, styleName: string, styleValue: string): void { + setElementStyle(renderElement: HTMLElement, styleName: string, styleValue: string): void { if (isPresent(styleValue)) { - getDOM().setStyle(renderElement, styleName, stringify(styleValue)); + (renderElement.style as any)[styleName] = stringify(styleValue); } else { - getDOM().removeStyle(renderElement, styleName); + // IE requires '' instead of null + // see https://github.com/angular/angular/issues/7916 + (renderElement.style as any)[styleName] = ''; } } - invokeElementMethod(renderElement: any, methodName: string, args: any[]): void { - getDOM().invoke(renderElement, methodName, args); + invokeElementMethod(renderElement: Element, methodName: string, args: any[]): void { + (renderElement as any)[methodName].apply(renderElement, args); } - setText(renderNode: any, text: string): void { getDOM().setText(renderNode, text); } + setText(renderNode: Text, text: string): void { renderNode.nodeValue = text; } animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], @@ -238,57 +249,59 @@ export class DomRenderer implements Renderer { } } -function moveNodesAfterSibling(sibling: any /** TODO #9100 */, nodes: any /** TODO #9100 */) { - var parent = getDOM().parentElement(sibling); - if (nodes.length > 0 && isPresent(parent)) { - var nextSibling = getDOM().nextSibling(sibling); - if (isPresent(nextSibling)) { - for (var i = 0; i < nodes.length; i++) { - getDOM().insertBefore(nextSibling, nodes[i]); +function moveNodesAfterSibling(sibling: Node, nodes: Node[]) { + const parent = sibling.parentElement; + if (nodes.length > 0 && parent) { + const nextSibling = sibling.nextSibling; + if (nextSibling) { + for (let i = 0; i < nodes.length; i++) { + parent.insertBefore(nodes[i], nextSibling); } } else { - for (var i = 0; i < nodes.length; i++) { - getDOM().appendChild(parent, nodes[i]); + for (let i = 0; i < nodes.length; i++) { + parent.appendChild(nodes[i]); } } } } -function appendNodes(parent: any /** TODO #9100 */, nodes: any /** TODO #9100 */) { - for (var i = 0; i < nodes.length; i++) { - getDOM().appendChild(parent, nodes[i]); +function appendNodes(parent: Element | DocumentFragment, nodes: Node[]) { + for (let i = 0; i < nodes.length; i++) { + parent.appendChild(nodes[i]); } } function decoratePreventDefault(eventHandler: Function): Function { - return (event: any /** TODO #9100 */) => { - var allowDefaultBehavior = eventHandler(event); + return (event: any) => { + const allowDefaultBehavior = eventHandler(event); if (allowDefaultBehavior === false) { // TODO(tbosch): move preventDefault into event plugins... - getDOM().preventDefault(event); + event.preventDefault(); + event.returnValue = false; } }; } -var COMPONENT_REGEX = /%COMP%/g; +const COMPONENT_REGEX = /%COMP%/g; export const COMPONENT_VARIABLE = '%COMP%'; export const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; -function _shimContentAttribute(componentShortId: string): string { +export function shimContentAttribute(componentShortId: string): string { return CONTENT_ATTR.replace(COMPONENT_REGEX, componentShortId); } -function _shimHostAttribute(componentShortId: string): string { +export function shimHostAttribute(componentShortId: string): string { return HOST_ATTR.replace(COMPONENT_REGEX, componentShortId); } -function _flattenStyles(compId: string, styles: Array, target: string[]): string[] { +export function flattenStyles( + compId: string, styles: Array, target: string[]): string[] { for (let i = 0; i < styles.length; i++) { let style = styles[i]; if (Array.isArray(style)) { - _flattenStyles(compId, style, target); + flattenStyles(compId, style, target); } else { style = style.replace(COMPONENT_REGEX, compId); target.push(style); @@ -299,10 +312,11 @@ function _flattenStyles(compId: string, styles: Array, target: string const NS_PREFIX_RE = /^:([^:]+):(.+)$/; -function splitNamespace(name: string): string[] { - if (name[0] != ':') { - return [null, name]; - } +export function isNamespaced(name: string) { + return name[0] === ':'; +} + +export function splitNamespace(name: string): string[] { const match = name.match(NS_PREFIX_RE); return [match[1], match[2]]; } diff --git a/modules/@angular/platform-browser/src/dom/shared_styles_host.ts b/modules/@angular/platform-browser/src/dom/shared_styles_host.ts index caa0a678c1..395e9bdb53 100644 --- a/modules/@angular/platform-browser/src/dom/shared_styles_host.ts +++ b/modules/@angular/platform-browser/src/dom/shared_styles_host.ts @@ -8,7 +8,6 @@ import {Inject, Injectable} from '@angular/core'; -import {getDOM} from './dom_adapter'; import {DOCUMENT} from './dom_tokens'; @Injectable() @@ -47,8 +46,9 @@ export class DomSharedStylesHost extends SharedStylesHost { /** @internal */ _addStylesToHost(styles: string[], host: Node) { for (var i = 0; i < styles.length; i++) { - var style = styles[i]; - getDOM().appendChild(host, getDOM().createStyleElement(style)); + const styleEl = document.createElement('style'); + styleEl.textContent = styles[i]; + host.appendChild(styleEl); } } addHost(hostNode: Node) { diff --git a/modules/@angular/platform-browser/src/private_export.ts b/modules/@angular/platform-browser/src/private_export.ts index 3738f46e88..50cd2b4190 100644 --- a/modules/@angular/platform-browser/src/private_export.ts +++ b/modules/@angular/platform-browser/src/private_export.ts @@ -36,6 +36,12 @@ export var __platform_browser_private__: { DomRootRenderer: typeof dom_renderer.DomRootRenderer, _DomRootRenderer_?: dom_renderer.DomRootRenderer, DomRootRenderer_: typeof dom_renderer.DomRootRenderer_, + NAMESPACE_URIS: typeof dom_renderer.NAMESPACE_URIS, + shimContentAttribute: typeof dom_renderer.shimContentAttribute, + shimHostAttribute: typeof dom_renderer.shimHostAttribute, + flattenStyles: typeof dom_renderer.flattenStyles, + splitNamespace: typeof dom_renderer.splitNamespace, + isNamespaced: typeof dom_renderer.isNamespaced, _DomSharedStylesHost?: shared_styles_host.DomSharedStylesHost, DomSharedStylesHost: typeof shared_styles_host.DomSharedStylesHost, _SharedStylesHost?: shared_styles_host.SharedStylesHost, @@ -58,6 +64,12 @@ export var __platform_browser_private__: { setRootDomAdapter: dom_adapter.setRootDomAdapter, DomRootRenderer_: dom_renderer.DomRootRenderer_, DomRootRenderer: dom_renderer.DomRootRenderer, + NAMESPACE_URIS: dom_renderer.NAMESPACE_URIS, + shimContentAttribute: dom_renderer.shimContentAttribute, + shimHostAttribute: dom_renderer.shimHostAttribute, + flattenStyles: dom_renderer.flattenStyles, + splitNamespace: dom_renderer.splitNamespace, + isNamespaced: dom_renderer.isNamespaced, DomSharedStylesHost: shared_styles_host.DomSharedStylesHost, SharedStylesHost: shared_styles_host.SharedStylesHost, ELEMENT_PROBE_PROVIDERS: ng_probe.ELEMENT_PROBE_PROVIDERS, diff --git a/modules/@angular/platform-server/src/private_import_core.ts b/modules/@angular/platform-server/src/private_import_core.ts index 1ee792590f..317204cfdb 100644 --- a/modules/@angular/platform-server/src/private_import_core.ts +++ b/modules/@angular/platform-server/src/private_import_core.ts @@ -11,3 +11,13 @@ import {__core_private__ as r} from '@angular/core'; export var reflector: typeof r.reflector = r.reflector; export var ReflectionCapabilities: typeof r.ReflectionCapabilities = r.ReflectionCapabilities; export var Console: typeof r.Console = r.Console; +export type AnimationKeyframe = typeof r._AnimationKeyframe; +export var AnimationKeyframe: typeof r.AnimationKeyframe = r.AnimationKeyframe; +export type AnimationPlayer = typeof r._AnimationPlayer; +export var AnimationPlayer: typeof r.AnimationPlayer = r.AnimationPlayer; +export type AnimationStyles = typeof r._AnimationStyles; +export var AnimationStyles: typeof r.AnimationStyles = r.AnimationStyles; +export type RenderDebugInfo = typeof r._RenderDebugInfo; +export var RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo; +export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer; +export var DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer; diff --git a/modules/@angular/platform-server/src/private_import_platform-browser.ts b/modules/@angular/platform-server/src/private_import_platform-browser.ts index d15eed3397..be5eac1a17 100644 --- a/modules/@angular/platform-server/src/private_import_platform-browser.ts +++ b/modules/@angular/platform-server/src/private_import_platform-browser.ts @@ -12,3 +12,11 @@ export type DomAdapter = typeof _._DomAdapter; export var DomAdapter: typeof _.DomAdapter = _.DomAdapter; export var setRootDomAdapter: typeof _.setRootDomAdapter = _.setRootDomAdapter; export var getDOM: typeof _.getDOM = _.getDOM; +export var SharedStylesHost: typeof _.SharedStylesHost = _.SharedStylesHost; +export type SharedStylesHost = typeof _.SharedStylesHost; +export var NAMESPACE_URIS: typeof _.NAMESPACE_URIS = _.NAMESPACE_URIS; +export var shimContentAttribute: typeof _.shimContentAttribute = _.shimContentAttribute; +export var shimHostAttribute: typeof _.shimHostAttribute = _.shimHostAttribute; +export var flattenStyles: typeof _.flattenStyles = _.flattenStyles; +export var splitNamespace: typeof _.splitNamespace = _.splitNamespace; +export var isNamespaced: typeof _.isNamespaced = _.isNamespaced; diff --git a/modules/@angular/platform-server/src/server.ts b/modules/@angular/platform-server/src/server.ts index b909eefb5e..85a669c4a9 100644 --- a/modules/@angular/platform-server/src/server.ts +++ b/modules/@angular/platform-server/src/server.ts @@ -8,10 +8,13 @@ import {PlatformLocation} from '@angular/common'; import {platformCoreDynamic} from '@angular/compiler'; -import {NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, createPlatformFactory, platformCore} from '@angular/core'; +import {NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {Parse5DomAdapter} from './parse5_adapter'; +import {DebugDomRootRenderer} from './private_import_core'; +import {SharedStylesHost} from './private_import_platform-browser'; +import {ServerRootRenderer} from './server_renderer'; function notSupported(feature: string): Error { throw new Error(`platform-server does not support '${feature}'.`); @@ -39,12 +42,26 @@ function initParse5Adapter() { Parse5DomAdapter.makeCurrent(); } + +export function _createConditionalRootRenderer(rootRenderer: any) { + if (isDevMode()) { + return new DebugDomRootRenderer(rootRenderer); + } + return rootRenderer; +} + +export const SERVER_RENDER_PROVIDERS: Provider[] = [ + ServerRootRenderer, + {provide: RootRenderer, useFactory: _createConditionalRootRenderer, deps: [ServerRootRenderer]}, + {provide: SharedStylesHost, useClass: SharedStylesHost}, +]; + /** * The ng module for the server. * * @experimental */ -@NgModule({imports: [BrowserModule]}) +@NgModule({imports: [BrowserModule], providers: SERVER_RENDER_PROVIDERS}) export class ServerModule { } diff --git a/modules/@angular/platform-server/src/server_renderer.ts b/modules/@angular/platform-server/src/server_renderer.ts new file mode 100644 index 0000000000..5635a2867a --- /dev/null +++ b/modules/@angular/platform-server/src/server_renderer.ts @@ -0,0 +1,243 @@ +/** + * @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 {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, RootRenderer, ViewEncapsulation} from '@angular/core'; +import {AnimationDriver, DOCUMENT} from '@angular/platform-browser'; + +import {isBlank, isPresent, stringify} from './facade/lang'; +import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from './private_import_core'; +import {NAMESPACE_URIS, SharedStylesHost, flattenStyles, getDOM, isNamespaced, shimContentAttribute, shimHostAttribute, splitNamespace} from './private_import_platform-browser'; + +const TEMPLATE_COMMENT_TEXT = 'template bindings={}'; +const TEMPLATE_BINDINGS_EXP = /^template bindings=(.*)$/; + +@Injectable() +export class ServerRootRenderer { + protected registeredComponents: Map = new Map(); + constructor( + @Inject(DOCUMENT) public document: any, public sharedStylesHost: SharedStylesHost, + public animationDriver: AnimationDriver, @Inject(APP_ID) public appId: string, + private _zone: NgZone) {} + renderComponent(componentProto: RenderComponentType): Renderer { + var renderer = this.registeredComponents.get(componentProto.id); + if (!renderer) { + renderer = new ServerRenderer( + this, componentProto, this.animationDriver, `${this.appId}-${componentProto.id}`, + this._zone); + this.registeredComponents.set(componentProto.id, renderer); + } + return renderer; + } +} + +export class ServerRenderer implements Renderer { + private _contentAttr: string; + private _hostAttr: string; + private _styles: string[]; + + constructor( + private _rootRenderer: ServerRootRenderer, private componentProto: RenderComponentType, + private _animationDriver: AnimationDriver, styleShimId: string, private _zone: NgZone) { + this._styles = flattenStyles(styleShimId, componentProto.styles, []); + if (componentProto.encapsulation === ViewEncapsulation.Native) { + throw new Error('Native encapsulation is not supported on the server!'); + } + if (this.componentProto.encapsulation === ViewEncapsulation.Emulated) { + this._contentAttr = shimContentAttribute(styleShimId); + this._hostAttr = shimHostAttribute(styleShimId); + } else { + this._contentAttr = null; + this._hostAttr = null; + } + } + + selectRootElement(selectorOrNode: string|any, debugInfo: RenderDebugInfo): Element { + var el: any /** TODO #9100 */; + if (typeof selectorOrNode === 'string') { + el = getDOM().querySelector(this._rootRenderer.document, selectorOrNode); + if (isBlank(el)) { + throw new Error(`The selector "${selectorOrNode}" did not match any elements`); + } + } else { + el = selectorOrNode; + } + getDOM().clearNodes(el); + return el; + } + + createElement(parent: Element, name: string, debugInfo: RenderDebugInfo): Node { + var el: any; + if (isNamespaced(name)) { + var nsAndName = splitNamespace(name); + el = getDOM().createElementNS(NAMESPACE_URIS[nsAndName[0]], nsAndName[1]); + } else { + el = getDOM().createElement(name); + } + if (isPresent(this._contentAttr)) { + getDOM().setAttribute(el, this._contentAttr, ''); + } + if (isPresent(parent)) { + getDOM().appendChild(parent, el); + } + return el; + } + + createViewRoot(hostElement: any): any { + var nodesParent: any /** TODO #9100 */; + if (isPresent(this._hostAttr)) { + getDOM().setAttribute(hostElement, this._hostAttr, ''); + } + nodesParent = hostElement; + return nodesParent; + } + + createTemplateAnchor(parentElement: any, debugInfo: RenderDebugInfo): any { + var comment = getDOM().createComment(TEMPLATE_COMMENT_TEXT); + if (isPresent(parentElement)) { + getDOM().appendChild(parentElement, comment); + } + return comment; + } + + createText(parentElement: any, value: string, debugInfo: RenderDebugInfo): any { + var node = getDOM().createTextNode(value); + if (isPresent(parentElement)) { + getDOM().appendChild(parentElement, node); + } + return node; + } + + projectNodes(parentElement: any, nodes: any[]) { + if (isBlank(parentElement)) return; + appendNodes(parentElement, nodes); + } + + attachViewAfter(node: any, viewRootNodes: any[]) { moveNodesAfterSibling(node, viewRootNodes); } + + detachView(viewRootNodes: any[]) { + for (var i = 0; i < viewRootNodes.length; i++) { + getDOM().remove(viewRootNodes[i]); + } + } + + destroyView(hostElement: any, viewAllNodes: any[]) {} + + listen(renderElement: any, name: string, callback: Function): Function { + var outsideHandler = (event: any) => this._zone.runGuarded(() => callback(event)); + return this._zone.runOutsideAngular( + () => getDOM().onAndCancel(renderElement, name, outsideHandler)); + } + + listenGlobal(target: string, name: string, callback: Function): Function { + var renderElement = getDOM().getGlobalEventTarget(target); + return this.listen(renderElement, name, callback); + } + + setElementProperty(renderElement: any, propertyName: string, propertyValue: any): void { + getDOM().setProperty(renderElement, propertyName, propertyValue); + } + + setElementAttribute(renderElement: any, attributeName: string, attributeValue: string): void { + let attrNs: string; + let attrNameWithoutNs = attributeName; + if (isNamespaced(attributeName)) { + const nsAndName = splitNamespace(attributeName); + attrNameWithoutNs = nsAndName[1]; + attributeName = nsAndName[0] + ':' + nsAndName[1]; + attrNs = NAMESPACE_URIS[nsAndName[0]]; + } + if (isPresent(attributeValue)) { + if (isPresent(attrNs)) { + getDOM().setAttributeNS(renderElement, attrNs, attributeName, attributeValue); + } else { + getDOM().setAttribute(renderElement, attributeName, attributeValue); + } + } else { + if (isPresent(attrNs)) { + getDOM().removeAttributeNS(renderElement, attrNs, attrNameWithoutNs); + } else { + getDOM().removeAttribute(renderElement, attributeName); + } + } + } + + setBindingDebugInfo(renderElement: any, propertyName: string, propertyValue: string): void { + if (getDOM().isCommentNode(renderElement)) { + const existingBindings = + getDOM().getText(renderElement).replace(/\n/g, '').match(TEMPLATE_BINDINGS_EXP); + var parsedBindings = JSON.parse(existingBindings[1]); + (parsedBindings as any /** TODO #9100 */)[propertyName] = propertyValue; + getDOM().setText( + renderElement, + TEMPLATE_COMMENT_TEXT.replace('{}', JSON.stringify(parsedBindings, null, 2))); + } else { + this.setElementAttribute(renderElement, propertyName, propertyValue); + } + } + + setElementClass(renderElement: any, className: string, isAdd: boolean): void { + if (isAdd) { + getDOM().addClass(renderElement, className); + } else { + getDOM().removeClass(renderElement, className); + } + } + + setElementStyle(renderElement: any, styleName: string, styleValue: string): void { + if (isPresent(styleValue)) { + getDOM().setStyle(renderElement, styleName, stringify(styleValue)); + } else { + getDOM().removeStyle(renderElement, styleName); + } + } + + invokeElementMethod(renderElement: any, methodName: string, args: any[]): void { + getDOM().invoke(renderElement, methodName, args); + } + + setText(renderNode: any, text: string): void { getDOM().setText(renderNode, text); } + + animate( + element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], + duration: number, delay: number, easing: string): AnimationPlayer { + return this._animationDriver.animate( + element, startingStyles, keyframes, duration, delay, easing); + } +} + +function moveNodesAfterSibling(sibling: any /** TODO #9100 */, nodes: any /** TODO #9100 */) { + var parent = getDOM().parentElement(sibling); + if (nodes.length > 0 && isPresent(parent)) { + var nextSibling = getDOM().nextSibling(sibling); + if (isPresent(nextSibling)) { + for (var i = 0; i < nodes.length; i++) { + getDOM().insertBefore(nextSibling, nodes[i]); + } + } else { + for (var i = 0; i < nodes.length; i++) { + getDOM().appendChild(parent, nodes[i]); + } + } + } +} + +function appendNodes(parent: any /** TODO #9100 */, nodes: any /** TODO #9100 */) { + for (var i = 0; i < nodes.length; i++) { + getDOM().appendChild(parent, nodes[i]); + } +} + +function decoratePreventDefault(eventHandler: Function): Function { + return (event: any /** TODO #9100 */) => { + var allowDefaultBehavior = eventHandler(event); + if (allowDefaultBehavior === false) { + // TODO(tbosch): move preventDefault into event plugins... + getDOM().preventDefault(event); + } + }; +} diff --git a/modules/@angular/platform-server/testing/server.ts b/modules/@angular/platform-server/testing/server.ts index c52a4220c2..52c41beb20 100644 --- a/modules/@angular/platform-server/testing/server.ts +++ b/modules/@angular/platform-server/testing/server.ts @@ -9,6 +9,9 @@ import {platformCoreDynamicTesting} from '@angular/compiler/testing'; import {NgModule, PlatformRef, Provider, createPlatformFactory} from '@angular/core'; import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; + +import {SERVER_RENDER_PROVIDERS} from '../src/server'; + import {INTERNAL_SERVER_PLATFORM_PROVIDERS} from './private_import_platform_server'; @@ -25,6 +28,6 @@ export const platformServerTesting = createPlatformFactory( * * @experimental API related to bootstrapping are still under review. */ -@NgModule({exports: [BrowserDynamicTestingModule]}) +@NgModule({exports: [BrowserDynamicTestingModule], providers: SERVER_RENDER_PROVIDERS}) export class ServerTestingModule { } diff --git a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_integration_spec.ts b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_integration_spec.ts index 755b099921..c5804ab095 100644 --- a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_integration_spec.ts +++ b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_integration_spec.ts @@ -57,6 +57,9 @@ export function main() { } describe('Web Worker Renderer', () => { + // Don't run on server... + if (!getDOM().supportsDOMEvents()) return; + var uiInjector: Injector; var uiRenderStore: RenderStore; var workerRenderStore: RenderStore;