fix(compiler): process imports
first and declarations
second while calculating scopes (#35850)
Prior to this commit, while calculating the scope for a module, Ivy compiler processed `declarations` field first and `imports` after that. That results in a couple issues: * for Pipes with the same `name` and present in `declarations` and in an imported module, Pipe from imported module was selected. In View Engine the logic is opposite: Pipes from `declarations` field receive higher priority. * for Directives with the same selector and present in `declarations` and in an imported module, we first invoked the logic of a Directive from `declarations` field and after that - imported Directive logic. In View Engine, it was the opposite and the logic of a Directive from the `declarations` field was invoked last. In order to align Ivy and View Engine behavior, this commit updates the logic in which we populate module scope: we first process all imports and after that handle `declarations` field. As a result, in Ivy both use-cases listed above work similar to View Engine. Resolves #35502. PR Close #35850
This commit is contained in:

committed by
Matias Niemelä

parent
191e4d15b5
commit
0bf6e58db2
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, Directive, ElementRef, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {Component, Directive, ElementRef, EventEmitter, NgModule, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {Input} from '@angular/core/src/metadata';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
@ -633,4 +633,96 @@ describe('directives', () => {
|
||||
expect(div.getAttribute('title')).toBe('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('directives with the same selector', () => {
|
||||
it('should process Directives from `declarations` list after imported ones', () => {
|
||||
const log: string[] = [];
|
||||
@Directive({selector: '[dir]'})
|
||||
class DirectiveA {
|
||||
constructor() { log.push('DirectiveA.constructor'); }
|
||||
ngOnInit() { log.push('DirectiveA.ngOnInit'); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [DirectiveA],
|
||||
exports: [DirectiveA],
|
||||
})
|
||||
class ModuleA {
|
||||
}
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
class DirectiveB {
|
||||
constructor() { log.push('DirectiveB.constructor'); }
|
||||
ngOnInit() { log.push('DirectiveB.ngOnInit'); }
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
template: '<div dir></div>',
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ModuleA],
|
||||
declarations: [DirectiveB, App],
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
|
||||
'DirectiveB.ngOnInit'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect imported module order', () => {
|
||||
const log: string[] = [];
|
||||
@Directive({selector: '[dir]'})
|
||||
class DirectiveA {
|
||||
constructor() { log.push('DirectiveA.constructor'); }
|
||||
ngOnInit() { log.push('DirectiveA.ngOnInit'); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [DirectiveA],
|
||||
exports: [DirectiveA],
|
||||
})
|
||||
class ModuleA {
|
||||
}
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
class DirectiveB {
|
||||
constructor() { log.push('DirectiveB.constructor'); }
|
||||
ngOnInit() { log.push('DirectiveB.ngOnInit'); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [DirectiveB],
|
||||
exports: [DirectiveB],
|
||||
})
|
||||
class ModuleB {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
template: '<div dir></div>',
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ModuleA, ModuleB],
|
||||
declarations: [App],
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
|
||||
'DirectiveB.ngOnInit'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -110,6 +110,86 @@ describe('pipe', () => {
|
||||
expect(fixture.nativeElement.textContent).toEqual('value a b default 0 1 2 3');
|
||||
});
|
||||
|
||||
it('should pick a Pipe defined in `declarations` over imported Pipes', () => {
|
||||
@Pipe({name: 'number'})
|
||||
class PipeA implements PipeTransform {
|
||||
transform(value: any) { return `PipeA: ${value}`; }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [PipeA],
|
||||
exports: [PipeA],
|
||||
})
|
||||
class ModuleA {
|
||||
}
|
||||
|
||||
@Pipe({name: 'number'})
|
||||
class PipeB implements PipeTransform {
|
||||
transform(value: any) { return `PipeB: ${value}`; }
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
template: '{{ count | number }}',
|
||||
})
|
||||
class App {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ModuleA],
|
||||
declarations: [PipeB, App],
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
|
||||
});
|
||||
|
||||
it('should respect imported module order when selecting Pipe (last imported Pipe is used)',
|
||||
() => {
|
||||
@Pipe({name: 'number'})
|
||||
class PipeA implements PipeTransform {
|
||||
transform(value: any) { return `PipeA: ${value}`; }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [PipeA],
|
||||
exports: [PipeA],
|
||||
})
|
||||
class ModuleA {
|
||||
}
|
||||
|
||||
@Pipe({name: 'number'})
|
||||
class PipeB implements PipeTransform {
|
||||
transform(value: any) { return `PipeB: ${value}`; }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [PipeB],
|
||||
exports: [PipeB],
|
||||
})
|
||||
class ModuleB {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
template: '{{ count | number }}',
|
||||
})
|
||||
class App {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ModuleA, ModuleB],
|
||||
declarations: [App],
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
|
||||
});
|
||||
|
||||
it('should do nothing when no change', () => {
|
||||
let calls: any[] = [];
|
||||
|
||||
|
Reference in New Issue
Block a user