fix(compiler): support `<ng-container>` whatever the namespace

fixes #14257
This commit is contained in:
Victor Berchet 2017-04-13 10:37:45 -07:00 committed by Tobias Bosch
parent 268884296a
commit 5b141fbf27
7 changed files with 60 additions and 28 deletions

View File

@ -11,7 +11,7 @@ import {ParseError, ParseSourceSpan} from '../parse_util';
import * as html from './ast';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import * as lex from './lexer';
import {TagDefinition, getNsPrefix, mergeNsAndName} from './tags';
import {TagDefinition, getNsPrefix, isNgContainer, mergeNsAndName} from './tags';
export class TreeError extends ParseError {
static create(elementName: string|null, span: ParseSourceSpan, msg: string): TreeError {
@ -352,11 +352,12 @@ class _TreeBuilder {
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
private _getParentElementSkippingContainers(): {parent: html.Element, container: html.Element} {
let container: html.Element = null !;
private _getParentElementSkippingContainers():
{parent: html.Element, container: html.Element|null} {
let container: html.Element|null = null;
for (let i = this._elementStack.length - 1; i >= 0; i--) {
if (this._elementStack[i].name !== 'ng-container') {
if (!isNgContainer(this._elementStack[i].name)) {
return {parent: this._elementStack[i], container};
}
container = this._elementStack[i];
@ -382,7 +383,7 @@ class _TreeBuilder {
* @internal
*/
private _insertBeforeContainer(
parent: html.Element, container: html.Element, node: html.Element) {
parent: html.Element, container: html.Element|null, node: html.Element) {
if (!container) {
this._addToParent(node);
this._elementStack.push(node);

View File

@ -12,7 +12,6 @@ export enum TagContentType {
PARSABLE_DATA
}
// TODO(vicb): read-only when TS supports it
export interface TagDefinition {
closedByParent: boolean;
requiredParents: {[key: string]: boolean};
@ -42,6 +41,21 @@ export function splitNsName(elementName: string): [string | null, string] {
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
}
// `<ng-container>` tags work the same regardless the namespace
export function isNgContainer(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-container';
}
// `<ng-content>` tags work the same regardless the namespace
export function isNgContent(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-content';
}
// `<ng-template>` tags work the same regardless the namespace
export function isNgTemplate(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-template';
}
export function getNsPrefix(fullName: string): string
export function getNsPrefix(fullName: null): null;
export function getNsPrefix(fullName: string | null): string |

View File

@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
import {CompilerInjectable} from '../injectable';
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
import {CompilerInjectable} from '../injectable';
import {isNgContainer, isNgContent} from '../ml_parser/tags';
import {dashCaseToCamelCase} from '../util';
import {SECURITY_SCHEMA} from './dom_security_schema';
@ -288,7 +289,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
}
if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return false;
}
@ -309,7 +310,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
}
if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return true;
}

View File

@ -7,7 +7,8 @@
*/
import {Inject, InjectionToken, Optional, SchemaMetadata, ɵConsole as Console} from '@angular/core';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTemplateSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
@ -18,13 +19,14 @@ import * as html from '../ml_parser/ast';
import {ParseTreeResult} from '../ml_parser/html_parser';
import {expandNodes} from '../ml_parser/icu_ast_expander';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {splitNsName} from '../ml_parser/tags';
import {isNgTemplate, splitNsName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector, SelectorMatcher} from '../selector';
import {isStyleUrlResolvable} from '../style_url_resolver';
import {syntaxError} from '../util';
import {BindingParser, BoundProperty} from './binding_parser';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast';
import {PreparsedElementType, preparseElement} from './template_preparser';
@ -53,7 +55,6 @@ const IDENT_PROPERTY_IDX = 9;
// Group 10 = identifier inside ()
const IDENT_EVENT_IDX = 10;
const NG_TEMPLATE_ELEMENT = 'ng-template';
// deprecated in 4.x
const TEMPLATE_ELEMENT = 'template';
// deprecated in 4.x
@ -891,9 +892,8 @@ function isEmptyExpression(ast: AST): boolean {
function isTemplate(
el: html.Element, enableLegacyTemplate: boolean,
reportDeprecation: (m: string, span: ParseSourceSpan) => void): boolean {
if (isNgTemplate(el.name)) return true;
const tagNoNs = splitNsName(el.name)[1];
// `<ng-template>` is an angular construct and is lower case
if (tagNoNs === NG_TEMPLATE_ELEMENT) return true;
// `<template>` is HTML and case insensitive
if (tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
if (enableLegacyTemplate && tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {

View File

@ -7,10 +7,9 @@
*/
import * as html from '../ml_parser/ast';
import {splitNsName} from '../ml_parser/tags';
import {isNgContent} from '../ml_parser/tags';
const NG_CONTENT_SELECT_ATTR = 'select';
const NG_CONTENT_ELEMENT = 'ng-content';
const LINK_ELEMENT = 'link';
const LINK_STYLE_REL_ATTR = 'rel';
const LINK_STYLE_HREF_ATTR = 'href';
@ -45,7 +44,7 @@ export function preparseElement(ast: html.Element): PreparsedElement {
selectAttr = normalizeNgContentSelect(selectAttr);
const nodeName = ast.name.toLowerCase();
let type = PreparsedElementType.OTHER;
if (splitNsName(nodeName)[1] == NG_CONTENT_ELEMENT) {
if (isNgContent(nodeName)) {
type = PreparsedElementType.NG_CONTENT;
} else if (nodeName == STYLE_ELEMENT) {
type = PreparsedElementType.STYLE;

View File

@ -14,6 +14,7 @@ import {CompilerConfig} from '../config';
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
import {Identifiers, createIdentifier, createIdentifierToken, resolveIdentifier} from '../identifiers';
import {CompilerInjectable} from '../injectable';
import {isNgContainer} from '../ml_parser/tags';
import * as o from '../output/output_ast';
import {convertValueToOutputAst} from '../output/value_util';
import {ParseSourceSpan} from '../parse_util';
@ -302,11 +303,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
// reserve the space in the nodeDefs array so we can add children
this.nodes.push(null !);
let elName: string|null = ast.name;
if (ast.name === NG_CONTAINER_TAG) {
// Using a null element name creates an anchor.
elName = null;
}
// Using a null element name creates an anchor.
const elName: string|null = isNgContainer(ast.name) ? null : ast.name;
const {flags, usedEvents, queryMatchesExpr, hostBindings: dirHostBindings, hostEvents} =
this._visitElementOrTemplate(nodeIndex, ast);
@ -988,7 +986,7 @@ function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean {
}
if (lastAstNode instanceof ElementAst) {
if (lastAstNode.name === NG_CONTAINER_TAG && lastAstNode.children.length) {
if (isNgContainer(lastAstNode.name) && lastAstNode.children.length) {
return needsAdditionalRootNode(lastAstNode.children);
}
return lastAstNode.hasViewContainer;
@ -1068,7 +1066,6 @@ function fixedAttrsDef(elementAst: ElementAst): o.Expression {
mapResult[name] = prevValue != null ? mergeAttributeValue(name, prevValue, value) : value;
});
});
const mapEntries: o.LiteralMapEntry[] = [];
// Note: We need to sort to get a defined output order
// for tests and for caching generated artifacts...
return o.literalArr(Object.keys(mapResult).sort().map(
@ -1197,4 +1194,4 @@ function calcStaticDynamicQueryFlags(
flags |= NodeFlags.DynamicQuery;
}
return flags;
}
}

View File

@ -9,14 +9,15 @@
import {Component, Directive, Input} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
export function main() {
describe('integration tests', () => {
let fixture: ComponentFixture<TestComponent>;
describe('directiv es', () => {
describe('directives', () => {
it('should support dotted selectors', async(() => {
@Directive({selector: '[dot.name]'})
class MyDir {
@ -38,6 +39,25 @@ export function main() {
}));
});
describe('ng-container', () => {
if (browserDetection.isChromeDesktop) {
it('should work regardless the namespace', async(() => {
@Component({
selector: 'comp',
template:
'<svg><ng-container *ngIf="1"><rect x="10" y="10" width="30" height="30"></rect></ng-container></svg>',
})
class MyCmp {
}
const f =
TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.children[0].children[0].tagName).toEqual('rect');
}));
}
});
});
}