feat(compiler): integrate compiler with view engine (#14487)

Aspects: di, query, content projection

Included refactoring:
- use a number as query id
- use a bloom filter for aggregating matched queries of nested elements
- separate static vs dynamic queries

Part of #14013
This commit is contained in:
Tobias Bosch
2017-02-15 08:36:49 -08:00
committed by Igor Minar
parent e9ba7aa4f8
commit 4e7752a12a
29 changed files with 810 additions and 558 deletions

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ComponentFactoryResolver} from '@angular/core';
import {noComponentFactoryError} from '@angular/core/src/linker/component_factory_resolver';
import {TestBed} from '@angular/core/testing';
@ -14,6 +15,19 @@ import {Console} from '../../src/console';
export function main() {
describe('Current compiler', () => { createTests({viewEngine: false}); });
describe('View Engine compiler', () => {
beforeEach(() => {
TestBed.configureCompiler(
{useJit: true, providers: [{provide: USE_VIEW_ENGINE, useValue: true}]});
});
createTests({viewEngine: true});
});
}
function createTests({viewEngine}: {viewEngine: boolean}) {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}

View File

@ -1250,7 +1250,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
.toThrowError(`Directive ${stringify(SomeDirective)} has no selector, please add it!`);
});
viewEngine || it('should use a default element name for components without selectors', () => {
it('should use a default element name for components without selectors', () => {
let noSelectorComponentFactory: ComponentFactory<SomeComponent>;
@Component({template: '----'})

View File

@ -6,14 +6,27 @@
* found in the LICENSE file at https://angular.io/license
*/
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {Component, Directive, ElementRef, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {beforeEach, ddescribe, describe, iit, it} from '@angular/core/testing/testing_internal';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('Current compiler', () => { createTests({viewEngine: false}); });
describe('View Engine compiler', () => {
beforeEach(() => {
TestBed.configureCompiler(
{useJit: true, providers: [{provide: USE_VIEW_ENGINE, useValue: true}]});
});
createTests({viewEngine: true});
});
}
function createTests({viewEngine}: {viewEngine: boolean}) {
describe('projection', () => {
beforeEach(() => TestBed.configureTestingModule({declarations: [MainComp, OtherComp, Simple]}));
@ -365,7 +378,7 @@ export function main() {
expect(main.nativeElement).toHaveText('TREE(0:TREE2(1:TREE(2:)))');
});
if (getDOM().supportsNativeShadowDOM()) {
if (!viewEngine && getDOM().supportsNativeShadowDOM()) {
it('should support native content projection and isolate styles per component', () => {
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
TestBed.overrideComponent(MainComp, {
@ -383,7 +396,7 @@ export function main() {
});
}
if (getDOM().supportsDOMEvents()) {
if (!viewEngine && getDOM().supportsDOMEvents()) {
it('should support non emulated styles', () => {
TestBed.configureTestingModule({declarations: [OtherComp]});
TestBed.overrideComponent(MainComp, {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, asNativeElements} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
@ -13,6 +14,19 @@ import {expect} from '@angular/platform-browser/testing/matchers';
import {stringify} from '../../src/facade/lang';
export function main() {
describe('Current compiler', () => { createTests({viewEngine: false}); });
describe('View Engine compiler', () => {
beforeEach(() => {
TestBed.configureCompiler(
{useJit: true, providers: [{provide: USE_VIEW_ENGINE, useValue: true}]});
});
createTests({viewEngine: true});
});
}
function createTests({viewEngine}: {viewEngine: boolean}) {
describe('Query API', () => {
beforeEach(() => TestBed.configureTestingModule({
@ -267,9 +281,10 @@ export function main() {
it('should contain the first descendant content child templateRef', () => {
const template = '<needs-content-child-template-ref-app>' +
'</needs-content-child-template-ref-app>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const view = createTestCmp(MyComp0, template);
view.detectChanges();
// can't execute checkNoChanges as our view modifies our content children (via a query).
view.detectChanges(false);
expect(view.nativeElement).toHaveText('OUTER');
});

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, DebugElement, Directive, ElementRef, Host, Inject, Input, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
import {beforeEach, beforeEachProviders, describe, it} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
@ -190,6 +190,19 @@ class TestComp {
}
export function main() {
describe('Current compiler', () => { createTests({viewEngine: false}); });
describe('View Engine compiler', () => {
beforeEach(() => {
TestBed.configureCompiler(
{useJit: true, providers: [{provide: USE_VIEW_ENGINE, useValue: true}]});
});
createTests({viewEngine: true});
});
}
function createTests({viewEngine}: {viewEngine: boolean}) {
function createComponentFixture<T>(
template: string, providers: Provider[] = null, comp: Type<T> = null): ComponentFixture<T> {
if (!comp) {
@ -213,9 +226,10 @@ export function main() {
// On CJS fakeAsync is not supported...
if (!getDOM().supportsDOMEvents()) return;
beforeEach(() => TestBed.configureTestingModule({declarations: [TestComp]}));
beforeEachProviders(() => [{provide: 'appService', useValue: 'appService'}]);
beforeEach(() => TestBed.configureTestingModule({
declarations: [TestComp],
providers: [{provide: 'appService', useValue: 'appService'}]
}));
describe('injection', () => {
it('should instantiate directives that have no dependencies', () => {
@ -591,20 +605,19 @@ export function main() {
.toBe(el.children[0].nativeElement);
});
it('should inject ChangeDetectorRef of the component\'s view into the component via a proxy',
() => {
TestBed.configureTestingModule({declarations: [PushComponentNeedsChangeDetectorRef]});
const cf = createComponentFixture('<div componentNeedsChangeDetectorRef></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
comp.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
});
it('should inject ChangeDetectorRef of the component\'s view into the component', () => {
TestBed.configureTestingModule({declarations: [PushComponentNeedsChangeDetectorRef]});
const cf = createComponentFixture('<div componentNeedsChangeDetectorRef></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
comp.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
});
it('should inject ChangeDetectorRef of the containing component into directives', () => {
TestBed.configureTestingModule(
@ -624,9 +637,9 @@ export function main() {
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
expect(compEl.children[0].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef)
.toBe(comp.changeDetectorRef);
.toEqual(comp.changeDetectorRef);
expect(compEl.children[1].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef)
.toBe(comp.changeDetectorRef);
.toEqual(comp.changeDetectorRef);
comp.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
@ -687,7 +700,7 @@ export function main() {
'<div [simpleDirective]="true | pipeNeedsChangeDetectorRef" directiveNeedsChangeDetectorRef></div>');
const cdRef =
el.children[0].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef;
expect(el.children[0].injector.get(SimpleDirective).value.changeDetectorRef).toBe(cdRef);
expect(el.children[0].injector.get(SimpleDirective).value.changeDetectorRef).toEqual(cdRef);
});
it('should cache pure pipes', () => {

View File

@ -33,6 +33,8 @@ export function main() {
return {rootNodes, view};
}
const someQueryId = 1;
class AService {}
class QueryService {
@ -42,19 +44,24 @@ export function main() {
function contentQueryProviders() {
return [
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.All})
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.All})
];
}
function viewQueryProviders(compView: ViewDefinition) {
return [
directiveDef(NodeFlags.None, null, 1, QueryService, [], null, null, () => compView),
queryDef(NodeFlags.HasViewQuery, 'query1', {'a': QueryBindingType.All})
queryDef(
NodeFlags.HasViewQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.All})
];
}
function aServiceProvider() {
return directiveDef(NodeFlags.None, [['query1', QueryValueType.Provider]], 0, AService, []);
return directiveDef(
NodeFlags.None, [[someQueryId, QueryValueType.Provider]], 0, AService, []);
}
describe('content queries', () => {
@ -251,7 +258,9 @@ export function main() {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 4, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.All}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.All}),
aServiceProvider(),
aServiceProvider(),
]));
@ -274,7 +283,9 @@ export function main() {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 4, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.First}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
aServiceProvider(),
aServiceProvider(),
]));
@ -293,9 +304,11 @@ export function main() {
}
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, [['query1', QueryValueType.ElementRef]], null, 2, 'div'),
elementDef(NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null, 2, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.First}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
@ -311,10 +324,12 @@ export function main() {
const {view} = createAndGetRootNodes(compViewDef([
anchorDef(
NodeFlags.None, [['query1', QueryValueType.TemplateRef]], null, 2,
NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null, 2,
embeddedViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.First}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
@ -329,9 +344,11 @@ export function main() {
}
const {view} = createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, [['query1', QueryValueType.ViewContainerRef]], null, 2),
anchorDef(NodeFlags.None, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.First}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
@ -367,7 +384,7 @@ export function main() {
expect(err).toBeTruthy();
expect(err.message)
.toBe(
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'Query query1 not dirty'. Current value: 'Query query1 dirty'.`);
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'Query 1 not dirty'. Current value: 'Query 1 dirty'.`);
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(2);
@ -381,7 +398,9 @@ export function main() {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.All}),
queryDef(
NodeFlags.HasContentQuery | NodeFlags.HasDynamicQuery, someQueryId,
{'a': QueryBindingType.All}),
aServiceProvider(),
]));

View File

@ -39,7 +39,7 @@ export function main() {
directiveDef(
NodeFlags.None, null, 0, AComp, [], null, null,
() => compViewDef([
elementDef(NodeFlags.None, [['#ref', QueryValueType.ElementRef]], null, 2, 'span'),
elementDef(NodeFlags.None, [['ref', QueryValueType.ElementRef]], null, 2, 'span'),
directiveDef(NodeFlags.None, null, 0, AService, []), textDef(null, ['a'])
])),
]));

View File

@ -7,6 +7,7 @@
*/
import {NodeFlags, QueryValueType, ViewData, ViewDefinition, ViewFlags, anchorDef, directiveDef, elementDef, textDef, viewDef} from '@angular/core/src/view/index';
import {filterQueryId} from '@angular/core/src/view/util';
export function main() {
describe('viewDef', () => {
@ -76,7 +77,7 @@ export function main() {
describe('parent', () => {
function parents(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.parent);
return viewDef.nodes.map(node => node.parent ? node.parent.index : null);
}
it('should calculate parents for one level', () => {
@ -86,7 +87,7 @@ export function main() {
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, 0]);
expect(parents(vd)).toEqual([null, 0, 0]);
});
it('should calculate parents for one level, multiple roots', () => {
@ -98,7 +99,7 @@ export function main() {
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, undefined, 2, undefined]);
expect(parents(vd)).toEqual([null, 0, null, 2, null]);
});
it('should calculate parents for multiple levels', () => {
@ -111,7 +112,7 @@ export function main() {
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([undefined, 0, 1, undefined, 3, undefined]);
expect(parents(vd)).toEqual([null, 0, 1, null, 3, null]);
});
});
@ -175,52 +176,56 @@ export function main() {
});
describe('childMatchedQueries', () => {
function childMatchedQueries(viewDef: ViewDefinition): string[][] {
return viewDef.nodes.map(node => Object.keys(node.childMatchedQueries).sort());
function childMatchedQueries(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.childMatchedQueries);
}
it('should calculate childMatchedQueries for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [['q1', QueryValueType.Provider]], 0, AService, [])
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, [])
]);
expect(childMatchedQueries(vd)).toEqual([['q1'], []]);
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), 0]);
});
it('should calculate childMatchedQueries for two levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [['q1', QueryValueType.Provider]], 0, AService, [])
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, [])
]);
expect(childMatchedQueries(vd)).toEqual([['q1'], ['q1'], []]);
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), filterQueryId(1), 0]);
});
it('should calculate childMatchedQueries for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [['q1', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.None, [['q2', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [['q3', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[2, QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[3, QueryValueType.Provider]], 0, AService, []),
]);
expect(childMatchedQueries(vd)).toEqual([['q1'], [], ['q2', 'q3'], [], []]);
expect(childMatchedQueries(vd)).toEqual([
filterQueryId(1), 0, filterQueryId(2) | filterQueryId(3), 0, 0
]);
});
it('should calculate childMatchedQueries for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [['q1', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.None, [['q2', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [['q3', QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[2, QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[3, QueryValueType.Provider]], 0, AService, []),
]);
expect(childMatchedQueries(vd)).toEqual([['q1'], ['q1'], [], ['q2', 'q3'], [], []]);
expect(childMatchedQueries(vd)).toEqual([
filterQueryId(1), filterQueryId(1), 0, filterQueryId(2) | filterQueryId(3), 0, 0
]);
});
it('should included embedded views into childMatchedQueries', () => {
@ -231,12 +236,12 @@ export function main() {
() => viewDef(
ViewFlags.None,
[
elementDef(NodeFlags.None, [['q1', QueryValueType.Provider]], null, 0, 'span'),
elementDef(NodeFlags.None, [[1, QueryValueType.Provider]], null, 0, 'span'),
]))
]);
// Note: the template will become a sibling to the anchor once stamped out,
expect(childMatchedQueries(vd)).toEqual([['q1'], []]);
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), 0]);
});
});
});