diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index 04d0aceeb5..f5ed8f8455 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -60,8 +60,8 @@ export function compileDirective( // e.g. 'type: MyDirective` field('type', outputCtx.importExpr(directive.type.reference)); - // e.g. `selector: [[[null, 'someDir', ''], null]]` - field('selector', createDirectiveSelector(directive.selector !)); + // e.g. `selectors: [['', 'someDir', '']]` + field('selectors', createDirectiveSelector(directive.selector !)); // e.g. `factory: () => new MyApp(injectElementRef())` field('factory', createFactory(directive.type, outputCtx, reflector, directive.queries)); @@ -121,8 +121,8 @@ export function compileComponent( // e.g. `type: MyApp` field('type', outputCtx.importExpr(component.type.reference)); - // e.g. `selector: [[['my-app'], null]]` - field('selector', createDirectiveSelector(component.selector !)); + // e.g. `selectors: [['my-app']]` + field('selectors', createDirectiveSelector(component.selector !)); const selector = component.selector && CssSelector.parse(component.selector); const firstSelector = selector && selector[0]; @@ -387,7 +387,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { const contentProjections = getContentProjection(asts, this.ngContentSelectors); this._contentProjections = contentProjections; if (contentProjections.size > 0) { - const infos: R3CssSelector[] = []; + const infos: R3CssSelectorList[] = []; Array.from(contentProjections.values()).forEach(info => { if (info.selector) { infos[info.index - 1] = info.selector; @@ -1097,7 +1097,7 @@ function findComponent(directives: DirectiveAst[]): DirectiveAst|undefined { interface NgContentInfo { index: number; - selector?: R3CssSelector; + selector?: R3CssSelectorList; } class ContentProjectionVisitor extends RecursiveTemplateAstVisitor { @@ -1128,28 +1128,67 @@ function getContentProjection(asts: TemplateAst[], ngContentSelectors: string[]) return projectIndexMap; } + +/** + * Flags used to generate R3-style CSS Selectors. They are pasted from + * core/src/render3/projection.ts because they cannot be referenced directly. + */ +const enum SelectorFlags { + /** Indicates this is the beginning of a new negative selector */ + NOT = 0b0001, + + /** Mode for matching attributes */ + ATTRIBUTE = 0b0010, + + /** Mode for matching tag names */ + ELEMENT = 0b0100, + + /** Mode for matching class names */ + CLASS = 0b1000, +} + // These are a copy the CSS types from core/src/render3/interfaces/projection.ts // They are duplicated here as they cannot be directly referenced from core. -type R3SimpleCssSelector = (string | null)[]; -type R3CssSelectorWithNegations = - [R3SimpleCssSelector, null] | [R3SimpleCssSelector, R3SimpleCssSelector]; -type R3CssSelector = R3CssSelectorWithNegations[]; +type R3CssSelector = (string | SelectorFlags)[]; +type R3CssSelectorList = R3CssSelector[]; -function parserSelectorToSimpleSelector(selector: CssSelector): R3SimpleCssSelector { - const classes = - selector.classNames && selector.classNames.length ? ['class', ...selector.classNames] : []; - return [selector.element, ...selector.attrs, ...classes]; +function parserSelectorToSimpleSelector(selector: CssSelector): R3CssSelector { + const classes = selector.classNames && selector.classNames.length ? + [SelectorFlags.CLASS, ...selector.classNames] : + []; + const elementName = selector.element && selector.element !== '*' ? selector.element : ''; + return [elementName, ...selector.attrs, ...classes]; } -function parserSelectorToR3Selector(selector: CssSelector): R3CssSelectorWithNegations { +function parserSelectorToNegativeSelector(selector: CssSelector): R3CssSelector { + const classes = selector.classNames && selector.classNames.length ? + [SelectorFlags.CLASS, ...selector.classNames] : + []; + + if (selector.element) { + return [ + SelectorFlags.NOT | SelectorFlags.ELEMENT, selector.element, ...selector.attrs, ...classes + ]; + } else if (selector.attrs.length) { + return [SelectorFlags.NOT | SelectorFlags.ATTRIBUTE, ...selector.attrs, ...classes]; + } else { + return selector.classNames && selector.classNames.length ? + [SelectorFlags.NOT | SelectorFlags.CLASS, ...selector.classNames] : + []; + } +} + +function parserSelectorToR3Selector(selector: CssSelector): R3CssSelector { const positive = parserSelectorToSimpleSelector(selector); - const negative = selector.notSelectors && selector.notSelectors.length && - parserSelectorToSimpleSelector(selector.notSelectors[0]); - return negative ? [positive, negative] : [positive, null]; + const negative: R3CssSelectorList = selector.notSelectors && selector.notSelectors.length ? + selector.notSelectors.map(notSelector => parserSelectorToNegativeSelector(notSelector)) : + []; + + return positive.concat(...negative); } -function parseSelectorsToR3Selector(selectors: CssSelector[]): R3CssSelector { +function parseSelectorsToR3Selector(selectors: CssSelector[]): R3CssSelectorList { return selectors.map(parserSelectorToR3Selector); } diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index 7b5cba9d77..df55aadd4a 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -92,7 +92,7 @@ describe('compiler compliance', () => { const ChildComponentDefinition = ` static ngComponentDef = $r3$.ɵdefineComponent({ type: ChildComponent, - selector: [[['child'], null]], + selectors: [['child']], factory: function ChildComponent_Factory() { return new ChildComponent(); }, template: function ChildComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -106,7 +106,7 @@ describe('compiler compliance', () => { const SomeDirectiveDefinition = ` static ngDirectiveDef = $r3$.ɵdefineDirective({ type: SomeDirective, - selector: [[[null, 'some-directive', ''], null]], + selectors: [['', 'some-directive', '']], factory: function SomeDirective_Factory() {return new SomeDirective(); } }); `; @@ -118,7 +118,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, - selector: [[['my-component'], null]], + selectors: [['my-component']], factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -143,6 +143,49 @@ describe('compiler compliance', () => { expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); }); + it('should support complex selectors', () => { + const files = { + app: { + 'spec.ts': ` + import {Directive, NgModule} from '@angular/core'; + + @Directive({selector: 'div.foo[some-directive]:not([title]):not(.baz)'}) + export class SomeDirective {} + + @Directive({selector: ':not(span[title]):not(.baz)'}) + export class OtherDirective {} + + @NgModule({declarations: [SomeDirective, OtherDirective]}) + export class MyModule{} + ` + } + }; + + // SomeDirective definition should be: + const SomeDirectiveDefinition = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: SomeDirective, + selectors: [['div', 'some-directive', '', 8, 'foo', 3, 'title', '', 9, 'baz']], + factory: function SomeDirective_Factory() {return new SomeDirective(); } + }); + `; + + // OtherDirective definition should be: + const OtherDirectiveDefinition = ` + static ngDirectiveDef = $r3$.ɵdefineDirective({ + type: OtherDirective, + selectors: [['', 5, 'span', 'title', '', 9, 'baz']], + factory: function OtherDirective_Factory() {return new OtherDirective(); } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef'); + expectEmit(source, OtherDirectiveDefinition, 'Incorrect OtherDirective.ngDirectiveDef'); + }); + it('should support host bindings', () => { const files = { app: { @@ -163,7 +206,7 @@ describe('compiler compliance', () => { const HostBindingDirDeclaration = ` static ngDirectiveDef = $r3$.ɵdefineDirective({ type: HostBindingDir, - selector: [[[null, 'hostBindingDir', ''], null]], + selectors: [['', 'hostBindingDir', '']], factory: function HostBindingDir_Factory() { return new HostBindingDir(); }, hostBindings: function HostBindingDir_HostBindings( dirIndex: $number$, elIndex: $number$) { @@ -206,7 +249,7 @@ describe('compiler compliance', () => { const IfDirectiveDefinition = ` static ngDirectiveDef = $r3$.ɵdefineDirective({ type: IfDirective, - selector: [[[null, 'if', ''], null]], + selectors: [['', 'if', '']], factory: function IfDirective_Factory() { return new IfDirective($r3$.ɵinjectTemplateRef()); } });`; const MyComponentDefinition = ` @@ -215,7 +258,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, - selector: [[['my-component'], null]], + selectors: [['my-component']], factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -287,7 +330,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyApp, - selector: [[['my-app'], null]], + selectors: [['my-app']], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { if (cm) { @@ -365,7 +408,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyApp, - selector: [[['my-app'], null]], + selectors: [['my-app']], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { if (cm) { @@ -425,7 +468,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyApp, - selector: [[['my-app'], null]], + selectors: [['my-app']], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { if (cm) { @@ -489,7 +532,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyApp, - selector: [[['my-app'], null]], + selectors: [['my-app']], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { if (cm) { @@ -546,7 +589,7 @@ describe('compiler compliance', () => { const SimpleComponentDefinition = ` static ngComponentDef = $r3$.ɵdefineComponent({ type: SimpleComponent, - selector: [[['simple'], null]], + selectors: [['simple']], factory: function SimpleComponent_Factory() { return new SimpleComponent(); }, template: function SimpleComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -559,13 +602,13 @@ describe('compiler compliance', () => { });`; const ComplexComponentDefinition = ` - const $c1$ = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]]; + const $c1$ = [[['span', 'title', 'tofirst']], [['span', 'title', 'tosecond']]]; const $c2$ = ['id','first']; const $c3$ = ['id','second']; … static ngComponentDef = $r3$.ɵdefineComponent({ type: ComplexComponent, - selector: [[['complex'], null]], + selectors: [['complex']], factory: function ComplexComponent_Factory() { return new ComplexComponent(); }, template: function ComplexComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -631,7 +674,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: ViewQueryComponent, - selector: [[['view-query-component'], null]], + selectors: [['view-query-component']], factory: function ViewQueryComponent_Factory() { return new ViewQueryComponent(); }, template: function ViewQueryComponent_Template(ctx: $ViewQueryComponent$, cm: $boolean$) { var $tmp$: $any$; @@ -689,7 +732,7 @@ describe('compiler compliance', () => { const ContentQueryComponentDefinition = ` static ngComponentDef = $r3$.ɵdefineComponent({ type: ContentQueryComponent, - selector: [[['content-query-component'], null]], + selectors: [['content-query-component']], factory: function ContentQueryComponent_Factory() { return [new ContentQueryComponent(), $r3$.ɵQ(null, SomeDirective, true)]; }, @@ -772,7 +815,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyApp, - selector: [[['my-app'], null]], + selectors: [['my-app']], factory: function MyApp_Factory() { return new MyApp(); }, template: function MyApp_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -813,7 +856,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, - selector: [[['my-component'], null]], + selectors: [['my-component']], factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -881,7 +924,7 @@ describe('compiler compliance', () => { const LifecycleCompDefinition = ` static ngComponentDef = $r3$.ɵdefineComponent({ type: LifecycleComp, - selector: [[['lifecycle-comp'], null]], + selectors: [['lifecycle-comp']], factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, template: function LifecycleComp_Template(ctx: IDENT, cm: IDENT) {}, inputs: {nameMin: 'name'}, @@ -893,7 +936,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: SimpleLayout, - selector: [[['simple-layout'], null]], + selectors: [['simple-layout']], factory: function SimpleLayout_Factory() { return new SimpleLayout(); }, template: function SimpleLayout_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -1001,7 +1044,7 @@ describe('compiler compliance', () => { const ForDirectiveDefinition = ` static ngDirectiveDef = $r3$.ɵdefineDirective({ type: ForOfDirective, - selector: [[[null, 'forOf', ''], null]], + selectors: [['', 'forOf', '']], factory: function ForOfDirective_Factory() { return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); }, @@ -1015,7 +1058,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, - selector: [[['my-component'], null]], + selectors: [['my-component']], factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { @@ -1093,7 +1136,7 @@ describe('compiler compliance', () => { … static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, - selector: [[['my-component'], null]], + selectors: [['my-component']], factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) {