diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index ae5dab1455..b52298b211 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -580,7 +580,7 @@ class TemplateParseVisitor implements html.Visitor { directive.inputs, props, directiveProperties, targetBoundDirectivePropNames); elementOrDirectiveRefs.forEach((elOrDirRef) => { if ((elOrDirRef.value.length === 0 && directive.isComponent) || - (directive.exportAs == elOrDirRef.value)) { + (elOrDirRef.isReferenceToDirective(directive))) { targetReferences.push(new ReferenceAst( elOrDirRef.name, createTokenForReference(directive.type.reference), elOrDirRef.sourceSpan)); @@ -805,8 +805,25 @@ class NonBindableVisitor implements html.Visitor { visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; } } +/** + * A reference to an element or directive in a template. E.g., the reference in this template: + * + *
+ * + * would be {name: 'myMenu', value: 'coolMenu', sourceSpan: ...} + */ class ElementOrDirectiveRef { constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} + + /** Gets whether this is a reference to the given directive. */ + isReferenceToDirective(directive: CompileDirectiveSummary) { + return splitExportAs(directive.exportAs).indexOf(this.value) !== -1; + } +} + +/** Splits a raw, potentially comma-delimted `exportAs` value into an array of names. */ +function splitExportAs(exportAs: string | null): string[] { + return exportAs ? exportAs.split(',').map(e => e.trim()) : []; } export function splitClasses(classAttrValue: string): string[] { diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index 7ffb4dfe58..7937bd7cbd 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -1209,6 +1209,24 @@ Binding to attribute 'onEvent' is disallowed for security reasons (" { + const pizzaTestDirective = + compileDirectiveMetadataCreate({ + selector: 'pizza-test', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Pizza'}}), + exportAs: 'pizza, cheeseSauceBread' + }).toSummary(); + + const template = ''; + + expect(humanizeTplAst(parse(template, [pizzaTestDirective]))).toEqual([ + [ElementAst, 'pizza-test'], + [ReferenceAst, 'food', createTokenForReference(pizzaTestDirective.type.reference)], + [ReferenceAst, 'yum', createTokenForReference(pizzaTestDirective.type.reference)], + [DirectiveAst, pizzaTestDirective], + ]); + }); + it('should report references with values that dont match a directive as errors', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: There is no directive with "exportAs" set to "dirA" ("
]#a="dirA">
"): TestComp@0:5`); @@ -1231,6 +1249,31 @@ Reference "#a" is defined several times ("
]#a>
}); + it('should report duplicate reference names when using mutliple exportAs names', () => { + const pizzaDirective = + compileDirectiveMetadataCreate({ + selector: '[dessert-pizza]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Pizza'}}), + exportAs: 'dessertPizza, chocolate' + }).toSummary(); + + const chocolateDirective = + compileDirectiveMetadataCreate({ + selector: '[chocolate]', + type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Chocolate'}}), + exportAs: 'chocolate' + }).toSummary(); + + const template = '
'; + const compileTemplate = () => parse(template, [pizzaDirective, chocolateDirective]); + const duplicateReferenceError = 'Template parse errors:\n' + + 'Reference "#snack" is defined several times ' + + '("
]#snack="chocolate">
")' + + ': TestComp@0:29'; + + expect(compileTemplate).toThrowError(duplicateReferenceError); + }); + it('should not throw error when there is same reference name in different templates', () => { expect(() => parse('
', [])) diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index 8ae0808f67..a8847605ae 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -54,7 +54,8 @@ export interface DirectiveDecorator { * * **Metadata Properties:** * - * * **exportAs** - name under which the component instance is exported in a template + * * **exportAs** - name under which the component instance is exported in a template. Can be + * given a single name or a comma-delimited list of names. * * **host** - map of class property to host element bindings for events, properties and * attributes * * **inputs** - list of class property names to data-bind as component inputs diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 5491c1c131..573a4d40d2 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -469,6 +469,20 @@ function declareTests({useJit}: {useJit: boolean}) { .toBeAnInstanceOf(ExportDir); }); + it('should assign a directive to a ref when it has multiple exportAs names', () => { + TestBed.configureTestingModule( + {declarations: [MyComp, DirectiveWithMultipleExportAsNames]}); + + const template = '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + + const fixture = TestBed.createComponent(MyComp); + expect(fixture.debugElement.children[0].references !['x']) + .toBeAnInstanceOf(DirectiveWithMultipleExportAsNames); + expect(fixture.debugElement.children[0].references !['y']) + .toBeAnInstanceOf(DirectiveWithMultipleExportAsNames); + }); + it('should make the assigned component accessible in property bindings, even if they were declared before the component', () => { TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); @@ -2442,6 +2456,10 @@ class SomeImperativeViewport { class ExportDir { } +@Directive({selector: '[multiple-export-as]', exportAs: 'dirX, dirY'}) +export class DirectiveWithMultipleExportAsNames { +} + @Component({selector: 'comp'}) class ComponentWithoutView { }