From 71cbb49672c820b8b681d2c6f14b545da34e3757 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 11 Sep 2015 13:35:46 -0700 Subject: [PATCH] refactor(compiler): allow to serialize and deserialize `DirectiveMetadata` --- modules/angular2/src/compiler/api.ts | 147 ++++++++++++---- modules/angular2/src/compiler/html_ast.ts | 18 +- modules/angular2/src/compiler/html_parser.ts | 41 ++++- .../angular2/src/compiler/template_loader.ts | 22 ++- .../core/change_detection/change_detection.ts | 2 +- .../src/core/change_detection/constants.ts | 24 ++- modules/angular2/src/core/render/api.ts | 6 + modules/angular2/test/compiler/api_spec.ts | 108 ++++++++++++ .../test/compiler/html_parser_spec.ts | 165 ++++++++++-------- .../test/compiler/template_loader_spec.ts | 67 ++++--- 10 files changed, 443 insertions(+), 157 deletions(-) create mode 100644 modules/angular2/test/compiler/api_spec.ts diff --git a/modules/angular2/src/compiler/api.ts b/modules/angular2/src/compiler/api.ts index 0440e4e792..bcbb8a07f9 100644 --- a/modules/angular2/src/compiler/api.ts +++ b/modules/angular2/src/compiler/api.ts @@ -1,19 +1,36 @@ -import {isPresent, normalizeBool} from 'angular2/src/core/facade/lang'; -import {HtmlAst} from './html_ast'; -import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection/change_detection'; +import {isPresent, normalizeBool, serializeEnum, Type} from 'angular2/src/core/facade/lang'; +import { + ChangeDetectionStrategy, + changeDetectionStrategyFromJson +} from 'angular2/src/core/change_detection/change_detection'; +import {ViewEncapsulation, viewEncapsulationFromJson} from 'angular2/src/core/render/api'; export class TypeMetadata { id: number; - type: any; + type: Type; typeName: string; typeUrl: string; constructor({id, type, typeName, typeUrl}: - {id?: number, type?: string, typeName?: string, typeUrl?: string} = {}) { + {id?: number, type?: Type, typeName?: string, typeUrl?: string} = {}) { this.id = id; this.type = type; this.typeName = typeName; this.typeUrl = typeUrl; } + + static fromJson(data: StringMap): TypeMetadata { + return new TypeMetadata( + {id: data['id'], type: data['type'], typeName: data['typeName'], typeUrl: data['typeUrl']}); + } + + toJson(): StringMap { + return { + // Note: Runtime type can't be serialized... + 'id': this.id, + 'typeName': this.typeName, + 'typeUrl': this.typeUrl + }; + } } export class ChangeDetectionMetadata { @@ -44,7 +61,7 @@ export class ChangeDetectionMetadata { callOnChanges?: boolean, callDoCheck?: boolean, callOnInit?: boolean - }) { + } = {}) { this.changeDetection = changeDetection; this.properties = properties; this.events = events; @@ -58,60 +75,102 @@ export class ChangeDetectionMetadata { this.callDoCheck = callDoCheck; this.callOnInit = callOnInit; } + + static fromJson(data: StringMap): ChangeDetectionMetadata { + return new ChangeDetectionMetadata({ + changeDetection: isPresent(data['changeDetection']) ? + changeDetectionStrategyFromJson(data['changeDetection']) : + data['changeDetection'], + properties: data['properties'], + events: data['events'], + hostListeners: data['hostListeners'], + hostProperties: data['hostProperties'], + callAfterContentInit: data['callAfterContentInit'], + callAfterContentChecked: data['callAfterContentChecked'], + callAfterViewInit: data['callAfterViewInit'], + callAfterViewChecked: data['callAfterViewChecked'], + callOnChanges: data['callOnChanges'], + callDoCheck: data['callDoCheck'], + callOnInit: data['callOnInit'] + }); + } + + toJson(): StringMap { + return { + 'changeDetection': isPresent(this.changeDetection) ? serializeEnum(this.changeDetection) : + this.changeDetection, + 'properties': this.properties, + 'events': this.events, + 'hostListeners': this.hostListeners, + 'hostProperties': this.hostProperties, + 'callAfterContentInit': this.callAfterContentInit, + 'callAfterContentChecked': this.callAfterContentChecked, + 'callAfterViewInit': this.callAfterViewInit, + 'callAfterViewChecked': this.callAfterViewChecked, + 'callOnChanges': this.callOnChanges, + 'callDoCheck': this.callDoCheck, + 'callOnInit': this.callOnInit + }; + } } export class TemplateMetadata { encapsulation: ViewEncapsulation; - nodes: HtmlAst[]; + template: string; styles: string[]; styleAbsUrls: string[]; ngContentSelectors: string[]; - constructor({encapsulation, nodes, styles, styleAbsUrls, ngContentSelectors}: { + constructor({encapsulation, template, styles, styleAbsUrls, ngContentSelectors}: { encapsulation?: ViewEncapsulation, - nodes?: HtmlAst[], + template?: string, styles?: string[], styleAbsUrls?: string[], ngContentSelectors?: string[] - }) { + } = {}) { this.encapsulation = encapsulation; - this.nodes = nodes; + this.template = template; this.styles = styles; this.styleAbsUrls = styleAbsUrls; this.ngContentSelectors = ngContentSelectors; } + + static fromJson(data: StringMap):TemplateMetadata { + return new TemplateMetadata({ + encapsulation: isPresent(data['encapsulation']) ? + viewEncapsulationFromJson(data['encapsulation']) : + data['encapsulation'], + template: data['template'], + styles: data['styles'], + styleAbsUrls: data['styleAbsUrls'], + ngContentSelectors: data['ngContentSelectors'], + }); + } + + toJson(): StringMap { + return { + 'encapsulation': + isPresent(this.encapsulation) ? serializeEnum(this.encapsulation) : this.encapsulation, + 'template': this.template, + 'styles': this.styles, + 'styleAbsUrls': this.styleAbsUrls, + 'ngContentSelectors': this.ngContentSelectors, + }; + } } -/** - * How the template and styles of a view should be encapsulated. - */ -export enum ViewEncapsulation { - /** - * Emulate scoping of styles by preprocessing the style rules - * and adding additional attributes to elements. This is the default. - */ - Emulated, - /** - * Uses the native mechanism of the renderer. For the DOM this means creating a ShadowRoot. - */ - Native, - /** - * Don't scope the template nor the styles. - */ - None -} export class DirectiveMetadata { type: TypeMetadata; isComponent: boolean; selector: string; - hostAttributes: Map; + hostAttributes: StringMap; changeDetection: ChangeDetectionMetadata; template: TemplateMetadata; constructor({type, isComponent, selector, hostAttributes, changeDetection, template}: { type?: TypeMetadata, isComponent?: boolean, selector?: string, - hostAttributes?: Map, + hostAttributes?: StringMap, changeDetection?: ChangeDetectionMetadata, template?: TemplateMetadata } = {}) { @@ -122,6 +181,32 @@ export class DirectiveMetadata { this.changeDetection = changeDetection; this.template = template; } + + static fromJson(data: StringMap): DirectiveMetadata { + return new DirectiveMetadata({ + type: isPresent(data['type']) ? TypeMetadata.fromJson(data['type']) : data['type'], + isComponent: data['isComponent'], + selector: data['selector'], + hostAttributes: data['hostAttributes'], + changeDetection: isPresent(data['changeDetection']) ? + ChangeDetectionMetadata.fromJson(data['changeDetection']) : + data['changeDetection'], + template: isPresent(data['template']) ? TemplateMetadata.fromJson(data['template']) : + data['template'] + }); + } + + toJson(): StringMap { + return { + 'type': isPresent(this.type) ? this.type.toJson() : this.type, + 'isComponent': this.isComponent, + 'selector': this.selector, + 'hostAttributes': this.hostAttributes, + 'changeDetection': + isPresent(this.changeDetection) ? this.changeDetection.toJson() : this.changeDetection, + 'template': isPresent(this.template) ? this.template.toJson() : this.template + }; + } } export class SourceModule { diff --git a/modules/angular2/src/compiler/html_ast.ts b/modules/angular2/src/compiler/html_ast.ts index 0f64ca4f74..067e6fa6ea 100644 --- a/modules/angular2/src/compiler/html_ast.ts +++ b/modules/angular2/src/compiler/html_ast.ts @@ -2,35 +2,35 @@ import {isPresent} from 'angular2/src/core/facade/lang'; export interface HtmlAst { sourceInfo: string; - visit(visitor: HtmlAstVisitor): any; + visit(visitor: HtmlAstVisitor, context: any): any; } export class HtmlTextAst implements HtmlAst { constructor(public value: string, public sourceInfo: string) {} - visit(visitor: HtmlAstVisitor): any { return visitor.visitText(this); } + visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); } } export class HtmlAttrAst implements HtmlAst { constructor(public name: string, public value: string, public sourceInfo: string) {} - visit(visitor: HtmlAstVisitor): any { return visitor.visitAttr(this); } + visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); } } export class HtmlElementAst implements HtmlAst { constructor(public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[], public sourceInfo: string) {} - visit(visitor: HtmlAstVisitor): any { return visitor.visitElement(this); } + visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); } } export interface HtmlAstVisitor { - visitElement(ast: HtmlElementAst): any; - visitAttr(ast: HtmlAttrAst): any; - visitText(ast: HtmlTextAst): any; + visitElement(ast: HtmlElementAst, context: any): any; + visitAttr(ast: HtmlAttrAst, context: any): any; + visitText(ast: HtmlTextAst, context: any): any; } -export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[]): any[] { +export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] { var result = []; asts.forEach(ast => { - var astResult = ast.visit(visitor); + var astResult = ast.visit(visitor, context); if (isPresent(astResult)) { result.push(astResult); } diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 654da00937..f5c0276b3a 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -7,7 +7,16 @@ import { } from 'angular2/src/core/facade/lang'; import {DOM} from 'angular2/src/core/dom/dom_adapter'; -import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlElementAst} from './html_ast'; +import { + HtmlAst, + HtmlAttrAst, + HtmlTextAst, + HtmlElementAst, + HtmlAstVisitor, + htmlVisitAll +} from './html_ast'; + +import {escapeDoubleQuoteString} from './util'; const NG_NON_BINDABLE = 'ng-non-bindable'; @@ -16,6 +25,12 @@ export class HtmlParser { var root = DOM.createTemplate(template); return parseChildNodes(root, sourceInfo); } + unparse(nodes: HtmlAst[]): string { + var visitor = new UnparseVisitor(); + var parts = []; + htmlVisitAll(visitor, nodes, parts); + return parts.join(''); + } } function parseText(text: Text, indexInParent: number, parentSourceInfo: string): HtmlTextAst { @@ -92,3 +107,27 @@ function ignoreChildren(attrs: HtmlAttrAst[]): boolean { } return false; } + +class UnparseVisitor implements HtmlAstVisitor { + visitElement(ast: HtmlElementAst, parts: string[]): any { + parts.push(`<${ast.name}`); + var attrs = []; + htmlVisitAll(this, ast.attrs, attrs); + if (ast.attrs.length > 0) { + parts.push(' '); + parts.push(attrs.join(' ')); + } + parts.push(`>`); + htmlVisitAll(this, ast.children, parts); + parts.push(``); + return null; + } + visitAttr(ast: HtmlAttrAst, parts: string[]): any { + parts.push(`${ast.name}=${escapeDoubleQuoteString(ast.value)}`); + return null; + } + visitText(ast: HtmlTextAst, parts: string[]): any { + parts.push(ast.value); + return null; + } +} diff --git a/modules/angular2/src/compiler/template_loader.ts b/modules/angular2/src/compiler/template_loader.ts index d835d8e8d1..c9d28278d6 100644 --- a/modules/angular2/src/compiler/template_loader.ts +++ b/modules/angular2/src/compiler/template_loader.ts @@ -1,5 +1,6 @@ -import {TypeMetadata, TemplateMetadata, ViewEncapsulation} from './api'; -import {isPresent} from 'angular2/src/core/facade/lang'; +import {TypeMetadata, TemplateMetadata} from './api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; +import {isPresent, isBlank} from 'angular2/src/core/facade/lang'; import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async'; import {XHR} from 'angular2/src/core/render/xhr'; @@ -61,7 +62,7 @@ export class TemplateLoader { allStyleUrls.map(styleUrl => this._urlResolver.resolve(templateSourceUrl, styleUrl)); return new TemplateMetadata({ encapsulation: encapsulation, - nodes: remainingNodes, + template: this._domParser.unparse(remainingNodes), styles: allResolvedStyles, styleAbsUrls: allStyleAbsUrls, ngContentSelectors: visitor.ngContentSelectors @@ -74,7 +75,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor { styles: string[] = []; styleUrls: string[] = []; - visitElement(ast: HtmlElementAst): HtmlElementAst { + visitElement(ast: HtmlElementAst, context: any): HtmlElementAst { var selectAttr = null; var hrefAttr = null; var relAttr = null; @@ -90,7 +91,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor { var nodeName = ast.name; var keepElement = true; if (nodeName == NG_CONTENT_ELEMENT) { - this.ngContentSelectors.push(selectAttr); + this.ngContentSelectors.push(normalizeNgContentSelect(selectAttr)); } else if (nodeName == STYLE_ELEMENT) { keepElement = false; var textContent = ''; @@ -111,6 +112,13 @@ class TemplatePreparseVisitor implements HtmlAstVisitor { return null; } } - visitAttr(ast: HtmlAttrAst): HtmlAttrAst { return ast; } - visitText(ast: HtmlTextAst): HtmlTextAst { return ast; } + visitAttr(ast: HtmlAttrAst, context: any): HtmlAttrAst { return ast; } + visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; } } + +function normalizeNgContentSelect(selectAttr: string): string { + if (isBlank(selectAttr) || selectAttr.length === 0) { + return '*'; + } + return selectAttr; +} \ No newline at end of file diff --git a/modules/angular2/src/core/change_detection/change_detection.ts b/modules/angular2/src/core/change_detection/change_detection.ts index d4a5d05a05..25ebafd691 100644 --- a/modules/angular2/src/core/change_detection/change_detection.ts +++ b/modules/angular2/src/core/change_detection/change_detection.ts @@ -34,7 +34,7 @@ export { DebugContext, ChangeDetectorGenConfig } from './interfaces'; -export {ChangeDetectionStrategy} from './constants'; +export {ChangeDetectionStrategy, changeDetectionStrategyFromJson} from './constants'; export {DynamicProtoChangeDetector} from './proto_change_detector'; export {BindingRecord, BindingTarget} from './binding_record'; export {DirectiveIndex, DirectiveRecord} from './directive_record'; diff --git a/modules/angular2/src/core/change_detection/constants.ts b/modules/angular2/src/core/change_detection/constants.ts index c0c8b2ec0c..64df160df2 100644 --- a/modules/angular2/src/core/change_detection/constants.ts +++ b/modules/angular2/src/core/change_detection/constants.ts @@ -1,5 +1,11 @@ -// TODO:vsavkin Use enums after switching to TypeScript -import {StringWrapper, normalizeBool, isBlank} from 'angular2/src/core/facade/lang'; +import { + StringWrapper, + normalizeBool, + isBlank, + serializeEnum, + deserializeEnum +} from 'angular2/src/core/facade/lang'; +import {MapWrapper} from 'angular2/src/core/facade/collection'; export enum ChangeDetectionStrategy { /** @@ -42,6 +48,20 @@ export enum ChangeDetectionStrategy { OnPushObserve } +var strategyMap: Map = MapWrapper.createFromPairs([ + [0, ChangeDetectionStrategy.CheckOnce], + [1, ChangeDetectionStrategy.Checked], + [2, ChangeDetectionStrategy.CheckAlways], + [3, ChangeDetectionStrategy.Detached], + [4, ChangeDetectionStrategy.OnPush], + [5, ChangeDetectionStrategy.Default], + [6, ChangeDetectionStrategy.OnPushObserve] +]); + +export function changeDetectionStrategyFromJson(value: number): ChangeDetectionStrategy { + return deserializeEnum(value, strategyMap); +} + export function isDefaultChangeDetectionStrategy(changeDetectionStrategy: ChangeDetectionStrategy): boolean { return isBlank(changeDetectionStrategy) || diff --git a/modules/angular2/src/core/render/api.ts b/modules/angular2/src/core/render/api.ts index b52339162c..0b6729e303 100644 --- a/modules/angular2/src/core/render/api.ts +++ b/modules/angular2/src/core/render/api.ts @@ -304,6 +304,12 @@ export enum ViewEncapsulation { None } +var encapsulationMap: Map = MapWrapper.createFromPairs( + [[0, ViewEncapsulation.Emulated], [1, ViewEncapsulation.Native], [2, ViewEncapsulation.None]]); +export function viewEncapsulationFromJson(value: number): ViewEncapsulation { + return deserializeEnum(value, encapsulationMap); +} + export class ViewDefinition { componentId: string; templateAbsUrl: string; diff --git a/modules/angular2/test/compiler/api_spec.ts b/modules/angular2/test/compiler/api_spec.ts new file mode 100644 index 0000000000..5c9556ff80 --- /dev/null +++ b/modules/angular2/test/compiler/api_spec.ts @@ -0,0 +1,108 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + el, + expect, + iit, + inject, + it, + xit, + TestComponentBuilder +} from 'angular2/test_lib'; + +import { + DirectiveMetadata, + TypeMetadata, + TemplateMetadata, + ChangeDetectionMetadata +} from 'angular2/src/compiler/api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; +import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection'; + +export function main() { + describe('Compiler api', () => { + var fullTypeMeta: TypeMetadata; + var fullTemplateMeta: TemplateMetadata; + var fullChangeDetectionMeta: ChangeDetectionMetadata; + var fullDirectiveMeta: DirectiveMetadata; + + beforeEach(() => { + fullTypeMeta = new TypeMetadata({id: 23, typeName: 'SomeType', typeUrl: 'someUrl'}); + fullTemplateMeta = new TemplateMetadata({ + encapsulation: ViewEncapsulation.Emulated, + template: '', + styles: ['someStyle'], + styleAbsUrls: ['someStyleUrl'], + ngContentSelectors: ['*'] + }); + fullChangeDetectionMeta = new ChangeDetectionMetadata({ + changeDetection: ChangeDetectionStrategy.Default, + properties: ['someProp'], + events: ['someEvent'], + hostListeners: {'event1': 'handler1'}, + hostProperties: {'prop1': 'expr1'}, + callAfterContentInit: true, + callAfterContentChecked: true, + callAfterViewInit: true, + callAfterViewChecked: true, + callOnChanges: true, + callDoCheck: true, + callOnInit: true + }); + fullDirectiveMeta = new DirectiveMetadata({ + selector: 'someSelector', + isComponent: true, + hostAttributes: {'attr1': 'attrValue2'}, + type: fullTypeMeta, template: fullTemplateMeta, + changeDetection: fullChangeDetectionMeta, + }); + + }); + + describe('DirectiveMetadata', () => { + it('should serialize with full data', () => { + expect(DirectiveMetadata.fromJson(fullDirectiveMeta.toJson())).toEqual(fullDirectiveMeta); + }); + + it('should serialize with no data', () => { + var empty = new DirectiveMetadata(); + expect(DirectiveMetadata.fromJson(empty.toJson())).toEqual(empty); + }); + }); + + describe('TypeMetadata', () => { + it('should serialize with full data', + () => { expect(TypeMetadata.fromJson(fullTypeMeta.toJson())).toEqual(fullTypeMeta); }); + + it('should serialize with no data', () => { + var empty = new TypeMetadata(); + expect(TypeMetadata.fromJson(empty.toJson())).toEqual(empty); + }); + }); + + describe('TemplateMetadata', () => { + it('should serialize with full data', () => { + expect(TemplateMetadata.fromJson(fullTemplateMeta.toJson())).toEqual(fullTemplateMeta); + }); + + it('should serialize with no data', () => { + var empty = new TemplateMetadata(); + expect(TemplateMetadata.fromJson(empty.toJson())).toEqual(empty); + }); + }); + + describe('ChangeDetectionMetadata', () => { + it('should serialize with full data', () => { + expect(ChangeDetectionMetadata.fromJson(fullChangeDetectionMeta.toJson())) + .toEqual(fullChangeDetectionMeta); + }); + + it('should serialize with no data', () => { + var empty = new ChangeDetectionMetadata(); + expect(ChangeDetectionMetadata.fromJson(empty.toJson())).toEqual(empty); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/angular2/test/compiler/html_parser_spec.ts b/modules/angular2/test/compiler/html_parser_spec.ts index b252181bfd..3824bcaf98 100644 --- a/modules/angular2/test/compiler/html_parser_spec.ts +++ b/modules/angular2/test/compiler/html_parser_spec.ts @@ -15,89 +15,114 @@ export function main() { var parser: HtmlParser; beforeEach(() => { parser = new HtmlParser(); }); - describe('text nodes', () => { - it('should parse root level text nodes', () => { - expect(humanizeDom(parser.parse('a', 'TestComp'))) - .toEqual([[HtmlTextAst, 'a', 'TestComp > #text(a):nth-child(0)']]); + describe('parse', () => { + + describe('text nodes', () => { + it('should parse root level text nodes', () => { + expect(humanizeDom(parser.parse('a', 'TestComp'))) + .toEqual([[HtmlTextAst, 'a', 'TestComp > #text(a):nth-child(0)']]); + }); + + it('should parse text nodes inside regular elements', () => { + expect(humanizeDom(parser.parse('
a
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], + [HtmlTextAst, 'a', 'TestComp > div:nth-child(0) > #text(a):nth-child(0)'] + ]); + }); + + it('should parse text nodes inside template elements', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], + [HtmlTextAst, 'a', 'TestComp > template:nth-child(0) > #text(a):nth-child(0)'] + ]); + }); }); - it('should parse text nodes inside regular elements', () => { - expect(humanizeDom(parser.parse('
a
', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], - [HtmlTextAst, 'a', 'TestComp > div:nth-child(0) > #text(a):nth-child(0)'] - ]); + describe('elements', () => { + it('should parse root level elements', () => { + expect(humanizeDom(parser.parse('
', 'TestComp'))) + .toEqual([[HtmlElementAst, 'div', 'TestComp > div:nth-child(0)']]); + }); + + it('should parse elements inside of regular elements', () => { + expect(humanizeDom(parser.parse('
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], + [HtmlElementAst, 'span', 'TestComp > div:nth-child(0) > span:nth-child(0)'] + ]); + }); + + it('should parse elements inside of template elements', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], + [HtmlElementAst, 'span', 'TestComp > template:nth-child(0) > span:nth-child(0)'] + ]); + }); }); - it('should parse text nodes inside template elements', () => { - expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], - [HtmlTextAst, 'a', 'TestComp > template:nth-child(0) > #text(a):nth-child(0)'] - ]); + describe('attributes', () => { + it('should parse attributes on regular elements', () => { + expect(humanizeDom(parser.parse('
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], + [HtmlAttrAst, 'k', 'v', 'TestComp > div:nth-child(0)[k=v]'] + ]); + }); + + it('should parse attributes on template elements', () => { + expect(humanizeDom(parser.parse('', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], + [HtmlAttrAst, 'k', 'v', 'TestComp > template:nth-child(0)[k=v]'] + ]); + }); + }); + + describe('ng-non-bindable', () => { + it('should ignore text nodes and elements inside of elements with ng-non-bindable', () => { + expect(humanizeDom( + parser.parse('
hello
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], + [ + HtmlAttrAst, + 'ng-non-bindable', + '', + 'TestComp > div:nth-child(0)[ng-non-bindable=]' + ] + ]); + }); }); }); - describe('elements', () => { - it('should parse root level elements', () => { - expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div', 'TestComp > div:nth-child(0)']]); + describe('unparse', () => { + it('should unparse text nodes', + () => { expect(parser.unparse(parser.parse('a', null))).toEqual('a'); }); + + it('should unparse elements', + () => { expect(parser.unparse(parser.parse('', null))).toEqual(''); }); + + it('should unparse attributes', () => { + expect(parser.unparse(parser.parse('
', null))) + .toEqual('
'); }); - it('should parse elements inside of regular elements', () => { - expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], - [HtmlElementAst, 'span', 'TestComp > div:nth-child(0) > span:nth-child(0)'] - ]); + it('should unparse nested elements', () => { + expect(parser.unparse(parser.parse('
', null))) + .toEqual('
'); }); - it('should parse elements inside of template elements', () => { - expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], - [HtmlElementAst, 'span', 'TestComp > template:nth-child(0) > span:nth-child(0)'] - ]); - }); - }); - - describe('attributes', () => { - it('should parse attributes on regular elements', () => { - expect(humanizeDom(parser.parse('
', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], - [HtmlAttrAst, 'k', 'v', 'TestComp > div:nth-child(0)[k=v]'] - ]); - }); - - it('should parse attributes on template elements', () => { - expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'template', 'TestComp > template:nth-child(0)'], - [HtmlAttrAst, 'k', 'v', 'TestComp > template:nth-child(0)[k=v]'] - ]); - }); - }); - - describe('ng-non-bindable', () => { - it('should ignore text nodes and elements inside of elements with ng-non-bindable', () => { - expect( - humanizeDom(parser.parse('
hello
', 'TestComp'))) - .toEqual([ - [HtmlElementAst, 'div', 'TestComp > div:nth-child(0)'], - [ - HtmlAttrAst, - 'ng-non-bindable', - '', - 'TestComp > div:nth-child(0)[ng-non-bindable=]' - ] - ]); + it('should unparse nested text nodes', () => { + expect(parser.unparse(parser.parse('
a
', null))).toEqual('
a
'); }); }); }); } -export function humanizeDom(asts: HtmlAst[]): any[] { +function humanizeDom(asts: HtmlAst[]): any[] { var humanizer = new Humanizer(); htmlVisitAll(humanizer, asts); return humanizer.result; @@ -105,17 +130,17 @@ export function humanizeDom(asts: HtmlAst[]): any[] { class Humanizer implements HtmlAstVisitor { result: any[] = []; - visitElement(ast: HtmlElementAst): any { + visitElement(ast: HtmlElementAst, context: any): any { this.result.push([HtmlElementAst, ast.name, ast.sourceInfo]); htmlVisitAll(this, ast.attrs); htmlVisitAll(this, ast.children); return null; } - visitAttr(ast: HtmlAttrAst): any { + visitAttr(ast: HtmlAttrAst, context: any): any { this.result.push([HtmlAttrAst, ast.name, ast.value, ast.sourceInfo]); return null; } - visitText(ast: HtmlTextAst): any { + visitText(ast: HtmlTextAst, context: any): any { this.result.push([HtmlTextAst, ast.value, ast.sourceInfo]); return null; } diff --git a/modules/angular2/test/compiler/template_loader_spec.ts b/modules/angular2/test/compiler/template_loader_spec.ts index be96316911..94a648e01d 100644 --- a/modules/angular2/test/compiler/template_loader_spec.ts +++ b/modules/angular2/test/compiler/template_loader_spec.ts @@ -13,12 +13,11 @@ import { } from 'angular2/test_lib'; import {HtmlParser} from 'angular2/src/compiler/html_parser'; -import {TypeMetadata, ViewEncapsulation, TemplateMetadata} from 'angular2/src/compiler/api'; +import {TypeMetadata, TemplateMetadata} from 'angular2/src/compiler/api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; import {TemplateLoader} from 'angular2/src/compiler/template_loader'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; -import {humanizeDom} from './html_parser_spec'; -import {HtmlTextAst, HtmlElementAst, HtmlAttrAst} from 'angular2/src/compiler/html_ast'; import {XHR} from 'angular2/src/core/render/xhr'; import {MockXHR} from 'angular2/src/core/render/xhr_mock'; @@ -27,21 +26,22 @@ export function main() { var loader: TemplateLoader; var dirType: TypeMetadata; var xhr: MockXHR; + var htmlParser: HtmlParser; + beforeEach(inject([XHR], (mockXhr) => { xhr = mockXhr; - var urlResolver = new UrlResolver(); - loader = new TemplateLoader(xhr, urlResolver, new HtmlParser()); + htmlParser = new HtmlParser(); + loader = new TemplateLoader(xhr, new UrlResolver(), htmlParser); dirType = new TypeMetadata({typeUrl: 'http://sometypeurl', typeName: 'SomeComp'}); })); describe('loadTemplate', () => { describe('inline template', () => { - it('should parse the template', inject([AsyncTestCompleter], (async) => { + it('should store the template', inject([AsyncTestCompleter], (async) => { loader.loadTemplate(dirType, null, 'a', null, [], ['test.css']) .then((template: TemplateMetadata) => { - expect(humanizeDom(template.nodes)) - .toEqual([[HtmlTextAst, 'a', 'SomeComp > #text(a):nth-child(0)']]) - async.done(); + expect(template.template).toEqual('a'); + async.done(); }); })); @@ -61,9 +61,8 @@ export function main() { xhr.expect('http://sometypeurl/sometplurl', 'a'); loader.loadTemplate(dirType, null, null, 'sometplurl', [], ['test.css']) .then((template: TemplateMetadata) => { - expect(humanizeDom(template.nodes)) - .toEqual([[HtmlTextAst, 'a', 'SomeComp > #text(a):nth-child(0)']]) - async.done(); + expect(template.template).toEqual('a'); + async.done(); }); xhr.flush(); })); @@ -91,44 +90,46 @@ export function main() { expect(template.encapsulation).toBe(viewEncapsulation); }); - it('should parse the template as html', () => { + it('should keep the template as html', () => { var template = loader.createTemplateFromString(dirType, null, 'a', 'http://someurl/', [], []); - expect(humanizeDom(template.nodes)) - .toEqual([[HtmlTextAst, 'a', 'SomeComp > #text(a):nth-child(0)']]) + expect(template.template).toEqual('a') }); it('should collect and keep ngContent', () => { - var template = loader.createTemplateFromString(dirType, null, '', - 'http://someurl/', [], []); + var template = loader.createTemplateFromString( + dirType, null, '', 'http://someurl/', [], []); expect(template.ngContentSelectors).toEqual(['a']); - expect(humanizeDom(template.nodes)) - .toEqual([ - [HtmlElementAst, 'ng-content', 'SomeComp > ng-content:nth-child(0)'], - [HtmlAttrAst, 'select', 'a', 'SomeComp > ng-content:nth-child(0)[select=a]'] - ]) + expect(template.template).toEqual(''); + }); + + it('should normalize ngContent wildcard selector', () => { + var template = loader.createTemplateFromString( + dirType, null, + '', + 'http://someurl/', [], []); + expect(template.ngContentSelectors).toEqual(['*', '*', '*']); }); it('should collect and remove top level styles in the template', () => { var template = loader.createTemplateFromString(dirType, null, '', 'http://someurl/', [], []); expect(template.styles).toEqual(['a']); - expect(template.nodes).toEqual([]); + expect(template.template).toEqual(''); }); it('should collect and remove styles inside in elements', () => { var template = loader.createTemplateFromString(dirType, null, '
', 'http://someurl/', [], []); expect(template.styles).toEqual(['a']); - expect(humanizeDom(template.nodes)) - .toEqual([[HtmlElementAst, 'div', 'SomeComp > div:nth-child(0)']]); + expect(template.template).toEqual('
'); }); it('should collect and remove styleUrls in the template', () => { var template = loader.createTemplateFromString( dirType, null, '', 'http://someurl/', [], []); expect(template.styleAbsUrls).toEqual(['http://someurl/aUrl']); - expect(template.nodes).toEqual([]); + expect(template.template).toEqual(''); }); it('should collect and remove styleUrls in elements', () => { @@ -136,20 +137,14 @@ export function main() { dirType, null, '
', 'http://someurl/', [], []); expect(template.styleAbsUrls).toEqual(['http://someurl/aUrl']); - expect(humanizeDom(template.nodes)) - .toEqual([[HtmlElementAst, 'div', 'SomeComp > div:nth-child(0)']]); + expect(template.template).toEqual('
'); }); it('should keep link elements with non stylesheet rel attribute', () => { - var template = loader.createTemplateFromString(dirType, null, '', - 'http://someurl/', [], []); + var template = loader.createTemplateFromString( + dirType, null, '', 'http://someurl/', [], []); expect(template.styleAbsUrls).toEqual([]); - expect(humanizeDom(template.nodes)) - .toEqual([ - [HtmlElementAst, 'link', 'SomeComp > link:nth-child(0)'], - [HtmlAttrAst, 'href', 'b', 'SomeComp > link:nth-child(0)[href=b]'], - [HtmlAttrAst, 'rel', 'a', 'SomeComp > link:nth-child(0)[rel=a]'] - ]); + expect(template.template).toEqual(''); }); it('should extract @import style urls into styleAbsUrl', () => {