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 {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);
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue