Skip to content

Commit dea3ef4

Browse files
rkirovgoderbauer
authored andcommitted
feat(query): view query is properly updated when dom changes.
Fixes a bug in view manager util where sibling injector is not correctly calculated. ViewQuery no longer includes the view's initiating component injector. Includes some refactoring of view methods and a removal of a polymorphic map call. Closes angular#3033 Closes angular#3439
1 parent f66edaf commit dea3ef4

File tree

5 files changed

+112
-74
lines changed

5 files changed

+112
-74
lines changed

modules/angular2/src/change_detection/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {CONST} from 'angular2/src/facade/lang';
33
import {Locals} from './parser/locals';
44
import {BindingRecord} from './binding_record';
55
import {DirectiveIndex, DirectiveRecord} from './directive_record';
6+
import {ChangeDetectorRef} from './change_detector_ref';
67

78
/**
89
* Interface used by Angular to control the change detection strategy for an application.
@@ -50,6 +51,7 @@ export interface ChangeDispatcher {
5051
export interface ChangeDetector {
5152
parent: ChangeDetector;
5253
mode: string;
54+
ref: ChangeDetectorRef;
5355

5456
addChild(cd: ChangeDetector): void;
5557
addShadowDomChild(cd: ChangeDetector): void;

modules/angular2/src/core/compiler/element_injector.ts

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ class _Context {
421421

422422
export class ElementInjector extends TreeNode<ElementInjector> implements DependencyProvider {
423423
private _host: ElementInjector;
424-
private _preBuiltObjects = null;
424+
private _preBuiltObjects: PreBuiltObjects = null;
425425

426426
// Queries are added during construction or linking with a new parent.
427427
// They are removed only through unlinking.
@@ -477,19 +477,35 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
477477
this._host = host;
478478
this._preBuiltObjects = preBuiltObjects;
479479

480-
this._reattachInjectors(imperativelyCreatedInjector);
481-
this._strategy.hydrate();
482-
483480
if (isPresent(host)) {
484481
this._addViewQueries(host);
485482
}
486483

484+
this._reattachInjectors(imperativelyCreatedInjector);
485+
this._strategy.hydrate();
486+
487487
this._addDirectivesToQueries();
488488
this._addVarBindingsToQueries();
489489

490+
// TODO(rado): optimize this call, if view queries are not moved around,
491+
// simply appending to the query list is faster than updating.
492+
this._updateViewQueries();
493+
490494
this.hydrated = true;
491495
}
492496

497+
private _updateViewQueries() {
498+
if (isPresent(this._query0) && this._query0.isViewQuery) {
499+
this._query0.update();
500+
}
501+
if (isPresent(this._query1) && this._query1.isViewQuery) {
502+
this._query1.update();
503+
}
504+
if (isPresent(this._query2) && this._query2.isViewQuery) {
505+
this._query2.update();
506+
}
507+
}
508+
493509
private _debugContext(): any {
494510
var p = this._preBuiltObjects;
495511
var index = p.elementRef.boundElementIndex - p.view.elementOffset;
@@ -651,22 +667,19 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
651667
}
652668

653669
private _addViewQueries(host: ElementInjector): void {
654-
if (isPresent(host._query0) && host._query0.originator == host &&
655-
host._query0.query.isViewQuery)
656-
this._addViewQuery(host._query0);
657-
if (isPresent(host._query1) && host._query1.originator == host &&
658-
host._query1.query.isViewQuery)
659-
this._addViewQuery(host._query1);
660-
if (isPresent(host._query2) && host._query2.originator == host &&
661-
host._query2.query.isViewQuery)
662-
this._addViewQuery(host._query2);
663-
}
664-
665-
private _addViewQuery(queryRef: QueryRef): void {
666-
// TODO(rado): Replace this.parent check with distanceToParent = 1 when
667-
// https://github.com/angular/angular/issues/2707 is fixed.
668-
if (!queryRef.query.descendants && isPresent(this.parent)) return;
669-
this._assignQueryRef(queryRef);
670+
this._addViewQuery(host._query0, host);
671+
this._addViewQuery(host._query1, host);
672+
this._addViewQuery(host._query2, host);
673+
}
674+
675+
private _addViewQuery(queryRef: QueryRef, host: ElementInjector): void {
676+
if (isBlank(queryRef) || !queryRef.isViewQuery || this._hasQuery(queryRef)) return;
677+
if (host._query0.originator == host) {
678+
// TODO(rado): Replace this.parent check with distanceToParent = 1 when
679+
// https://github.com/angular/angular/issues/2707 is fixed.
680+
if (!queryRef.query.descendants && isPresent(this.parent)) return;
681+
this._assignQueryRef(queryRef);
682+
}
670683
}
671684

672685
private _addVarBindingsToQueries(): void {
@@ -694,6 +707,7 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
694707

695708
private _addDirectivesToQuery(queryRef: QueryRef): void {
696709
if (isBlank(queryRef) || queryRef.query.isVarBindingQuery) return;
710+
if (queryRef.isViewQuery && queryRef.originator == this) return;
697711

698712
var matched = [];
699713
this.addDirectivesMatchingQuery(queryRef.query, matched);
@@ -754,42 +768,37 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
754768
this._addParentQueries();
755769
}
756770

771+
unlink(): void {
772+
var parent = this.parent;
773+
this.remove();
774+
this._removeParentQueries(parent);
775+
}
776+
757777
private _addParentQueries(): void {
758778
if (isBlank(this.parent)) return;
759-
if (isPresent(this.parent._query0) && !this.parent._query0.query.isViewQuery) {
760-
this._addQueryToTree(this.parent._query0);
761-
if (this.hydrated) this.parent._query0.update();
762-
}
763-
if (isPresent(this.parent._query1) && !this.parent._query1.query.isViewQuery) {
764-
this._addQueryToTree(this.parent._query1);
765-
if (this.hydrated) this.parent._query1.update();
766-
}
767-
if (isPresent(this.parent._query2) && !this.parent._query2.query.isViewQuery) {
768-
this._addQueryToTree(this.parent._query2);
769-
if (this.hydrated) this.parent._query2.update();
770-
}
779+
this._addParentQuery(this.parent._query0);
780+
this._addParentQuery(this.parent._query1);
781+
this._addParentQuery(this.parent._query2);
771782
}
772783

773-
unlink(): void {
774-
var queriesToUpdate = [];
775-
if (isPresent(this.parent._query0)) {
776-
this._pruneQueryFromTree(this.parent._query0);
777-
queriesToUpdate.push(this.parent._query0);
778-
}
779-
if (isPresent(this.parent._query1)) {
780-
this._pruneQueryFromTree(this.parent._query1);
781-
queriesToUpdate.push(this.parent._query1);
782-
}
783-
if (isPresent(this.parent._query2)) {
784-
this._pruneQueryFromTree(this.parent._query2);
785-
queriesToUpdate.push(this.parent._query2);
784+
private _addParentQuery(query): void {
785+
if (isPresent(query) && !this._hasQuery(query)) {
786+
this._addQueryToTree(query);
787+
if (this.hydrated) query.update();
786788
}
789+
}
787790

788-
this.remove();
789-
// TODO(rado): update should work on view queries too, however currently it
790-
// is not implemented, so we filter to non-view queries.
791-
var nonViewQueries = ListWrapper.filter(queriesToUpdate, (q) => !q.query.isViewQuery);
792-
ListWrapper.forEach(nonViewQueries, (q) => q.update());
791+
private _removeParentQueries(parent: ElementInjector): void {
792+
this._removeParentQuery(parent._query0);
793+
this._removeParentQuery(parent._query1);
794+
this._removeParentQuery(parent._query2);
795+
}
796+
797+
private _removeParentQuery(query: QueryRef) {
798+
if (isPresent(query)) {
799+
this._pruneQueryFromTree(query);
800+
query.update();
801+
}
793802
}
794803

795804
private _pruneQueryFromTree(query: QueryRef): void {
@@ -852,6 +861,12 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
852861
getHost(): ElementInjector { return this._host; }
853862

854863
getBoundElementIndex(): number { return this._proto.index; }
864+
865+
getRootViewInjectors(): ElementInjector[] {
866+
var view = this._preBuiltObjects.view;
867+
return view.getNestedView(view.elementOffset + this.getBoundElementIndex())
868+
.rootElementInjectors;
869+
}
855870
}
856871

857872
interface _ElementInjectorStrategy {
@@ -1133,9 +1148,19 @@ export class QueryRef {
11331148
constructor(public query: Query, public list: QueryList<any>,
11341149
public originator: ElementInjector) {}
11351150

1151+
get isViewQuery(): boolean { return this.query.isViewQuery; }
1152+
11361153
update(): void {
11371154
var aggregator = [];
1138-
this.visit(this.originator, aggregator);
1155+
if (this.query.isViewQuery) {
1156+
// intentionally skipping originator for view queries.
1157+
var rootViewInjectors = this.originator.getRootViewInjectors();
1158+
for (var i = 0; i < rootViewInjectors.length; i++) {
1159+
this.visit(rootViewInjectors[i], aggregator);
1160+
}
1161+
} else {
1162+
this.visit(this.originator, aggregator);
1163+
}
11391164
this.list.reset(aggregator);
11401165
}
11411166

modules/angular2/src/core/compiler/view_manager_utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,14 @@ export class AppViewManagerUtils {
120120
parentView.viewContainers[boundElementIndex] = viewContainer;
121121
}
122122
ListWrapper.insert(viewContainer.views, atIndex, view);
123+
var elementInjector = contextView.elementInjectors[contextBoundElementIndex];
124+
123125
var sibling;
124126
if (atIndex == 0) {
125-
sibling = null;
127+
sibling = elementInjector;
126128
} else {
127129
sibling = ListWrapper.last(viewContainer.views[atIndex - 1].rootElementInjectors);
128130
}
129-
var elementInjector = contextView.elementInjectors[contextBoundElementIndex];
130131
for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) {
131132
if (isPresent(elementInjector.parent)) {
132133
view.rootElementInjectors[i].linkAfter(elementInjector.parent, sibling);

modules/angular2/test/core/compiler/query_integration_spec.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ export function main() {
355355

356356
view.detectChanges();
357357

358-
expect(q.query.map((d: TextDirective) => d.text)).toEqual(["self", "1", "2", "3"]);
358+
expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3"]);
359359

360360
async.done();
361361
});
@@ -407,26 +407,29 @@ export function main() {
407407
});
408408
}));
409409

410-
/* TODO(rado): fix and reenable.
411410

412411
it('should maintain directives in pre-order depth-first DOM order after dynamic insertion',
413-
inject([TestComponentBuilder, AsyncTestCompleter], (tcb:TestComponentBuilder, async) => {
414-
var template = '<needs-view-query-order #q text="self"></needs-view-query-order>';
412+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
413+
var template = '<needs-view-query-order #q></needs-view-query-order>';
414+
415+
tcb.overrideTemplate(MyComp, template)
416+
.createAsync(MyComp)
417+
.then((view) => {
418+
var q: NeedsViewQueryOrder = view.componentViewChildren[0].getLocal("q");
415419

416-
tcb.overrideTemplate(MyComp, template)
417-
.createAsync(MyComp)
418-
.then((view) => {
419-
var q:NeedsViewQueryOrder = view.componentViewChildren[0].getLocal("q");
420+
view.detectChanges();
420421

421-
view.detectChanges();
422+
expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3", "4"]);
423+
424+
q.list = ["-3", "2"];
425+
view.detectChanges();
422426

423-
expect(q.query.length).toBe(4);
424-
expect(q.query.first.text).toEqual("1");
425-
expect(q.query.first.text).toEqual("4");
426427

427-
async.done();
428-
});
429-
}));*/
428+
expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "-3", "2", "4"]);
429+
430+
async.done();
431+
});
432+
}));
430433
});
431434
});
432435
}
@@ -551,17 +554,23 @@ class NeedsViewQueryNestedIf {
551554
}
552555

553556

557+
// TODO(rado): once https://github.com/angular/angular/issues/3438 is resolved
558+
// duplicate the test without InertDirective.
554559
@Component({selector: 'needs-view-query-order'})
555560
@View({
556-
directives: [NgFor, TextDirective],
557-
template: '<div text="1">' +
558-
'<div *ng-for="var i of [\'2\', \'3\']" [text]="i"></div>' +
559-
'<div text="4">'
561+
directives: [NgFor, TextDirective, InertDirective],
562+
template: '<div dir><div text="1"></div>' +
563+
'<div *ng-for="var i of list" [text]="i"></div>' +
564+
'<div text="4"></div></div>'
560565
})
561566
@Injectable()
562567
class NeedsViewQueryOrder {
563568
query: QueryList<TextDirective>;
564-
constructor(@ViewQuery(TextDirective) query: QueryList<TextDirective>) { this.query = query; }
569+
list: string[];
570+
constructor(@ViewQuery(TextDirective, {descendants: true}) query: QueryList<TextDirective>) {
571+
this.query = query;
572+
this.list = ['2', '3'];
573+
}
565574
}
566575

567576
@Component({selector: 'needs-tpl'})

modules/angular2/test/core/compiler/view_manager_utils_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ export function main() {
171171
createViews(2);
172172
utils.attachViewInContainer(parentView, 0, contextView, 1, 0, childView);
173173
expect(childView.rootElementInjectors[0].spy('linkAfter'))
174-
.toHaveBeenCalledWith(contextView.elementInjectors[0], null);
174+
.toHaveBeenCalledWith(contextView.elementInjectors[0],
175+
contextView.elementInjectors[1]);
175176
});
176177
});
177178

0 commit comments

Comments
 (0)