perf(core): add option to remove blank text nodes from compiled templates

This commit is contained in:
Pawel Kozlowski 2017-07-28 15:58:28 +02:00 committed by Hans
parent 088532bf2e
commit d2c0d986d4
27 changed files with 450 additions and 46 deletions

View File

@ -111,6 +111,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
i18nInFile?: string; i18nInFile?: string;
// How to handle missing messages // How to handle missing messages
i18nInMissingTranslations?: 'error'|'warning'|'ignore'; i18nInMissingTranslations?: 'error'|'warning'|'ignore';
// Whether to remove blank text nodes from compiled templates. It is `true` by default
// in Angular 5 and will be re-visited in Angular 6.
preserveWhitespaces?: boolean;
} }
export interface ModuleFilenameResolver { export interface ModuleFilenameResolver {

View File

@ -347,6 +347,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation, i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation,
enableLegacyTemplate: options.enableLegacyTemplate, enableLegacyTemplate: options.enableLegacyTemplate,
enableSummariesForJit: true, enableSummariesForJit: true,
preserveWhitespaces: options.preserveWhitespaces,
}; };
} }

View File

@ -322,9 +322,10 @@ export class AotCompiler {
const pipes = ngModule.transitiveModule.pipes.map( const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference)); pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas, compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !)); templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
const viewResult = this._viewCompiler.compileComponent( const viewResult = this._viewCompiler.compileComponent(
outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes); outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes);

View File

@ -54,6 +54,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
useJit: false, useJit: false,
enableLegacyTemplate: options.enableLegacyTemplate !== false, enableLegacyTemplate: options.enableLegacyTemplate !== false,
missingTranslation: options.missingTranslation, missingTranslation: options.missingTranslation,
preserveWhitespaces: options.preserveWhitespaces,
}); });
const normalizer = new DirectiveNormalizer( const normalizer = new DirectiveNormalizer(
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);

View File

@ -15,4 +15,5 @@ export interface AotCompilerOptions {
missingTranslation?: MissingTranslationStrategy; missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean; enableLegacyTemplate?: boolean;
enableSummariesForJit?: boolean; enableSummariesForJit?: boolean;
preserveWhitespaces?: boolean;
} }

View File

@ -252,8 +252,9 @@ export class CompileTemplateMetadata {
animations: any[]; animations: any[];
ngContentSelectors: string[]; ngContentSelectors: string[];
interpolation: [string, string]|null; interpolation: [string, string]|null;
preserveWhitespaces: boolean;
constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets,
animations, ngContentSelectors, interpolation, isInline}: { animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: {
encapsulation: ViewEncapsulation | null, encapsulation: ViewEncapsulation | null,
template: string|null, template: string|null,
templateUrl: string|null, templateUrl: string|null,
@ -263,7 +264,8 @@ export class CompileTemplateMetadata {
ngContentSelectors: string[], ngContentSelectors: string[],
animations: any[], animations: any[],
interpolation: [string, string]|null, interpolation: [string, string]|null,
isInline: boolean isInline: boolean,
preserveWhitespaces: boolean
}) { }) {
this.encapsulation = encapsulation; this.encapsulation = encapsulation;
this.template = template; this.template = template;
@ -278,6 +280,7 @@ export class CompileTemplateMetadata {
} }
this.interpolation = interpolation; this.interpolation = interpolation;
this.isInline = isInline; this.isInline = isInline;
this.preserveWhitespaces = preserveWhitespaces;
} }
toSummary(): CompileTemplateSummary { toSummary(): CompileTemplateSummary {
@ -516,7 +519,8 @@ export function createHostComponentMeta(
animations: [], animations: [],
isInline: true, isInline: true,
externalStylesheets: [], externalStylesheets: [],
interpolation: null interpolation: null,
preserveWhitespaces: false,
}), }),
exportAs: null, exportAs: null,
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,

View File

@ -24,7 +24,7 @@
export {VERSION} from './version'; export {VERSION} from './version';
export * from './template_parser/template_ast'; export * from './template_parser/template_ast';
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser'; export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
export {CompilerConfig} from './config'; export {CompilerConfig, preserveWhitespacesDefault} from './config';
export * from './compile_metadata'; export * from './compile_metadata';
export * from './aot/compiler_factory'; export * from './aot/compiler_factory';
export * from './aot/compiler'; export * from './aot/compiler';

View File

@ -6,11 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {InjectionToken, MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core'; import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core';
import {noUndefined} from './util';
import {CompileIdentifierMetadata} from './compile_metadata';
import {Identifiers} from './identifiers';
export class CompilerConfig { export class CompilerConfig {
public defaultEncapsulation: ViewEncapsulation|null; public defaultEncapsulation: ViewEncapsulation|null;
@ -19,18 +16,26 @@ export class CompilerConfig {
public enableLegacyTemplate: boolean; public enableLegacyTemplate: boolean;
public useJit: boolean; public useJit: boolean;
public missingTranslation: MissingTranslationStrategy|null; public missingTranslation: MissingTranslationStrategy|null;
public preserveWhitespaces: boolean;
constructor( constructor(
{defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation, {defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation,
enableLegacyTemplate}: { enableLegacyTemplate, preserveWhitespaces}: {
defaultEncapsulation?: ViewEncapsulation, defaultEncapsulation?: ViewEncapsulation,
useJit?: boolean, useJit?: boolean,
missingTranslation?: MissingTranslationStrategy, missingTranslation?: MissingTranslationStrategy,
enableLegacyTemplate?: boolean, enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean
} = {}) { } = {}) {
this.defaultEncapsulation = defaultEncapsulation; this.defaultEncapsulation = defaultEncapsulation;
this.useJit = !!useJit; this.useJit = !!useJit;
this.missingTranslation = missingTranslation || null; this.missingTranslation = missingTranslation || null;
this.enableLegacyTemplate = enableLegacyTemplate !== false; this.enableLegacyTemplate = enableLegacyTemplate !== false;
this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces));
} }
} }
export function preserveWhitespacesDefault(
preserveWhitespacesOption: boolean | null, defaultSetting = true): boolean {
return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption;
}

View File

@ -9,7 +9,7 @@
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core'; import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata'; import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata';
import {CompilerConfig} from './config'; import {CompilerConfig, preserveWhitespacesDefault} from './config';
import {CompilerInjectable} from './injectable'; import {CompilerInjectable} from './injectable';
import * as html from './ml_parser/ast'; import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser'; import {HtmlParser} from './ml_parser/html_parser';
@ -31,6 +31,7 @@ export interface PrenormalizedTemplateMetadata {
interpolation: [string, string]|null; interpolation: [string, string]|null;
encapsulation: ViewEncapsulation|null; encapsulation: ViewEncapsulation|null;
animations: CompileAnimationEntryMetadata[]; animations: CompileAnimationEntryMetadata[];
preserveWhitespaces: boolean|null;
} }
@CompilerInjectable() @CompilerInjectable()
@ -82,6 +83,13 @@ export class DirectiveNormalizer {
throw syntaxError( throw syntaxError(
`No template specified for component ${stringify(prenormData.componentType)}`); `No template specified for component ${stringify(prenormData.componentType)}`);
} }
if (isDefined(prenormData.preserveWhitespaces) &&
typeof prenormData.preserveWhitespaces !== 'boolean') {
throw syntaxError(
`The preserveWhitespaces option for component ${stringify(prenormData.componentType)} must be a boolean`);
}
return SyncAsync.then( return SyncAsync.then(
this.normalizeTemplateOnly(prenormData), this.normalizeTemplateOnly(prenormData),
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result)); (result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
@ -149,7 +157,9 @@ export class DirectiveNormalizer {
ngContentSelectors: visitor.ngContentSelectors, ngContentSelectors: visitor.ngContentSelectors,
animations: prenormData.animations, animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline, interpolation: prenormData.interpolation, isInline,
externalStylesheets: [] externalStylesheets: [],
preserveWhitespaces: preserveWhitespacesDefault(
prenormData.preserveWhitespaces, this._config.preserveWhitespaces),
}); });
} }
@ -168,6 +178,7 @@ export class DirectiveNormalizer {
animations: templateMeta.animations, animations: templateMeta.animations,
interpolation: templateMeta.interpolation, interpolation: templateMeta.interpolation,
isInline: templateMeta.isInline, isInline: templateMeta.isInline,
preserveWhitespaces: templateMeta.preserveWhitespaces,
})); }));
} }

View File

@ -152,7 +152,8 @@ export class DirectiveResolver {
styleUrls: directive.styleUrls, styleUrls: directive.styleUrls,
encapsulation: directive.encapsulation, encapsulation: directive.encapsulation,
animations: directive.animations, animations: directive.animations,
interpolation: directive.interpolation interpolation: directive.interpolation,
preserveWhitespaces: directive.preserveWhitespaces,
}); });
} else { } else {
return new Directive({ return new Directive({

View File

@ -262,6 +262,7 @@ export class JitCompiler implements Compiler {
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>(); const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
const outputContext = createOutputContext(); const outputContext = createOutputContext();
const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta); const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta);
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const compiledStylesheet = const compiledStylesheet =
this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta); this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta);
@ -274,7 +275,8 @@ export class JitCompiler implements Compiler {
pipe => this._metadataResolver.getPipeSummary(pipe.reference)); pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas, compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas,
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !)); templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !),
preserveWhitespaces);
const compileResult = this._viewCompiler.compileComponent( const compileResult = this._viewCompiler.compileComponent(
outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar), outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar),
usedPipes); usedPipes);

View File

@ -123,6 +123,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: ViewEncapsulation.Emulated, defaultEncapsulation: ViewEncapsulation.Emulated,
missingTranslation: MissingTranslationStrategy.Warning, missingTranslation: MissingTranslationStrategy.Warning,
enableLegacyTemplate: true, enableLegacyTemplate: true,
preserveWhitespaces: true,
}; };
this._defaultOptions = [compilerOptions, ...defaultOptions]; this._defaultOptions = [compilerOptions, ...defaultOptions];
@ -142,6 +143,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: opts.defaultEncapsulation, defaultEncapsulation: opts.defaultEncapsulation,
missingTranslation: opts.missingTranslation, missingTranslation: opts.missingTranslation,
enableLegacyTemplate: opts.enableLegacyTemplate, enableLegacyTemplate: opts.enableLegacyTemplate,
preserveWhitespaces: opts.preserveWhitespaces,
}); });
}, },
deps: [] deps: []
@ -169,6 +171,7 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
providers: _mergeArrays(optionsArr.map(options => options.providers !)), providers: _mergeArrays(optionsArr.map(options => options.providers !)),
missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)), missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)),
enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)), enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)),
preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)),
}; };
} }

View File

@ -219,7 +219,8 @@ export class CompileMetadataResolver {
styles: template.styles, styles: template.styles,
styleUrls: template.styleUrls, styleUrls: template.styleUrls,
animations: template.animations, animations: template.animations,
interpolation: template.interpolation interpolation: template.interpolation,
preserveWhitespaces: template.preserveWhitespaces
}); });
if (isPromise(templateMeta) && isSync) { if (isPromise(templateMeta) && isSync) {
this._reportError(componentStillLoadingError(directiveType), directiveType); this._reportError(componentStillLoadingError(directiveType), directiveType);
@ -267,7 +268,8 @@ export class CompileMetadataResolver {
interpolation: noUndefined(dirMeta.interpolation), interpolation: noUndefined(dirMeta.interpolation),
isInline: !!dirMeta.template, isInline: !!dirMeta.template,
externalStylesheets: [], externalStylesheets: [],
ngContentSelectors: [] ngContentSelectors: [],
preserveWhitespaces: noUndefined(dirMeta.preserveWhitespaces),
}); });
} }

View File

@ -0,0 +1,83 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from './ast';
import {ParseTreeResult} from './parser';
import {NGSP_UNICODE} from './tags';
export const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces';
const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']);
function hasPreserveWhitespacesAttr(attrs: html.Attribute[]): boolean {
return attrs.some((attr: html.Attribute) => attr.name === PRESERVE_WS_ATTR_NAME);
}
/**
* This visitor can walk HTML parse tree and remove / trim text nodes using the following rules:
* - consider spaces, tabs and new lines as whitespace characters;
* - drop text nodes consisting of whitespace characters only;
* - for all other text nodes replace consecutive whitespace characters with one space;
* - convert &ngsp; pseudo-entity to a single space;
*
* The idea of using &ngsp; as a placeholder for non-removable space was originally introduced in
* Angular Dart, see:
* https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
* In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
* and later on replaced by a space. We are re-implementing the same idea here.
*
* Removal and trimming of whitespaces have positive performance impact (less code to generate
* while compiling templates, faster view creation). At the same time it can be "destructive"
* in some cases (whitespaces can influence layout). Becouse of the potential of breaking layout
* this visitor is not activated by default in Angular 5 and people need to explicitly opt-in for
* whitespace removal. The default option for whitespace removal will be revisited in Angular 6
* and might be changed to "on" by default.
*/
class WhitespaceVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) {
// don't descent into elements where we need to preserve whitespaces
// but still visit all attributes to eliminate one used as a market to preserve WS
return new html.Element(
element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
return new html.Element(
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
visitAttribute(attribute: html.Attribute, context: any): any {
return attribute.name !== PRESERVE_WS_ATTR_NAME ? attribute : null;
}
visitText(text: html.Text, context: any): any {
const isBlank = text.value.trim().length === 0;
if (!isBlank) {
// lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE
return new html.Text(
text.value.replace(NGSP_UNICODE, ' ').replace(/\s\s+/g, ' '), text.sourceSpan);
}
return null;
}
visitComment(comment: html.Comment, context: any): any { return comment; }
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
}
export function removeWhitespaces(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
return new ParseTreeResult(
html.visitAll(new WhitespaceVisitor(), htmlAstWithErrors.rootNodes),
htmlAstWithErrors.errors);
}

View File

@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string {
// This list is not exhaustive to keep the compiler footprint low. // This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not // The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not
// exist. // exist.
export const NAMED_ENTITIES: {[k: string]: string} = { export const NAMED_ENTITIES: {[k: string]: string} = {
'Aacute': '\u00C1', 'Aacute': '\u00C1',
'aacute': '\u00E1', 'aacute': '\u00E1',
@ -325,3 +326,9 @@ export const NAMED_ENTITIES: {[k: string]: string} = {
'zwj': '\u200D', 'zwj': '\u200D',
'zwnj': '\u200C', 'zwnj': '\u200C',
}; };
// The &ngsp; pseudo-entity is denoting a space. see:
// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart
export const NGSP_UNICODE = '\uE500';
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE;

View File

@ -18,6 +18,7 @@ import {Identifiers, createTokenForExternalReference, createTokenForReference} f
import {CompilerInjectable} from '../injectable'; import {CompilerInjectable} from '../injectable';
import * as html from '../ml_parser/ast'; import * as html from '../ml_parser/ast';
import {ParseTreeResult} from '../ml_parser/html_parser'; import {ParseTreeResult} from '../ml_parser/html_parser';
import {removeWhitespaces} from '../ml_parser/html_whitespaces';
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 {isNgTemplate, splitNsName} from '../ml_parser/tags'; import {isNgTemplate, splitNsName} from '../ml_parser/tags';
@ -113,9 +114,10 @@ export class TemplateParser {
parse( parse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[], component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
templateUrl: string): {template: TemplateAst[], pipes: CompilePipeSummary[]} { preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl); const result = this.tryParse(
component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces);
const warnings = const warnings =
result.errors !.filter(error => error.level === ParseErrorLevel.WARNING) result.errors !.filter(error => error.level === ParseErrorLevel.WARNING)
.filter(warnOnlyOnce( .filter(warnOnlyOnce(
@ -137,12 +139,17 @@ export class TemplateParser {
tryParse( tryParse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[], component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
templateUrl: string): TemplateParseResult { preserveWhitespaces: boolean): TemplateParseResult {
let htmlParseResult = this._htmlParser !.parse(
template, templateUrl, true, this.getInterpolationConfig(component));
if (!preserveWhitespaces) {
htmlParseResult = removeWhitespaces(htmlParseResult);
}
return this.tryParseHtml( return this.tryParseHtml(
this.expandHtml(this._htmlParser !.parse( this.expandHtml(htmlParseResult), component, directives, pipes, schemas);
template, templateUrl, true, this.getInterpolationConfig(component))),
component, directives, pipes, schemas);
} }
tryParseHtml( tryParseHtml(

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CompileAnimationEntryMetadata} from '@angular/compiler'; import {CompileAnimationEntryMetadata} from '@angular/compiler';
import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compiler/src/compile_metadata';
import {CompilerConfig} from '@angular/compiler/src/config'; import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config';
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer'; import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
import {ResourceLoader} from '@angular/compiler/src/resource_loader'; import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
@ -31,6 +31,7 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
interpolation?: [string, string] | null; interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null; encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[]; animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean | null;
}) { }) {
return normalizer.normalizeTemplate({ return normalizer.normalizeTemplate({
ngModuleType: noUndefined(o.ngModuleType), ngModuleType: noUndefined(o.ngModuleType),
@ -42,7 +43,8 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
styleUrls: noUndefined(o.styleUrls), styleUrls: noUndefined(o.styleUrls),
interpolation: noUndefined(o.interpolation), interpolation: noUndefined(o.interpolation),
encapsulation: noUndefined(o.encapsulation), encapsulation: noUndefined(o.encapsulation),
animations: noUndefined(o.animations) animations: noUndefined(o.animations),
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
}); });
} }
@ -54,6 +56,7 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
interpolation?: [string, string] | null; interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null; encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[]; animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean | null;
}) { }) {
return normalizer.normalizeTemplateOnly({ return normalizer.normalizeTemplateOnly({
ngModuleType: noUndefined(o.ngModuleType), ngModuleType: noUndefined(o.ngModuleType),
@ -65,13 +68,14 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
styleUrls: noUndefined(o.styleUrls), styleUrls: noUndefined(o.styleUrls),
interpolation: noUndefined(o.interpolation), interpolation: noUndefined(o.interpolation),
encapsulation: noUndefined(o.encapsulation), encapsulation: noUndefined(o.encapsulation),
animations: noUndefined(o.animations) animations: noUndefined(o.animations),
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
}); });
} }
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors, externalStylesheets, animations, ngContentSelectors,
interpolation, isInline}: { interpolation, isInline, preserveWhitespaces}: {
encapsulation?: ViewEncapsulation | null, encapsulation?: ViewEncapsulation | null,
template?: string | null, template?: string | null,
templateUrl?: string | null, templateUrl?: string | null,
@ -81,7 +85,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
ngContentSelectors?: string[], ngContentSelectors?: string[],
animations?: any[], animations?: any[],
interpolation?: [string, string] | null, interpolation?: [string, string] | null,
isInline?: boolean isInline?: boolean,
preserveWhitespaces?: boolean | null
}): CompileTemplateMetadata { }): CompileTemplateMetadata {
return new CompileTemplateMetadata({ return new CompileTemplateMetadata({
encapsulation: encapsulation || null, encapsulation: encapsulation || null,
@ -94,6 +99,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
animations: animations || [], animations: animations || [],
interpolation: interpolation || null, interpolation: interpolation || null,
isInline: !!isInline, isInline: !!isInline,
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
}); });
} }
@ -106,6 +112,7 @@ function normalizeLoadedTemplate(
interpolation?: [string, string] | null; interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null; encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[]; animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean;
}, },
template: string, templateAbsUrl: string) { template: string, templateAbsUrl: string) {
return normalizer.normalizeLoadedTemplate( return normalizer.normalizeLoadedTemplate(
@ -120,6 +127,7 @@ function normalizeLoadedTemplate(
interpolation: o.interpolation || null, interpolation: o.interpolation || null,
encapsulation: o.encapsulation || null, encapsulation: o.encapsulation || null,
animations: o.animations || [], animations: o.animations || [],
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
}, },
template, templateAbsUrl); template, templateAbsUrl);
} }
@ -169,6 +177,18 @@ export function main() {
})) }))
.toThrowError(`'SomeComp' component cannot define both template and templateUrl`); .toThrowError(`'SomeComp' component cannot define both template and templateUrl`);
})); }));
it('should throw if preserveWhitespaces is not a boolean',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: '',
preserveWhitespaces: <any>'WRONG',
}))
.toThrowError(
'The preserveWhitespaces option for component SomeComp must be a boolean');
}));
}); });
describe('normalizeTemplateOnly sync', () => { describe('normalizeTemplateOnly sync', () => {
@ -431,6 +451,28 @@ export function main() {
expect(template.encapsulation).toBe(viewEncapsulation); expect(template.encapsulation).toBe(viewEncapsulation);
})); }));
it('should use preserveWhitespaces setting from compiler config if none provided',
inject(
[DirectiveNormalizer, CompilerConfig],
(normalizer: DirectiveNormalizer, config: CompilerConfig) => {
const template = normalizeLoadedTemplate(normalizer, {}, '', '');
expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces);
}));
it('should store the preserveWhitespaces=false in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', '');
expect(template.preserveWhitespaces).toBe(false);
}));
it('should store the preserveWhitespaces=true in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', '');
expect(template.preserveWhitespaces).toBe(true);
}));
it('should keep the template as html', it('should keep the template as html',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate( const template = normalizeLoadedTemplate(

View File

@ -79,7 +79,12 @@ class SomeDirectiveWithViewChild {
c: any; c: any;
} }
@Component({selector: 'sample', template: 'some template', styles: ['some styles']}) @Component({
selector: 'sample',
template: 'some template',
styles: ['some styles'],
preserveWhitespaces: true
})
class ComponentWithTemplate { class ComponentWithTemplate {
} }
@ -439,6 +444,7 @@ export function main() {
const compMetadata: Component = resolver.resolve(ComponentWithTemplate); const compMetadata: Component = resolver.resolve(ComponentWithTemplate);
expect(compMetadata.template).toEqual('some template'); expect(compMetadata.template).toEqual('some template');
expect(compMetadata.styles).toEqual(['some styles']); expect(compMetadata.styles).toEqual(['some styles']);
expect(compMetadata.preserveWhitespaces).toBe(true);
}); });
}); });
}); });

View File

@ -9,7 +9,6 @@
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 {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';

View File

@ -0,0 +1,118 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {PRESERVE_WS_ATTR_NAME, removeWhitespaces} from '../../src/ml_parser/html_whitespaces';
import {humanizeDom} from './ast_spec_utils';
export function main() {
describe('removeWhitespaces', () => {
function parseAndRemoveWS(template: string): any[] {
return humanizeDom(removeWhitespaces(new HtmlParser().parse(template, 'TestComp')));
}
it('should remove blank text nodes', () => {
expect(parseAndRemoveWS(' ')).toEqual([]);
expect(parseAndRemoveWS('\n')).toEqual([]);
expect(parseAndRemoveWS('\t')).toEqual([]);
expect(parseAndRemoveWS(' \t \n ')).toEqual([]);
});
it('should remove whitespaces (space, tab, new line) between elements', () => {
expect(parseAndRemoveWS('<br> <br>\t<br>\n<br>')).toEqual([
[html.Element, 'br', 0],
[html.Element, 'br', 0],
[html.Element, 'br', 0],
[html.Element, 'br', 0],
]);
});
it('should remove whitespaces from child text nodes', () => {
expect(parseAndRemoveWS('<div><span> </span></div>')).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
]);
});
it('should remove whitespaces from the beginning and end of a template', () => {
expect(parseAndRemoveWS(` <br>\t`)).toEqual([
[html.Element, 'br', 0],
]);
});
it('should convert &ngsp; to a space and preserve it', () => {
expect(parseAndRemoveWS('<div><span>foo</span>&ngsp;<span>bar</span></div>')).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
[html.Text, 'foo', 2],
[html.Text, ' ', 1],
[html.Element, 'span', 1],
[html.Text, 'bar', 2],
]);
});
it('should replace multiple whitespaces with one space', () => {
expect(parseAndRemoveWS('\n\n\nfoo\t\t\t')).toEqual([[html.Text, ' foo ', 0]]);
expect(parseAndRemoveWS(' \n foo \t ')).toEqual([[html.Text, ' foo ', 0]]);
});
it('should not replace single tab and newline with spaces', () => {
expect(parseAndRemoveWS('\nfoo')).toEqual([[html.Text, '\nfoo', 0]]);
expect(parseAndRemoveWS('\tfoo')).toEqual([[html.Text, '\tfoo', 0]]);
});
it('should preserve single whitespaces between interpolations', () => {
expect(parseAndRemoveWS(`{{fooExp}} {{barExp}}`)).toEqual([
[html.Text, '{{fooExp}} {{barExp}}', 0],
]);
expect(parseAndRemoveWS(`{{fooExp}}\t{{barExp}}`)).toEqual([
[html.Text, '{{fooExp}}\t{{barExp}}', 0],
]);
expect(parseAndRemoveWS(`{{fooExp}}\n{{barExp}}`)).toEqual([
[html.Text, '{{fooExp}}\n{{barExp}}', 0],
]);
});
it('should preserve whitespaces around interpolations', () => {
expect(parseAndRemoveWS(` {{exp}} `)).toEqual([
[html.Text, ' {{exp}} ', 0],
]);
});
it('should preserve whitespaces inside <pre> elements', () => {
expect(parseAndRemoveWS(`<pre><strong>foo</strong>\n<strong>bar</strong></pre>`)).toEqual([
[html.Element, 'pre', 0],
[html.Element, 'strong', 1],
[html.Text, 'foo', 2],
[html.Text, '\n', 1],
[html.Element, 'strong', 1],
[html.Text, 'bar', 2],
]);
});
it('should skip whitespace trimming in <textarea>', () => {
expect(parseAndRemoveWS(`<textarea>foo\n\n bar</textarea>`)).toEqual([
[html.Element, 'textarea', 0],
[html.Text, 'foo\n\n bar', 1],
]);
});
it(`should preserve whitespaces inside elements annotated with ${PRESERVE_WS_ATTR_NAME}`,
() => {
expect(parseAndRemoveWS(`<div ${PRESERVE_WS_ATTR_NAME}><img> <img></div>`)).toEqual([
[html.Element, 'div', 0],
[html.Element, 'img', 1],
[html.Text, ' ', 1],
[html.Element, 'img', 1],
]);
});
});
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol} from '@angular/compiler'; import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol, preserveWhitespacesDefault} from '@angular/compiler';
import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry';
@ -84,7 +84,7 @@ function compileDirectiveMetadataCreate(
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors, externalStylesheets, animations, ngContentSelectors,
interpolation, isInline}: { interpolation, isInline, preserveWhitespaces}: {
encapsulation?: ViewEncapsulation | null, encapsulation?: ViewEncapsulation | null,
template?: string | null, template?: string | null,
templateUrl?: string | null, templateUrl?: string | null,
@ -94,7 +94,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
ngContentSelectors?: string[], ngContentSelectors?: string[],
animations?: any[], animations?: any[],
interpolation?: [string, string] | null, interpolation?: [string, string] | null,
isInline?: boolean isInline?: boolean,
preserveWhitespaces?: boolean | null,
}): CompileTemplateMetadata { }): CompileTemplateMetadata {
return new CompileTemplateMetadata({ return new CompileTemplateMetadata({
encapsulation: noUndefined(encapsulation), encapsulation: noUndefined(encapsulation),
@ -106,7 +107,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
animations: animations || [], animations: animations || [],
ngContentSelectors: ngContentSelectors || [], ngContentSelectors: ngContentSelectors || [],
interpolation: noUndefined(interpolation), interpolation: noUndefined(interpolation),
isInline: !!isInline isInline: !!isInline,
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
}); });
} }
@ -116,7 +118,7 @@ export function main() {
let ngIf: CompileDirectiveSummary; let ngIf: CompileDirectiveSummary;
let parse: ( let parse: (
template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[], template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[],
schemas?: SchemaMetadata[]) => TemplateAst[]; schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[];
let console: ArrayConsole; let console: ArrayConsole;
function commonBeforeEach() { function commonBeforeEach() {
@ -148,12 +150,15 @@ export function main() {
parse = parse =
(template: string, directives: CompileDirectiveSummary[], (template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[] | null = null, pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [],
schemas: SchemaMetadata[] = []): TemplateAst[] => { preserveWhitespaces = true): TemplateAst[] => {
if (pipes === null) { if (pipes === null) {
pipes = []; pipes = [];
} }
return parser.parse(component, template, directives, pipes, schemas, 'TestComp') return parser
.parse(
component, template, directives, pipes, schemas, 'TestComp',
preserveWhitespaces)
.template; .template;
}; };
})); }));
@ -398,7 +403,8 @@ export function main() {
externalStylesheets: [], externalStylesheets: [],
styleUrls: [], styleUrls: [],
styles: [], styles: [],
encapsulation: null encapsulation: null,
preserveWhitespaces: preserveWhitespacesDefault(null),
}), }),
isHost: false, isHost: false,
exportAs: null, exportAs: null,
@ -417,7 +423,7 @@ export function main() {
}); });
expect(humanizeTplAst( expect(humanizeTplAst(
parser.parse(component, '{%a%}', [], [], [], 'TestComp').template, parser.parse(component, '{%a%}', [], [], [], 'TestComp', true).template,
{start: '{%', end: '%}'})) {start: '{%', end: '%}'}))
.toEqual([[BoundTextAst, '{% a %}']]); .toEqual([[BoundTextAst, '{% a %}']]);
})); }));
@ -2052,6 +2058,48 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
}); });
}); });
describe('whitespaces removal', () => {
it('should not remove whitespaces by default', () => {
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', []))).toEqual([
[TextAst, ' '],
[ElementAst, 'br'],
[TextAst, ' '],
[ElementAst, 'br'],
[TextAst, '\t'],
[ElementAst, 'br'],
[TextAst, '\n'],
[ElementAst, 'br'],
[TextAst, ' '],
]);
});
it('should remove whitespaces when explicitly requested', () => {
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', [], [], [], false))).toEqual([
[ElementAst, 'br'],
[ElementAst, 'br'],
[ElementAst, 'br'],
[ElementAst, 'br'],
]);
});
it('should remove whitespace between ICU expansions when not preserving whitespaces', () => {
const shortForm = '{ count, plural, =0 {small} many {big} }';
const expandedForm = '<ng-container [ngPlural]="count">' +
'<ng-template ngPluralCase="=0">small</ng-template>' +
'<ng-template ngPluralCase="many">big</ng-template>' +
'</ng-container>';
const humanizedExpandedForm = humanizeTplAst(parse(expandedForm, []));
// ICU expansions are converted to `<ng-container>` tags and all blank text nodes are reomved
// so any whitespace between ICU exansions are removed as well
expect(humanizeTplAst(parse(`${shortForm} ${shortForm}`, [], [], [], false))).toEqual([
...humanizedExpandedForm, ...humanizedExpandedForm
]);
});
});
describe('Template Parser - opt-out `<template>` support', () => { describe('Template Parser - opt-out `<template>` support', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureCompiler({ TestBed.configureCompiler({

View File

@ -85,7 +85,8 @@ export class MockDirectiveResolver extends DirectiveResolver {
styles: view.styles, styles: view.styles,
styleUrls: view.styleUrls, styleUrls: view.styleUrls,
encapsulation: view.encapsulation, encapsulation: view.encapsulation,
interpolation: view.interpolation interpolation: view.interpolation,
preserveWhitespaces: view.preserveWhitespaces,
}); });
} }

View File

@ -107,6 +107,7 @@ export type CompilerOptions = {
// Whether to support the `<template>` tag and the `template` attribute to define angular // Whether to support the `<template>` tag and the `template` attribute to define angular
// templates. They have been deprecated in 4.x, `<ng-template>` should be used instead. // templates. They have been deprecated in 4.x, `<ng-template>` should be used instead.
enableLegacyTemplate?: boolean, enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean,
}; };
/** /**

View File

@ -675,6 +675,16 @@ export interface Component extends Directive {
* {@link ComponentFactoryResolver}. * {@link ComponentFactoryResolver}.
*/ */
entryComponents?: Array<Type<any>|any[]>; entryComponents?: Array<Type<any>|any[]>;
/**
* If preserveWhitespaces is set to `false` potentially superfluous blank characters (space, tab,
* new line) will be removed from compiled templates. This can greatly reduce generated code size
* as well as speed up components' creation. The whitespace removal algorithm will drop all
* the blank text nodes and collapse series of whitespaces to just one space.
* Those transformations can potentially influence layout of the generated markup so
* the `preserveWhitespaces` should be used with care.
*/
preserveWhitespaces?: boolean;
} }
/** /**

View File

@ -1758,6 +1758,51 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
}); });
describe('whitespaces in templates', () => {
it('should not remove whitespaces by default', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(3);
}));
it('should not remove whitespaces when explicitly requested not to do so', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
preserveWhitespaces: true,
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(3);
}));
it('should remove whitespaces when explicitly requested to do so', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
preserveWhitespaces: false,
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(2);
}));
});
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {
describe('svg', () => { describe('svg', () => {

View File

@ -201,6 +201,7 @@ export declare type CompilerOptions = {
providers?: StaticProvider[]; providers?: StaticProvider[];
missingTranslation?: MissingTranslationStrategy; missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean; enableLegacyTemplate?: boolean;
preserveWhitespaces?: boolean;
}; };
/** @stable */ /** @stable */