From 047cda5b3c4d0f40121247f8b782c8c84681d1c1 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 13 Feb 2017 15:17:40 -0800 Subject: [PATCH] fix(platform-server): reflect properties to attributes for known elements, for serialization --- .../platform-server/src/parse5_adapter.ts | 2 +- .../platform-server/src/server_renderer.ts | 29 +++++++++++++++++-- .../platform-server/test/integration_spec.ts | 20 +++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/modules/@angular/platform-server/src/parse5_adapter.ts b/modules/@angular/platform-server/src/parse5_adapter.ts index 473f413c8f..8bb791fe71 100644 --- a/modules/@angular/platform-server/src/parse5_adapter.ts +++ b/modules/@angular/platform-server/src/parse5_adapter.ts @@ -367,7 +367,7 @@ export class Parse5DomAdapter extends DomAdapter { return this.querySelectorAll(element, '.' + name); } getElementsByTagName(element: any, name: string): HTMLElement[] { - throw _notImplemented('getElementsByTagName'); + return this.querySelectorAll(element, name); } classList(element: any): string[] { let classAttrValue: any = null; diff --git a/modules/@angular/platform-server/src/server_renderer.ts b/modules/@angular/platform-server/src/server_renderer.ts index 07994c860c..c3c2ac0967 100644 --- a/modules/@angular/platform-server/src/server_renderer.ts +++ b/modules/@angular/platform-server/src/server_renderer.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {DomElementSchemaRegistry} from '@angular/compiler'; import {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, RendererV2, RootRenderer, ViewEncapsulation} from '@angular/core'; import {AnimationDriver, DOCUMENT} from '@angular/platform-browser'; @@ -16,9 +17,13 @@ import {NAMESPACE_URIS, SharedStylesHost, flattenStyles, getDOM, isNamespaced, s const TEMPLATE_COMMENT_TEXT = 'template bindings={}'; const TEMPLATE_BINDINGS_EXP = /^template bindings=(.*)$/; +const EMPTY_ARRAY: any[] = []; + @Injectable() export class ServerRootRenderer implements RootRenderer { protected registeredComponents: Map = new Map(); + private _schema = new DomElementSchemaRegistry(); + constructor( @Inject(DOCUMENT) public document: any, public sharedStylesHost: SharedStylesHost, public animationDriver: AnimationDriver, @Inject(APP_ID) public appId: string, @@ -28,7 +33,7 @@ export class ServerRootRenderer implements RootRenderer { if (!renderer) { renderer = new ServerRenderer( this, componentProto, this.animationDriver, `${this.appId}-${componentProto.id}`, - this._zone); + this._zone, this._schema); this.registeredComponents.set(componentProto.id, renderer); } return renderer; @@ -42,7 +47,8 @@ export class ServerRenderer implements Renderer { constructor( private _rootRenderer: ServerRootRenderer, private componentProto: RenderComponentType, - private _animationDriver: AnimationDriver, styleShimId: string, private _zone: NgZone) { + private _animationDriver: AnimationDriver, styleShimId: string, private _zone: NgZone, + private _schema: DomElementSchemaRegistry) { this._styles = flattenStyles(styleShimId, componentProto.styles, []); if (componentProto.encapsulation === ViewEncapsulation.Native) { throw new Error('Native encapsulation is not supported on the server!'); @@ -141,8 +147,27 @@ export class ServerRenderer implements Renderer { return this.listen(renderElement, name, callback); } + // The value was validated already as a property binding, against the property name. + // To know this value is safe to use as an attribute, the security context of the + // attribute with the given name is checked against that security context of the + // property. + private _isSafeToReflectProperty(tagName: string, propertyName: string): boolean { + return this._schema.securityContext(tagName, propertyName, true) === + this._schema.securityContext(tagName, propertyName, false); + } + setElementProperty(renderElement: any, propertyName: string, propertyValue: any): void { getDOM().setProperty(renderElement, propertyName, propertyValue); + + // Mirror property values for known HTML element properties in the attributes. + const tagName = (renderElement.tagName as string).toLowerCase(); + if (isPresent(propertyValue) && + (typeof propertyValue === 'number' || typeof propertyValue == 'string') && + this._schema.hasElement(tagName, EMPTY_ARRAY) && + this._schema.hasProperty(tagName, propertyName, EMPTY_ARRAY) && + this._isSafeToReflectProperty(tagName, propertyName)) { + this.setElementAttribute(renderElement, propertyName, propertyValue.toString()); + } } setElementAttribute(renderElement: any, attributeName: string, attributeValue: string): void { diff --git a/modules/@angular/platform-server/test/integration_spec.ts b/modules/@angular/platform-server/test/integration_spec.ts index fb9dabae81..74e2ce15f3 100644 --- a/modules/@angular/platform-server/test/integration_spec.ts +++ b/modules/@angular/platform-server/test/integration_spec.ts @@ -90,6 +90,14 @@ export class HttpBeforeExampleModule { export class HttpAfterExampleModule { } +@Component({selector: 'app', template: ``}) +class ImageApp { +} + +@NgModule({declarations: [ImageApp], imports: [ServerModule], bootstrap: [ImageApp]}) +class ImageExampleModule { +} + export function main() { if (getDOM().supportsDOMEvents()) return; // NODE only @@ -143,6 +151,18 @@ export function main() { }); })); + it('copies known properties to attributes', async(() => { + const platform = platformDynamicServer( + [{provide: INITIAL_CONFIG, useValue: {document: ''}}]); + platform.bootstrapModule(ImageExampleModule).then(ref => { + const appRef: ApplicationRef = ref.injector.get(ApplicationRef); + const app = appRef.components[0].location.nativeElement; + const img = getDOM().getElementsByTagName(app, 'img')[0] as any; + expect(img.attribs['src']).toEqual('link'); + }); + })); + + describe('PlatformLocation', () => { it('is injectable', async(() => { const platform = platformDynamicServer(