fix(ivy): support multiple directives with the same selector (#27298)
Previously the concept of multiple directives with the same selector was not supported by ngtsc. This is due to the treatment of directives for a component as a Map from selector to the directive, which is an erroneous representation. Now the directives for a component are stored as an array which supports multiple directives with the same selector. Testing strategy: a new ngtsc_spec test asserts that multiple directives with the same selector are matched on an element. PR Close #27298
This commit is contained in:
		
							parent
							
								
									c331fc6f0c
								
							
						
					
					
						commit
						412e47d311
					
				| @ -23,6 +23,7 @@ import {ScopeDirective, SelectorScopeRegistry} from './selector_scope'; | ||||
| import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util'; | ||||
| 
 | ||||
| const EMPTY_MAP = new Map<string, Expression>(); | ||||
| const EMPTY_ARRAY: any[] = []; | ||||
| 
 | ||||
| export interface ComponentHandlerData { | ||||
|   meta: R3ComponentMetadata; | ||||
| @ -208,7 +209,7 @@ export class ComponentDecoratorHandler implements | ||||
|           // These will be replaced during the compilation step, after all `NgModule`s have been
 | ||||
|           // analyzed and the full compilation scope for the component can be realized.
 | ||||
|           pipes: EMPTY_MAP, | ||||
|           directives: EMPTY_MAP, | ||||
|           directives: EMPTY_ARRAY, | ||||
|           wrapDirectivesAndPipesInClosure: false,  //
 | ||||
|           animations, | ||||
|           viewProviders | ||||
| @ -225,7 +226,7 @@ export class ComponentDecoratorHandler implements | ||||
|     const matcher = new SelectorMatcher<ScopeDirective<any>>(); | ||||
|     if (scope !== null) { | ||||
|       scope.directives.forEach( | ||||
|           (meta, selector) => { matcher.addSelectables(CssSelector.parse(selector), meta); }); | ||||
|           ({selector, meta}) => { matcher.addSelectables(CssSelector.parse(selector), meta); }); | ||||
|       ctx.addTemplate(node as ts.ClassDeclaration, meta.parsedTemplate, matcher); | ||||
|     } | ||||
|   } | ||||
| @ -241,8 +242,9 @@ export class ComponentDecoratorHandler implements | ||||
|       // scope. This is possible now because during compile() the whole compilation unit has been
 | ||||
|       // fully analyzed.
 | ||||
|       const {pipes, containsForwardDecls} = scope; | ||||
|       const directives = new Map<string, Expression>(); | ||||
|       scope.directives.forEach((meta, selector) => directives.set(selector, meta.directive)); | ||||
|       const directives: {selector: string, expression: Expression}[] = []; | ||||
|       scope.directives.forEach( | ||||
|           ({selector, meta}) => directives.push({selector, expression: meta.directive})); | ||||
|       const wrapDirectivesAndPipesInClosure: boolean = !!containsForwardDecls; | ||||
|       metadata = {...metadata, directives, pipes, wrapDirectivesAndPipesInClosure}; | ||||
|     } | ||||
|  | ||||
| @ -31,7 +31,7 @@ export interface ModuleData { | ||||
|  * context of some module. | ||||
|  */ | ||||
| export interface CompilationScope<T> { | ||||
|   directives: Map<string, ScopeDirective<T>>; | ||||
|   directives: {selector: string, meta: ScopeDirective<T>}[]; | ||||
|   pipes: Map<string, T>; | ||||
|   containsForwardDecls?: boolean; | ||||
| } | ||||
| @ -153,7 +153,7 @@ export class SelectorScopeRegistry { | ||||
|     } | ||||
| 
 | ||||
|     // This is the first time the scope for this module is being computed.
 | ||||
|     const directives = new Map<string, ScopeDirective<Reference<ts.Declaration>>>(); | ||||
|     const directives: {selector: string, meta: ScopeDirective<Reference<ts.Declaration>>}[] = []; | ||||
|     const pipes = new Map<string, Reference>(); | ||||
| 
 | ||||
|     // Process the declaration scope of the module, and lookup the selector of every declared type.
 | ||||
| @ -166,7 +166,7 @@ export class SelectorScopeRegistry { | ||||
|       const metadata = this.lookupDirectiveMetadata(ref); | ||||
|       // Only directives/components with selectors get added to the scope.
 | ||||
|       if (metadata != null) { | ||||
|         directives.set(metadata.selector, {...metadata, directive: ref}); | ||||
|         directives.push({selector: metadata.selector, meta: {...metadata, directive: ref}}); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
| @ -418,18 +418,16 @@ function absoluteModuleName(ref: Reference): string|null { | ||||
|   return ref.moduleName; | ||||
| } | ||||
| 
 | ||||
| function convertDirectiveReferenceMap( | ||||
|     map: Map<string, ScopeDirective<Reference>>, | ||||
|     context: ts.SourceFile): Map<string, ScopeDirective<Expression>> { | ||||
|   const newMap = new Map<string, ScopeDirective<Expression>>(); | ||||
|   map.forEach((meta, selector) => { | ||||
| function convertDirectiveReferenceList( | ||||
|     input: {selector: string, meta: ScopeDirective<Reference>}[], | ||||
|     context: ts.SourceFile): {selector: string, meta: ScopeDirective<Expression>}[] { | ||||
|   return input.map(({selector, meta}) => { | ||||
|     const directive = meta.directive.toExpression(context); | ||||
|     if (directive === null) { | ||||
|       throw new Error(`Could not write expression to reference ${meta.directive.node}`); | ||||
|     } | ||||
|     newMap.set(selector, {...meta, directive}); | ||||
|     return {selector, meta: {...meta, directive}}; | ||||
|   }); | ||||
|   return newMap; | ||||
| } | ||||
| 
 | ||||
| function convertPipeReferenceMap( | ||||
| @ -448,13 +446,13 @@ function convertPipeReferenceMap( | ||||
| function convertScopeToExpressions( | ||||
|     scope: CompilationScope<Reference>, context: ts.Declaration): CompilationScope<Expression> { | ||||
|   const sourceContext = ts.getOriginalNode(context).getSourceFile(); | ||||
|   const directives = convertDirectiveReferenceMap(scope.directives, sourceContext); | ||||
|   const directives = convertDirectiveReferenceList(scope.directives, sourceContext); | ||||
|   const pipes = convertPipeReferenceMap(scope.pipes, sourceContext); | ||||
|   const declPointer = maybeUnwrapNameOfDeclaration(context); | ||||
|   let containsForwardDecls = false; | ||||
|   directives.forEach(expr => { | ||||
|   directives.forEach(({selector, meta}) => { | ||||
|     containsForwardDecls = containsForwardDecls || | ||||
|         isExpressionForwardReference(expr.directive, declPointer, sourceContext); | ||||
|         isExpressionForwardReference(meta.directive, declPointer, sourceContext); | ||||
|   }); | ||||
|   !containsForwardDecls && pipes.forEach(expr => { | ||||
|     containsForwardDecls = | ||||
|  | ||||
| @ -91,7 +91,7 @@ describe('SelectorScopeRegistry', () => { | ||||
|     const scope = registry.lookupCompilationScope(ProgramCmp) !; | ||||
|     expect(scope).toBeDefined(); | ||||
|     expect(scope.directives).toBeDefined(); | ||||
|     expect(scope.directives.size).toBe(2); | ||||
|     expect(scope.directives.length).toBe(2); | ||||
|   }); | ||||
| 
 | ||||
|   it('exports of third-party libs work', () => { | ||||
| @ -162,6 +162,6 @@ describe('SelectorScopeRegistry', () => { | ||||
|     const scope = registry.lookupCompilationScope(ProgramCmp) !; | ||||
|     expect(scope).toBeDefined(); | ||||
|     expect(scope.directives).toBeDefined(); | ||||
|     expect(scope.directives.size).toBe(2); | ||||
|     expect(scope.directives.length).toBe(2); | ||||
|   }); | ||||
| }); | ||||
| @ -832,4 +832,31 @@ describe('ngtsc behavioral tests', () => { | ||||
|     expect(jsContents).toContain('ɵsetClassMetadata(TestNgModule, '); | ||||
|     expect(jsContents).toContain('ɵsetClassMetadata(TestPipe, '); | ||||
|   }); | ||||
| 
 | ||||
|   it('should compile a template using multiple directives with the same selector', () => { | ||||
|     env.tsconfig(); | ||||
|     env.write('test.ts', ` | ||||
|       import {Component, Directive, NgModule} from '@angular/core'; | ||||
| 
 | ||||
|       @Directive({selector: '[test]'}) | ||||
|       class DirA {} | ||||
| 
 | ||||
|       @Directive({selector: '[test]'}) | ||||
|       class DirB {} | ||||
| 
 | ||||
|       @Component({ | ||||
|         template: '<div test></div>', | ||||
|       }) | ||||
|       class Cmp {} | ||||
| 
 | ||||
|       @NgModule({ | ||||
|         declarations: [Cmp, DirA, DirB], | ||||
|       }) | ||||
|       class Module {} | ||||
|     `);
 | ||||
| 
 | ||||
|     env.driveMain(); | ||||
|     const jsContents = env.getContents('test.js'); | ||||
|     expect(jsContents).toMatch(/directives: \[DirA,\s+DirB\]/); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -128,7 +128,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { | ||||
|   animations: any[]|undefined; | ||||
|   viewQueries: R3QueryMetadataFacade[]; | ||||
|   pipes: Map<string, any>; | ||||
|   directives: Map<string, any>; | ||||
|   directives: {selector: string, expression: any}[]; | ||||
|   styles: string[]; | ||||
|   encapsulation: ViewEncapsulation; | ||||
|   viewProviders: Provider[]|null; | ||||
|  | ||||
| @ -153,10 +153,10 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { | ||||
|   pipes: Map<string, o.Expression>; | ||||
| 
 | ||||
|   /** | ||||
|    * A map of directive selectors to an expression referencing the directive type which are in the | ||||
|    * A list of directive selectors and an expression referencing the directive type which are in the | ||||
|    * scope of the compilation. | ||||
|    */ | ||||
|   directives: Map<string, o.Expression>; | ||||
|   directives: {selector: string, expression: o.Expression}[]; | ||||
| 
 | ||||
|   /** | ||||
|    * Whether to wrap the 'directives' and/or `pipes` array, if one is generated, in a closure. | ||||
|  | ||||
| @ -231,11 +231,11 @@ export function compileComponentFromMetadata( | ||||
|   // Generate the CSS matcher that recognize directive
 | ||||
|   let directiveMatcher: SelectorMatcher|null = null; | ||||
| 
 | ||||
|   if (meta.directives.size) { | ||||
|   if (meta.directives.length > 0) { | ||||
|     const matcher = new SelectorMatcher(); | ||||
|     meta.directives.forEach((expression, selector: string) => { | ||||
|     for (const {selector, expression} of meta.directives) { | ||||
|       matcher.addSelectables(CssSelector.parse(selector), expression); | ||||
|     }); | ||||
|     } | ||||
|     directiveMatcher = matcher; | ||||
|   } | ||||
| 
 | ||||
| @ -375,7 +375,7 @@ export function compileComponentFromRender2( | ||||
|       ngContentSelectors: render3Ast.ngContentSelectors, | ||||
|       relativeContextFilePath: '', | ||||
|     }, | ||||
|     directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx), | ||||
|     directives: [], | ||||
|     pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx), | ||||
|     viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx), | ||||
|     wrapDirectivesAndPipesInClosure: false, | ||||
|  | ||||
| @ -128,7 +128,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { | ||||
|   animations: any[]|undefined; | ||||
|   viewQueries: R3QueryMetadataFacade[]; | ||||
|   pipes: Map<string, any>; | ||||
|   directives: Map<string, any>; | ||||
|   directives: {selector: string, expression: any}[]; | ||||
|   styles: string[]; | ||||
|   encapsulation: ViewEncapsulation; | ||||
|   viewProviders: Provider[]|null; | ||||
|  | ||||
| @ -58,7 +58,7 @@ export function compileComponent(type: Type<any>, metadata: Component): void { | ||||
|           styles: metadata.styles || EMPTY_ARRAY, | ||||
|           animations: metadata.animations, | ||||
|           viewQueries: extractQueriesMetadata(getReflect().propMetadata(type), isViewQuery), | ||||
|           directives: new Map(), | ||||
|           directives: [], | ||||
|           pipes: new Map(), | ||||
|           encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, | ||||
|           viewProviders: metadata.viewProviders || null, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user