fix(compiler): support <ng-container>
whatever the namespace
fixes #14257
This commit is contained in:
parent
268884296a
commit
5b141fbf27
@ -11,7 +11,7 @@ import {ParseError, ParseSourceSpan} from '../parse_util';
|
|||||||
import * as html from './ast';
|
import * as html from './ast';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
|
||||||
import * as lex from './lexer';
|
import * as lex from './lexer';
|
||||||
import {TagDefinition, getNsPrefix, mergeNsAndName} from './tags';
|
import {TagDefinition, getNsPrefix, isNgContainer, mergeNsAndName} from './tags';
|
||||||
|
|
||||||
export class TreeError extends ParseError {
|
export class TreeError extends ParseError {
|
||||||
static create(elementName: string|null, span: ParseSourceSpan, msg: string): TreeError {
|
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.
|
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
|
||||||
*/
|
*/
|
||||||
private _getParentElementSkippingContainers(): {parent: html.Element, container: html.Element} {
|
private _getParentElementSkippingContainers():
|
||||||
let container: html.Element = null !;
|
{parent: html.Element, container: html.Element|null} {
|
||||||
|
let container: html.Element|null = null;
|
||||||
|
|
||||||
for (let i = this._elementStack.length - 1; i >= 0; i--) {
|
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};
|
return {parent: this._elementStack[i], container};
|
||||||
}
|
}
|
||||||
container = this._elementStack[i];
|
container = this._elementStack[i];
|
||||||
@ -382,7 +383,7 @@ class _TreeBuilder {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
private _insertBeforeContainer(
|
private _insertBeforeContainer(
|
||||||
parent: html.Element, container: html.Element, node: html.Element) {
|
parent: html.Element, container: html.Element|null, node: html.Element) {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
this._addToParent(node);
|
this._addToParent(node);
|
||||||
this._elementStack.push(node);
|
this._elementStack.push(node);
|
||||||
|
@ -12,7 +12,6 @@ export enum TagContentType {
|
|||||||
PARSABLE_DATA
|
PARSABLE_DATA
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(vicb): read-only when TS supports it
|
|
||||||
export interface TagDefinition {
|
export interface TagDefinition {
|
||||||
closedByParent: boolean;
|
closedByParent: boolean;
|
||||||
requiredParents: {[key: string]: 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)];
|
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: string): string
|
||||||
export function getNsPrefix(fullName: null): null;
|
export function getNsPrefix(fullName: null): null;
|
||||||
export function getNsPrefix(fullName: string | null): string |
|
export function getNsPrefix(fullName: string | null): string |
|
||||||
|
@ -6,9 +6,10 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
|
||||||
import {CompilerInjectable} from '../injectable';
|
|
||||||
|
|
||||||
|
import {CompilerInjectable} from '../injectable';
|
||||||
|
import {isNgContainer, isNgContent} from '../ml_parser/tags';
|
||||||
import {dashCaseToCamelCase} from '../util';
|
import {dashCaseToCamelCase} from '../util';
|
||||||
|
|
||||||
import {SECURITY_SCHEMA} from './dom_security_schema';
|
import {SECURITY_SCHEMA} from './dom_security_schema';
|
||||||
@ -288,7 +289,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tagName.indexOf('-') > -1) {
|
if (tagName.indexOf('-') > -1) {
|
||||||
if (tagName === 'ng-container' || tagName === 'ng-content') {
|
if (isNgContainer(tagName) || isNgContent(tagName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +310,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tagName.indexOf('-') > -1) {
|
if (tagName.indexOf('-') > -1) {
|
||||||
if (tagName === 'ng-container' || tagName === 'ng-content') {
|
if (isNgContainer(tagName) || isNgContent(tagName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Inject, InjectionToken, Optional, SchemaMetadata, ɵConsole as Console} from '@angular/core';
|
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 {CompilerConfig} from '../config';
|
||||||
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
||||||
import {Parser} from '../expression_parser/parser';
|
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 {ParseTreeResult} from '../ml_parser/html_parser';
|
||||||
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
||||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
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 {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
|
||||||
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
|
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
|
||||||
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
||||||
import {CssSelector, SelectorMatcher} from '../selector';
|
import {CssSelector, SelectorMatcher} from '../selector';
|
||||||
import {isStyleUrlResolvable} from '../style_url_resolver';
|
import {isStyleUrlResolvable} from '../style_url_resolver';
|
||||||
import {syntaxError} from '../util';
|
import {syntaxError} from '../util';
|
||||||
|
|
||||||
import {BindingParser, BoundProperty} from './binding_parser';
|
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 {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';
|
import {PreparsedElementType, preparseElement} from './template_preparser';
|
||||||
@ -53,7 +55,6 @@ const IDENT_PROPERTY_IDX = 9;
|
|||||||
// Group 10 = identifier inside ()
|
// Group 10 = identifier inside ()
|
||||||
const IDENT_EVENT_IDX = 10;
|
const IDENT_EVENT_IDX = 10;
|
||||||
|
|
||||||
const NG_TEMPLATE_ELEMENT = 'ng-template';
|
|
||||||
// deprecated in 4.x
|
// deprecated in 4.x
|
||||||
const TEMPLATE_ELEMENT = 'template';
|
const TEMPLATE_ELEMENT = 'template';
|
||||||
// deprecated in 4.x
|
// deprecated in 4.x
|
||||||
@ -891,9 +892,8 @@ function isEmptyExpression(ast: AST): boolean {
|
|||||||
function isTemplate(
|
function isTemplate(
|
||||||
el: html.Element, enableLegacyTemplate: boolean,
|
el: html.Element, enableLegacyTemplate: boolean,
|
||||||
reportDeprecation: (m: string, span: ParseSourceSpan) => void): boolean {
|
reportDeprecation: (m: string, span: ParseSourceSpan) => void): boolean {
|
||||||
|
if (isNgTemplate(el.name)) return true;
|
||||||
const tagNoNs = splitNsName(el.name)[1];
|
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
|
// `<template>` is HTML and case insensitive
|
||||||
if (tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
|
if (tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
|
||||||
if (enableLegacyTemplate && tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
|
if (enableLegacyTemplate && tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
|
||||||
|
@ -7,10 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as html from '../ml_parser/ast';
|
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_SELECT_ATTR = 'select';
|
||||||
const NG_CONTENT_ELEMENT = 'ng-content';
|
|
||||||
const LINK_ELEMENT = 'link';
|
const LINK_ELEMENT = 'link';
|
||||||
const LINK_STYLE_REL_ATTR = 'rel';
|
const LINK_STYLE_REL_ATTR = 'rel';
|
||||||
const LINK_STYLE_HREF_ATTR = 'href';
|
const LINK_STYLE_HREF_ATTR = 'href';
|
||||||
@ -45,7 +44,7 @@ export function preparseElement(ast: html.Element): PreparsedElement {
|
|||||||
selectAttr = normalizeNgContentSelect(selectAttr);
|
selectAttr = normalizeNgContentSelect(selectAttr);
|
||||||
const nodeName = ast.name.toLowerCase();
|
const nodeName = ast.name.toLowerCase();
|
||||||
let type = PreparsedElementType.OTHER;
|
let type = PreparsedElementType.OTHER;
|
||||||
if (splitNsName(nodeName)[1] == NG_CONTENT_ELEMENT) {
|
if (isNgContent(nodeName)) {
|
||||||
type = PreparsedElementType.NG_CONTENT;
|
type = PreparsedElementType.NG_CONTENT;
|
||||||
} else if (nodeName == STYLE_ELEMENT) {
|
} else if (nodeName == STYLE_ELEMENT) {
|
||||||
type = PreparsedElementType.STYLE;
|
type = PreparsedElementType.STYLE;
|
||||||
|
@ -14,6 +14,7 @@ import {CompilerConfig} from '../config';
|
|||||||
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
|
import {AST, ASTWithSource, Interpolation} from '../expression_parser/ast';
|
||||||
import {Identifiers, createIdentifier, createIdentifierToken, resolveIdentifier} from '../identifiers';
|
import {Identifiers, createIdentifier, createIdentifierToken, resolveIdentifier} from '../identifiers';
|
||||||
import {CompilerInjectable} from '../injectable';
|
import {CompilerInjectable} from '../injectable';
|
||||||
|
import {isNgContainer} from '../ml_parser/tags';
|
||||||
import * as o from '../output/output_ast';
|
import * as o from '../output/output_ast';
|
||||||
import {convertValueToOutputAst} from '../output/value_util';
|
import {convertValueToOutputAst} from '../output/value_util';
|
||||||
import {ParseSourceSpan} from '../parse_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
|
// reserve the space in the nodeDefs array so we can add children
|
||||||
this.nodes.push(null !);
|
this.nodes.push(null !);
|
||||||
|
|
||||||
let elName: string|null = ast.name;
|
// Using a null element name creates an anchor.
|
||||||
if (ast.name === NG_CONTAINER_TAG) {
|
const elName: string|null = isNgContainer(ast.name) ? null : ast.name;
|
||||||
// Using a null element name creates an anchor.
|
|
||||||
elName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {flags, usedEvents, queryMatchesExpr, hostBindings: dirHostBindings, hostEvents} =
|
const {flags, usedEvents, queryMatchesExpr, hostBindings: dirHostBindings, hostEvents} =
|
||||||
this._visitElementOrTemplate(nodeIndex, ast);
|
this._visitElementOrTemplate(nodeIndex, ast);
|
||||||
@ -988,7 +986,7 @@ function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastAstNode instanceof ElementAst) {
|
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 needsAdditionalRootNode(lastAstNode.children);
|
||||||
}
|
}
|
||||||
return lastAstNode.hasViewContainer;
|
return lastAstNode.hasViewContainer;
|
||||||
@ -1068,7 +1066,6 @@ function fixedAttrsDef(elementAst: ElementAst): o.Expression {
|
|||||||
mapResult[name] = prevValue != null ? mergeAttributeValue(name, prevValue, value) : value;
|
mapResult[name] = prevValue != null ? mergeAttributeValue(name, prevValue, value) : value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const mapEntries: o.LiteralMapEntry[] = [];
|
|
||||||
// Note: We need to sort to get a defined output order
|
// Note: We need to sort to get a defined output order
|
||||||
// for tests and for caching generated artifacts...
|
// for tests and for caching generated artifacts...
|
||||||
return o.literalArr(Object.keys(mapResult).sort().map(
|
return o.literalArr(Object.keys(mapResult).sort().map(
|
||||||
@ -1197,4 +1194,4 @@ function calcStaticDynamicQueryFlags(
|
|||||||
flags |= NodeFlags.DynamicQuery;
|
flags |= NodeFlags.DynamicQuery;
|
||||||
}
|
}
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
@ -9,14 +9,15 @@
|
|||||||
import {Component, Directive, Input} from '@angular/core';
|
import {Component, Directive, Input} from '@angular/core';
|
||||||
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
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';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('integration tests', () => {
|
describe('integration tests', () => {
|
||||||
let fixture: ComponentFixture<TestComponent>;
|
let fixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
describe('directives', () => {
|
||||||
describe('directiv es', () => {
|
|
||||||
it('should support dotted selectors', async(() => {
|
it('should support dotted selectors', async(() => {
|
||||||
@Directive({selector: '[dot.name]'})
|
@Directive({selector: '[dot.name]'})
|
||||||
class MyDir {
|
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');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user