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…
Reference in New Issue