Skip to content

Commit

Permalink
feat(compiler): allow multiple exportAs names
Browse files Browse the repository at this point in the history
This change allows users to specify multiple exportAs names for a
directive by giving a comma-delimited list inside the string.

The primary motivation for this change is to allow these names to be
changed in a backwards compatible way.
  • Loading branch information
jelbourn authored and hansl committed Aug 16, 2017
1 parent 0d45828 commit 3a50098
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 2 deletions.
19 changes: 18 additions & 1 deletion packages/compiler/src/template_parser/template_parser.ts
Expand Up @@ -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));
Expand Down Expand Up @@ -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:
*
* <div #myMenu="coolMenu">
*
* 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[] {
Expand Down
43 changes: 43 additions & 0 deletions packages/compiler/test/template_parser/template_parser_spec.ts
Expand Up @@ -1209,6 +1209,24 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
]);
});

it('should assign references to directives via exportAs with multiple names', () => {
const pizzaTestDirective =
compileDirectiveMetadataCreate({
selector: 'pizza-test',
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Pizza'}}),
exportAs: 'pizza, cheeseSauceBread'
}).toSummary();

const template = '<pizza-test #food="pizza" #yum="cheeseSauceBread"></pizza-test>';

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('<div #a="dirA"></div>', [])).toThrowError(`Template parse errors:
There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"></div>"): TestComp@0:5`);
Expand All @@ -1231,6 +1249,31 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>

});

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 = '<div dessert-pizza chocolate #snack="chocolate"></div>';
const compileTemplate = () => parse(template, [pizzaDirective, chocolateDirective]);
const duplicateReferenceError = 'Template parse errors:\n' +
'Reference "#snack" is defined several times ' +
'("<div dessert-pizza chocolate [ERROR ->]#snack="chocolate"></div>")' +
': TestComp@0:29';

expect(compileTemplate).toThrowError(duplicateReferenceError);
});

it('should not throw error when there is same reference name in different templates',
() => {
expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/metadata/directives.ts
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/core/test/linker/integration_spec.ts
Expand Up @@ -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 = '<div multiple-export-as #x="dirX" #y="dirY"></div>';
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]});
Expand Down Expand Up @@ -2442,6 +2456,10 @@ class SomeImperativeViewport {
class ExportDir {
}

@Directive({selector: '[multiple-export-as]', exportAs: 'dirX, dirY'})
export class DirectiveWithMultipleExportAsNames {
}

@Component({selector: 'comp'})
class ComponentWithoutView {
}
Expand Down

0 comments on commit 3a50098

Please sign in to comment.