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:

committed by
Igor Minar

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\]/);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user