diff --git a/modules/@angular/compiler/src/html_parser.ts b/modules/@angular/compiler/src/html_parser.ts index 04c795a75e..092232c1e0 100644 --- a/modules/@angular/compiler/src/html_parser.ts +++ b/modules/@angular/compiler/src/html_parser.ts @@ -1,11 +1,7 @@ import {Injectable} from '@angular/core'; - import {isPresent, isBlank,} from '../src/facade/lang'; - import {ListWrapper} from '../src/facade/collection'; - import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst} from './html_ast'; - import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer'; import {ParseError, ParseSourceSpan} from './parse_util'; import {getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags'; @@ -267,18 +263,17 @@ class TreeBuilder { } } - var tagDef = getHtmlTagDefinition(el.name); - var parentEl = this._getParentElement(); - if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) { + const tagDef = getHtmlTagDefinition(el.name); + const {parent, container} = this._getParentElementSkippingContainers(); + + if (tagDef.requireExtraParent(isPresent(parent) ? parent.name : null)) { var newParent = new HtmlElementAst( - tagDef.parentToAdd, [], [el], el.sourceSpan, el.startSourceSpan, el.endSourceSpan); - this._addToParent(newParent); - this.elementStack.push(newParent); - this.elementStack.push(el); - } else { - this._addToParent(el); - this.elementStack.push(el); + tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan); + this._insertBeforeContainer(parent, container, newParent); } + + this._addToParent(el); + this.elementStack.push(el); } private _consumeEndTag(endTagToken: HtmlToken) { @@ -330,6 +325,25 @@ class TreeBuilder { return this.elementStack.length > 0 ? ListWrapper.last(this.elementStack) : null; } + /** + * Returns the parent in the DOM and the container. + * + * `` elements are skipped as they are not rendered as DOM element. + */ + private _getParentElementSkippingContainers(): + {parent: HtmlElementAst, container: HtmlElementAst} { + let container: HtmlElementAst = null; + + for (let i = this.elementStack.length - 1; i >= 0; i--) { + if (this.elementStack[i].name !== 'ng-container') { + return { parent: this.elementStack[i], container } + } + container = this.elementStack[i]; + } + + return {parent: ListWrapper.last(this.elementStack), container}; + } + private _addToParent(node: HtmlAst) { var parent = this._getParentElement(); if (isPresent(parent)) { @@ -338,6 +352,31 @@ class TreeBuilder { this.rootNodes.push(node); } } + + /** + * Insert a node between the parent and the container. + * When no container is given, the node is appended as a child of the parent. + * Also updates the element stack accordingly. + * + * @internal + */ + private _insertBeforeContainer( + parent: HtmlElementAst, container: HtmlElementAst, node: HtmlElementAst) { + if (!container) { + this._addToParent(node); + this.elementStack.push(node); + } else { + if (parent) { + // replace the container with the new node in the children + let index = parent.children.indexOf(container); + parent.children[index] = node; + } else { + this.rootNodes.push(node); + } + node.children.push(container); + this.elementStack.splice(this.elementStack.indexOf(container), 0, node); + } + } } function getElementFullName( diff --git a/modules/@angular/compiler/src/util.ts b/modules/@angular/compiler/src/util.ts index 56bee5731a..2efbef948d 100644 --- a/modules/@angular/compiler/src/util.ts +++ b/modules/@angular/compiler/src/util.ts @@ -1,21 +1,15 @@ import {StringMapWrapper} from './facade/collection'; -import {IS_DART, Math, StringWrapper, isArray, isBlank, isPrimitive, isStrictStringMap} from './facade/lang'; +import {IS_DART, StringWrapper, isArray, isBlank, isPrimitive, isStrictStringMap} from './facade/lang'; export var MODULE_SUFFIX = IS_DART ? '.dart' : ''; var CAMEL_CASE_REGEXP = /([A-Z])/g; -var DASH_CASE_REGEXP = /-([a-z])/g; export function camelCaseToDashCase(input: string): string { return StringWrapper.replaceAllMapped( input, CAMEL_CASE_REGEXP, (m: string[]) => { return '-' + m[1].toLowerCase(); }); } -export function dashCaseToCamelCase(input: string): string { - return StringWrapper.replaceAllMapped( - input, DASH_CASE_REGEXP, (m: string[]) => { return m[1].toUpperCase(); }); -} - export function splitAtColon(input: string, defaultValues: string[]): string[] { var parts = StringWrapper.split(input.trim(), /\s*:\s*/g); if (parts.length > 1) { diff --git a/modules/@angular/compiler/test/html_ast_spec_utils.ts b/modules/@angular/compiler/test/html_ast_spec_utils.ts index b5fbb59ec5..901a75d9f4 100644 --- a/modules/@angular/compiler/test/html_ast_spec_utils.ts +++ b/modules/@angular/compiler/test/html_ast_spec_utils.ts @@ -1,6 +1,6 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '@angular/compiler/src/html_ast'; -import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '@angular/compiler/src/html_parser'; -import {ParseError, ParseLocation} from '@angular/compiler/src/parse_util'; +import {HtmlParseTreeResult} from '@angular/compiler/src/html_parser'; +import {ParseLocation} from '@angular/compiler/src/parse_util'; import {BaseException} from '../src/facade/exceptions'; diff --git a/modules/@angular/compiler/test/html_parser_spec.ts b/modules/@angular/compiler/test/html_parser_spec.ts index 248b6cf180..e2ca0d6987 100644 --- a/modules/@angular/compiler/test/html_parser_spec.ts +++ b/modules/@angular/compiler/test/html_parser_spec.ts @@ -2,6 +2,7 @@ import {HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpan import {HtmlTokenType} from '@angular/compiler/src/html_lexer'; import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '@angular/compiler/src/html_parser'; import {ParseError} from '@angular/compiler/src/parse_util'; +import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing/testing_internal'; import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils'; @@ -111,12 +112,42 @@ export function main() { '
', 'TestComp'))) .toEqual([ - [HtmlElementAst, 'table', 0], [HtmlElementAst, 'thead', 1], - [HtmlElementAst, 'tr', 2], [HtmlAttrAst, 'head', ''], [HtmlElementAst, 'tbody', 1], - [HtmlElementAst, 'tr', 2], [HtmlAttrAst, 'noparent', ''], - [HtmlElementAst, 'tbody', 1], [HtmlElementAst, 'tr', 2], [HtmlAttrAst, 'body', ''], - [HtmlElementAst, 'tfoot', 1], [HtmlElementAst, 'tr', 2], - [HtmlAttrAst, 'foot', ''] + [HtmlElementAst, 'table', 0], + [HtmlElementAst, 'thead', 1], + [HtmlElementAst, 'tr', 2], + [HtmlAttrAst, 'head', ''], + [HtmlElementAst, 'tbody', 1], + [HtmlElementAst, 'tr', 2], + [HtmlAttrAst, 'noparent', ''], + [HtmlElementAst, 'tbody', 1], + [HtmlElementAst, 'tr', 2], + [HtmlAttrAst, 'body', ''], + [HtmlElementAst, 'tfoot', 1], + [HtmlElementAst, 'tr', 2], + [HtmlAttrAst, 'foot', ''], + ]); + }); + + it('should append the required parent considering ng-container', () => { + expect(humanizeDom(parser.parse( + '
', 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'table', 0], + [HtmlElementAst, 'tbody', 1], + [HtmlElementAst, 'ng-container', 2], + [HtmlElementAst, 'tr', 3], + ]); + }); + + it('should special case ng-container when adding a required parent', () => { + expect(humanizeDom(parser.parse( + '
', + 'TestComp'))) + .toEqual([ + [HtmlElementAst, 'table', 0], + [HtmlElementAst, 'thead', 1], + [HtmlElementAst, 'ng-container', 2], + [HtmlElementAst, 'tr', 3], ]); }); diff --git a/modules/@angular/core/testing/testing.ts b/modules/@angular/core/testing/testing.ts index 1b4677972d..b062b25f62 100644 --- a/modules/@angular/core/testing/testing.ts +++ b/modules/@angular/core/testing/testing.ts @@ -8,7 +8,7 @@ import {TestInjector, async, getTestInjector, inject, injectAsync} from './test_ export {async, inject, injectAsync} from './test_injector'; -declare var global: any /** TODO #9100 */; +declare var global: any; var _global = (typeof window === 'undefined' ? global : window);