From c20a5d65d86c57227a1f3d3cc0b0ca2b085eac48 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Tue, 28 Jul 2015 11:38:40 -0700 Subject: [PATCH] fix(compiler): Allow components to use any style of selector. Fixes #1602 --- .../src/render/dom/compiler/compiler.ts | 13 +++++++---- .../render/dom/compiler/directive_parser.ts | 9 -------- .../src/render/dom/compiler/selector.ts | 15 ++++++++++++ .../dom/compiler/compiler_common_tests.ts | 21 +++++++++++++++++ .../dom/compiler/directive_parser_spec.ts | 6 ----- .../test/render/dom/compiler/selector_spec.ts | 23 +++++++++++++++++++ 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/modules/angular2/src/render/dom/compiler/compiler.ts b/modules/angular2/src/render/dom/compiler/compiler.ts index 16f03bef9c..ae633f2698 100644 --- a/modules/angular2/src/render/dom/compiler/compiler.ts +++ b/modules/angular2/src/render/dom/compiler/compiler.ts @@ -20,6 +20,7 @@ import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory'; import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {Parser} from 'angular2/src/change_detection/change_detection'; import * as pvm from '../view/proto_view_merger'; +import {CssSelector} from './selector'; import {DOCUMENT_TOKEN, APP_ID_TOKEN} from '../dom_tokens'; import {Inject} from 'angular2/di'; import {SharedStylesHost} from '../view/shared_styles_host'; @@ -50,7 +51,7 @@ export class DomCompiler extends RenderCompiler { } compileHost(directiveMetadata: DirectiveMetadata): Promise { - var hostViewDef = new ViewDefinition({ + let hostViewDef = new ViewDefinition({ componentId: directiveMetadata.id, templateAbsUrl: null, template: null, styles: null, @@ -58,10 +59,12 @@ export class DomCompiler extends RenderCompiler { directives: [directiveMetadata], encapsulation: ViewEncapsulation.NONE }); - return this._compileView( - hostViewDef, new TemplateAndStyles( - `<${directiveMetadata.selector}>`, []), - ViewType.HOST); + + let selector = CssSelector.parse(directiveMetadata.selector)[0]; + let hostTemplate = selector.getMatchingElementTemplate(); + let templateAndStyles = new TemplateAndStyles(hostTemplate, []); + + return this._compileView(hostViewDef, templateAndStyles, ViewType.HOST); } mergeProtoViewsRecursively( diff --git a/modules/angular2/src/render/dom/compiler/directive_parser.ts b/modules/angular2/src/render/dom/compiler/directive_parser.ts index 1b7cda1164..a1496c1af0 100644 --- a/modules/angular2/src/render/dom/compiler/directive_parser.ts +++ b/modules/angular2/src/render/dom/compiler/directive_parser.ts @@ -24,21 +24,12 @@ export class DirectiveParser implements CompileStep { for (var i = 0; i < _directives.length; i++) { var directive = _directives[i]; var selector = CssSelector.parse(directive.selector); - this._ensureComponentOnlyHasElementSelector(selector, directive); this._selectorMatcher.addSelectables(selector, i); } } processStyle(style: string): string { return style; } - _ensureComponentOnlyHasElementSelector(selector, directive) { - var isElementSelector = selector.length === 1 && selector[0].isElementSelector(); - if (!isElementSelector && directive.type === DirectiveMetadata.COMPONENT_TYPE) { - throw new BaseException( - `Component '${directive.id}' can only have an element selector, but had '${directive.selector}'`); - } - } - processElement(parent: CompileElement, current: CompileElement, control: CompileControl) { var attrs = current.attrs(); var classList = current.classList(); diff --git a/modules/angular2/src/render/dom/compiler/selector.ts b/modules/angular2/src/render/dom/compiler/selector.ts index 74e7357013..ca1b0bd9c5 100644 --- a/modules/angular2/src/render/dom/compiler/selector.ts +++ b/modules/angular2/src/render/dom/compiler/selector.ts @@ -91,6 +91,21 @@ export class CssSelector { this.element = element; } + /** Gets a template string for an element that matches the selector. */ + getMatchingElementTemplate(): string { + let tagName = isPresent(this.element) ? this.element : 'div'; + let classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : ''; + + let attrs = ''; + for (let i = 0; i < this.attrs.length; i += 2) { + let attrName = this.attrs[i]; + let attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : ''; + attrs += ` ${attrName}${attrValue}`; + } + + return `<${tagName}${classAttr}${attrs}>`; + } + addAttribute(name: string, value: string = _EMPTY_ATTR_VALUE) { this.attrs.push(name.toLowerCase()); if (isPresent(value)) { diff --git a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts index 68c92291f9..82aca739b8 100644 --- a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts +++ b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts @@ -89,6 +89,27 @@ export function runCompilerCommonTests() { }); })); + it('should create element from component selector', inject([AsyncTestCompleter], (async) => { + var compiler = createCompiler((parent, current, control) => { + current.inheritedProtoView.bindVariable('b', 'a'); + }); + + var dirMetadata = DirectiveMetadata.create({ + id: 'id', + selector: 'marquee.jazzy[size=huge]', + type: DirectiveMetadata.COMPONENT_TYPE + }); + + compiler.compileHost(dirMetadata) + .then((protoView) => { + let element = DOM.firstChild(DOM.content(templateRoot(protoView))); + expect(DOM.tagName(element).toLowerCase()).toEqual('marquee'); + expect(DOM.hasClass(element, 'jazzy')).toBe(true); + expect(DOM.getAttribute(element, 'size')).toEqual('huge'); + async.done(); + }); + })); + it('should use the inline template and compile in sync', inject([AsyncTestCompleter], (async) => { var compiler = createCompiler(EMPTY_STEP); diff --git a/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts b/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts index 6c9c7b4935..6b7d3669a0 100644 --- a/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts +++ b/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts @@ -174,12 +174,6 @@ export function main() { expect(results[0].componentId).toEqual('someComponent'); }); - it('should throw when the provided selector is not an element selector', () => { - expect(() => { createPipeline(null, [componentWithNonElementSelector]); }) - .toThrowError( - `Component 'componentWithNonElementSelector' can only have an element selector, but had '[attr]'`); - }); - it('should not allow multiple component directives on the same element', () => { expect(() => { process(el(''), null, [someComponent, someComponentDup]); diff --git a/modules/angular2/test/render/dom/compiler/selector_spec.ts b/modules/angular2/test/render/dom/compiler/selector_spec.ts index bbe15d29d2..683dfda485 100644 --- a/modules/angular2/test/render/dom/compiler/selector_spec.ts +++ b/modules/angular2/test/render/dom/compiler/selector_spec.ts @@ -335,4 +335,27 @@ export function main() { expect(cssSelectors[2].notSelectors[0].classNames).toEqual(['special']); }); }); + + describe('CssSelector.getMatchingElementTemplate', () => { + it('should create an element with a tagName, classes, and attributes', () => { + let selector = CssSelector.parse('blink.neon.hotpink[sweet][dismissable=false]')[0]; + let template = selector.getMatchingElementTemplate(); + + expect(template).toEqual(''); + }); + + it('should create an element without a tag name', () => { + let selector = CssSelector.parse('[fancy]')[0]; + let template = selector.getMatchingElementTemplate(); + + expect(template).toEqual('
'); + }); + + it('should ignore :not selectors', () => { + let selector = CssSelector.parse('grape:not(.red)')[0]; + let template = selector.getMatchingElementTemplate(); + + expect(template).toEqual(''); + }); + }); }