From 9b39e499ace74addf7edbb0202dd261de038b32b Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 28 Jul 2016 19:39:10 +0200 Subject: [PATCH] fix(core): support components without a selector (#10331) Components without a selector now get the selector `ng-component`. Directives without a selector will throw an error message. Closes #3464 Closes #10216 --- modules/@angular/compiler-cli/src/codegen.ts | 11 ++++--- .../@angular/compiler-cli/src/core_private.ts | 3 ++ .../@angular/compiler-cli/src/extract_i18n.ts | 6 +++- .../compiler/src/metadata_resolver.ts | 13 +++++++- .../src/schema/dom_element_schema_registry.ts | 2 ++ .../src/schema/element_schema_registry.ts | 1 + .../compiler/testing/schema_registry_mock.ts | 2 ++ .../core/test/linker/integration_spec.ts | 30 ++++++++++++++++++- 8 files changed, 61 insertions(+), 7 deletions(-) diff --git a/modules/@angular/compiler-cli/src/codegen.ts b/modules/@angular/compiler-cli/src/codegen.ts index bec68aef07..5273fb005c 100644 --- a/modules/@angular/compiler-cli/src/codegen.ts +++ b/modules/@angular/compiler-cli/src/codegen.ts @@ -17,6 +17,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {CompileMetadataResolver, DirectiveNormalizer, DomElementSchemaRegistry, HtmlParser, Lexer, NgModuleCompiler, Parser, StyleCompiler, TemplateParser, TypeScriptEmitter, ViewCompiler} from './compiler_private'; +import {Console} from './core_private'; import {ReflectorHost, ReflectorHostContext} from './reflector_host'; import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; import {StaticReflector, StaticSymbol} from './static_reflector'; @@ -135,13 +136,15 @@ export class CodeGenerator { }); const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config); const expressionParser = new Parser(new Lexer()); - const tmplParser = new TemplateParser( - expressionParser, new DomElementSchemaRegistry(), htmlParser, - /*console*/ null, []); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const console = new Console(); + const tmplParser = + new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []); const resolver = new CompileMetadataResolver( new compiler.NgModuleResolver(staticReflector), new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), - new compiler.ViewResolver(staticReflector), config, /*console*/ null, staticReflector); + new compiler.ViewResolver(staticReflector), config, console, elementSchemaRegistry, + staticReflector); const offlineCompiler = new compiler.OfflineCompiler( resolver, normalizer, tmplParser, new StyleCompiler(urlResolver), new ViewCompiler(config), new NgModuleCompiler(), new TypeScriptEmitter(reflectorHost)); diff --git a/modules/@angular/compiler-cli/src/core_private.ts b/modules/@angular/compiler-cli/src/core_private.ts index a71797d00c..dc5a0f9009 100644 --- a/modules/@angular/compiler-cli/src/core_private.ts +++ b/modules/@angular/compiler-cli/src/core_private.ts @@ -14,4 +14,7 @@ export var ReflectorReader: typeof t.ReflectorReader = r.ReflectorReader; export type ReflectionCapabilities = t.ReflectionCapabilities; export var ReflectionCapabilities: typeof t.ReflectionCapabilities = r.ReflectionCapabilities; +export type Console = t.Console; +export var Console: typeof t.Console = r.Console; + export var reflector: typeof t.reflector = r.reflector; diff --git a/modules/@angular/compiler-cli/src/extract_i18n.ts b/modules/@angular/compiler-cli/src/extract_i18n.ts index c0c7ba0680..711c13e38b 100644 --- a/modules/@angular/compiler-cli/src/extract_i18n.ts +++ b/modules/@angular/compiler-cli/src/extract_i18n.ts @@ -23,6 +23,7 @@ import {ViewEncapsulation} from '@angular/core'; import {StaticReflector} from './static_reflector'; import {CompileMetadataResolver, HtmlParser, DirectiveNormalizer, Lexer, Parser, DomElementSchemaRegistry, TypeScriptEmitter, MessageExtractor, removeDuplicates, ExtractionResult, Message, ParseError, serializeXmb,} from './compiler_private'; +import {Console} from './core_private'; import {ReflectorHost} from './reflector_host'; import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; @@ -146,10 +147,13 @@ class Extractor { }); const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config); const expressionParser = new Parser(new Lexer()); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const console = new Console(); const resolver = new CompileMetadataResolver( new compiler.NgModuleResolver(staticReflector), new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), - new compiler.ViewResolver(staticReflector), config, /*console*/ null, staticReflector); + new compiler.ViewResolver(staticReflector), config, console, elementSchemaRegistry, + staticReflector); // TODO(vicb): handle implicit const extractor = new MessageExtractor(htmlParser, expressionParser, [], {}); diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 8331cea54e..7b00c65403 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -21,6 +21,7 @@ import {DirectiveResolver} from './directive_resolver'; import {Identifiers, identifierToken} from './identifiers'; import {NgModuleResolver} from './ng_module_resolver'; import {PipeResolver} from './pipe_resolver'; +import {ElementSchemaRegistry} from './schema/element_schema_registry'; import {getUrlScheme} from './url_resolver'; import {MODULE_SUFFIX, ValueTransformer, sanitizeIdentifier, visitValue} from './util'; import {ViewResolver} from './view_resolver'; @@ -38,6 +39,7 @@ export class CompileMetadataResolver { private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver, private _pipeResolver: PipeResolver, private _viewResolver: ViewResolver, private _config: CompilerConfig, private _console: Console, + private _schemaRegistry: ElementSchemaRegistry, private _reflector: ReflectorReader = reflector) {} private sanitizeTokenName(token: any): string { @@ -124,6 +126,7 @@ export class CompileMetadataResolver { var viewProviders: Array = []; var moduleUrl = staticTypeModuleUrl(directiveType); var entryComponentTypes: cpl.CompileTypeMetadata[] = []; + let selector = dirMeta.selector; if (dirMeta instanceof ComponentMetadata) { var cmpMeta = dirMeta; var viewMeta = this._viewResolver.resolve(directiveType); @@ -155,6 +158,14 @@ export class CompileMetadataResolver { flattenArray(cmpMeta.entryComponents) .map((cmp) => this.getTypeMetadata(cmp, staticTypeModuleUrl(cmp))); } + if (!selector) { + selector = this._schemaRegistry.getDefaultComponentElementName(); + } + } else { + if (!selector) { + throw new BaseException( + `Directive ${stringify(directiveType)} has no selector, please add it!`); + } } var providers: Array = []; @@ -170,7 +181,7 @@ export class CompileMetadataResolver { viewQueries = this.getQueriesMetadata(dirMeta.queries, true, directiveType); } meta = cpl.CompileDirectiveMetadata.create({ - selector: dirMeta.selector, + selector: selector, exportAs: dirMeta.exportAs, isComponent: isPresent(templateMeta), type: this.getTypeMetadata(directiveType, moduleUrl), diff --git a/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts b/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts index ccd983937d..07e4d6e5be 100644 --- a/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts +++ b/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts @@ -313,4 +313,6 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { var mappedPropName = StringMapWrapper.get(attrToPropMap, propName); return isPresent(mappedPropName) ? mappedPropName : propName; } + + getDefaultComponentElementName(): string { return 'ng-component'; } } diff --git a/modules/@angular/compiler/src/schema/element_schema_registry.ts b/modules/@angular/compiler/src/schema/element_schema_registry.ts index 32a0f3a304..88bfa1794d 100644 --- a/modules/@angular/compiler/src/schema/element_schema_registry.ts +++ b/modules/@angular/compiler/src/schema/element_schema_registry.ts @@ -12,4 +12,5 @@ export abstract class ElementSchemaRegistry { abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean; abstract securityContext(tagName: string, propName: string): any; abstract getMappedPropName(propName: string): string; + abstract getDefaultComponentElementName(): string; } diff --git a/modules/@angular/compiler/testing/schema_registry_mock.ts b/modules/@angular/compiler/testing/schema_registry_mock.ts index 42f3e441fe..2f0210fc52 100644 --- a/modules/@angular/compiler/testing/schema_registry_mock.ts +++ b/modules/@angular/compiler/testing/schema_registry_mock.ts @@ -29,4 +29,6 @@ export class MockSchemaRegistry implements ElementSchemaRegistry { var result = this.attrPropMapping[attrName]; return isPresent(result) ? result : attrName; } + + getDefaultComponentElementName(): string { return 'ng-component'; } } diff --git a/modules/@angular/core/test/linker/integration_spec.ts b/modules/@angular/core/test/linker/integration_spec.ts index 923ca32065..d65bbee515 100644 --- a/modules/@angular/core/test/linker/integration_spec.ts +++ b/modules/@angular/core/test/linker/integration_spec.ts @@ -14,7 +14,7 @@ import {isPresent, stringify, isBlank,} from '../../src/facade/lang'; import {BaseException} from '../../src/facade/exceptions'; import {PromiseWrapper, EventEmitter, ObservableWrapper, PromiseCompleter,} from '../../src/facade/async'; -import {Injector, Injectable, forwardRef, OpaqueToken, Inject, Host, SkipSelf, SkipSelfMetadata, OnDestroy, ReflectiveInjector} from '@angular/core'; +import {Injector, Injectable, forwardRef, OpaqueToken, Inject, Host, SkipSelf, SkipSelfMetadata, OnDestroy, ReflectiveInjector, Compiler} from '@angular/core'; import {NgIf, NgFor, AsyncPipe} from '@angular/common'; @@ -1477,6 +1477,34 @@ function declareTests({useJit}: {useJit: boolean}) { async.done(); }); })); + + it('should throw when using directives without selector', + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + @Directive({}) + class SomeDirective { + } + + @Component({selector: 'comp', template: '', directives: [SomeDirective]}) + class SomeComponent { + } + + expect(() => tcb.createSync(SomeComponent)) + .toThrowError( + `Directive ${stringify(SomeDirective)} has no selector, please add it!`); + })); + + it('should use a default element name for components without selectors', + inject([Compiler, Injector], (compiler: Compiler, injector: Injector) => { + @Component({template: ''}) + class SomeComponent { + } + + const compFactory = compiler.compileComponentSync(SomeComponent); + expect(compFactory.selector).toBe('ng-component'); + expect( + getDOM().nodeName(compFactory.create(injector).location.nativeElement).toLowerCase()) + .toEqual('ng-component'); + })); }); describe('error handling', () => {