feat(compiler): allow multiple exportAs names

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.
This commit is contained in:
Jeremy Elbourn 2017-08-15 16:34:47 -07:00 committed by Hans
parent 0d45828460
commit 3a500981ef
4 changed files with 81 additions and 2 deletions

View File

@ -580,7 +580,7 @@ class TemplateParseVisitor implements html.Visitor {
directive.inputs, props, directiveProperties, targetBoundDirectivePropNames); directive.inputs, props, directiveProperties, targetBoundDirectivePropNames);
elementOrDirectiveRefs.forEach((elOrDirRef) => { elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) || if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
(directive.exportAs == elOrDirRef.value)) { (elOrDirRef.isReferenceToDirective(directive))) {
targetReferences.push(new ReferenceAst( targetReferences.push(new ReferenceAst(
elOrDirRef.name, createTokenForReference(directive.type.reference), elOrDirRef.name, createTokenForReference(directive.type.reference),
elOrDirRef.sourceSpan)); elOrDirRef.sourceSpan));
@ -805,8 +805,25 @@ class NonBindableVisitor implements html.Visitor {
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; } 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 { class ElementOrDirectiveRef {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} 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[] { export function splitClasses(classAttrValue: string): string[] {

View File

@ -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', () => { it('should report references with values that dont match a directive as errors', () => {
expect(() => parse('<div #a="dirA"></div>', [])).toThrowError(`Template parse 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`); There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"></div>"): TestComp@0:5`);
@ -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', 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>', [])) expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))

View File

@ -54,7 +54,8 @@ export interface DirectiveDecorator {
* *
* **Metadata Properties:** * **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 * * **host** - map of class property to host element bindings for events, properties and
* attributes * attributes
* * **inputs** - list of class property names to data-bind as component inputs * * **inputs** - list of class property names to data-bind as component inputs

View File

@ -469,6 +469,20 @@ function declareTests({useJit}: {useJit: boolean}) {
.toBeAnInstanceOf(ExportDir); .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', it('should make the assigned component accessible in property bindings, even if they were declared before the component',
() => { () => {
TestBed.configureTestingModule({declarations: [MyComp, ChildComp]}); TestBed.configureTestingModule({declarations: [MyComp, ChildComp]});
@ -2442,6 +2456,10 @@ class SomeImperativeViewport {
class ExportDir { class ExportDir {
} }
@Directive({selector: '[multiple-export-as]', exportAs: 'dirX, dirY'})
export class DirectiveWithMultipleExportAsNames {
}
@Component({selector: 'comp'}) @Component({selector: 'comp'})
class ComponentWithoutView { class ComponentWithoutView {
} }