Skip to content

Commit 93864b5

Browse files
author
Alexander Vakrilov
authored
Merge pull request #288 from NativeScript/list-view-samples
FIX: ListView infinite change detection
2 parents 6028514 + 9d03c28 commit 93864b5

File tree

9 files changed

+287
-137
lines changed

9 files changed

+287
-137
lines changed

Diff for: nativescript-angular/directives/list-view-comp.ts

+70-54
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
import {
2-
Component,
3-
DoCheck,
4-
ElementRef,
2+
Component,
3+
DoCheck,
4+
OnDestroy,
5+
ElementRef,
56
ViewContainerRef,
6-
TemplateRef,
7-
ContentChild,
7+
TemplateRef,
8+
ContentChild,
89
EmbeddedViewRef,
9-
HostListener,
10-
IterableDiffers,
10+
IterableDiffers,
1111
IterableDiffer,
1212
ChangeDetectorRef,
1313
EventEmitter,
1414
ViewChild,
1515
Output,
16-
ChangeDetectionStrategy} from '@angular/core';
16+
ChangeDetectionStrategy } from '@angular/core';
1717
import {isBlank} from '@angular/core/src/facade/lang';
1818
import {isListLikeIterable} from '@angular/core/src/facade/collection';
19-
import {Observable as RxObservable} from 'rxjs'
2019
import {ListView} from 'ui/list-view';
2120
import {View} from 'ui/core/view';
22-
import {NgView} from '../view-util';
2321
import {ObservableArray} from 'data/observable-array';
2422
import {LayoutBase} from 'ui/layouts/layout-base';
25-
import {rendererLog, rendererError} from "../trace";
23+
import {listViewLog} from "../trace";
24+
2625
const NG_VIEW = "_ngViewRef";
2726

2827
export class ListItemContext {
@@ -51,15 +50,15 @@ export interface SetupItemViewArgs {
5150
inputs: ['items'],
5251
changeDetection: ChangeDetectionStrategy.OnPush
5352
})
54-
export class ListViewComponent {
53+
export class ListViewComponent implements DoCheck, OnDestroy {
5554
public get nativeElement(): ListView {
5655
return this.listView;
5756
}
58-
57+
5958
private listView: ListView;
6059
private _items: any;
6160
private _differ: IterableDiffer;
62-
61+
6362
@ViewChild('loader', { read: ViewContainerRef }) loader: ViewContainerRef;
6463

6564
@Output() public setupItemView: EventEmitter<SetupItemViewArgs> = new EventEmitter<SetupItemViewArgs>();
@@ -73,21 +72,24 @@ export class ListViewComponent {
7372
needDiffer = false;
7473
}
7574
if (needDiffer && !this._differ && isListLikeIterable(value)) {
76-
this._differ = this._iterableDiffers.find(this._items).create(this._cdr, (index, item) => { return item;});
75+
this._differ = this._iterableDiffers.find(this._items).create(this._cdr, (index, item) => { return item; });
7776
}
77+
7878
this.listView.items = this._items;
7979
}
8080

81-
private timerId: number;
82-
private doCheckDelay = 5;
83-
8481
constructor(private _elementRef: ElementRef,
85-
private _iterableDiffers: IterableDiffers,
86-
private _cdr: ChangeDetectorRef) {
82+
private _iterableDiffers: IterableDiffers,
83+
private _cdr: ChangeDetectorRef) {
8784
this.listView = _elementRef.nativeElement;
85+
86+
this.listView.on("itemLoading", this.onItemLoading, this);
87+
}
88+
89+
ngOnDestroy() {
90+
this.listView.off("itemLoading", this.onItemLoading, this);
8891
}
8992

90-
@HostListener("itemLoading", ['$event'])
9193
public onItemLoading(args) {
9294
if (!this.itemTemplate) {
9395
return;
@@ -99,20 +101,22 @@ export class ListViewComponent {
99101
let viewRef: EmbeddedViewRef<ListItemContext>;
100102

101103
if (args.view) {
102-
rendererLog("ListView.onItemLoading: " + index + " - Reusing existing view");
104+
listViewLog("onItemLoading: " + index + " - Reusing existing view");
103105
viewRef = args.view[NG_VIEW];
104106
// getting angular view from original element (in cases when ProxyViewContainer is used NativeScript internally wraps it in a StackLayout)
105107
if (!viewRef) {
106108
viewRef = (args.view._subViews && args.view._subViews.length > 0) ? args.view._subViews[0][NG_VIEW] : undefined;
107109
}
108110
}
109111
else {
110-
rendererLog("ListView.onItemLoading: " + index + " - Creating view from template");
112+
listViewLog("onItemLoading: " + index + " - Creating view from template");
111113
viewRef = this.loader.createEmbeddedView(this.itemTemplate, new ListItemContext(), 0);
112114
args.view = getSingleViewFromViewRef(viewRef);
113115
args.view[NG_VIEW] = viewRef;
114116
}
115117
this.setupViewRef(viewRef, currentItem, index);
118+
119+
this.detectChangesOnChild(viewRef, index);
116120
}
117121

118122
public setupViewRef(viewRef: EmbeddedViewRef<ListItemContext>, data: any, index: number): void {
@@ -126,49 +130,61 @@ export class ListViewComponent {
126130
context.even = (index % 2 == 0);
127131
context.odd = !context.even;
128132

129-
this.setupItemView.next({view: viewRef, data: data, index: index, context: context});
133+
this.setupItemView.next({ view: viewRef, data: data, index: index, context: context });
130134
}
131135

132-
ngDoCheck() {
133-
if (this.timerId) {
134-
clearTimeout(this.timerId);
135-
}
136+
private detectChangesOnChild(viewRef: EmbeddedViewRef<ListItemContext>, index: number) {
137+
// Manually detect changes in child view ref
138+
// TODO: Is there a better way of getting viewRef's change detector
139+
const childChangeDetector = <ChangeDetectorRef>(<any>viewRef);
136140

137-
this.timerId = setTimeout(() => {
138-
clearTimeout(this.timerId);
139-
if (this._differ) {
140-
var changes = this._differ.diff(this._items);
141-
if (changes) {
142-
this.listView.refresh();
143-
}
141+
listViewLog("Manually detect changes in child: " + index)
142+
childChangeDetector.markForCheck();
143+
childChangeDetector.detectChanges();
144+
}
145+
146+
ngDoCheck() {
147+
if (this._differ) {
148+
listViewLog("ngDoCheck() - execute differ")
149+
const changes = this._differ.diff(this._items);
150+
if (changes) {
151+
listViewLog("ngDoCheck() - refresh")
152+
this.listView.refresh();
144153
}
145-
}, this.doCheckDelay);
154+
}
146155
}
147156
}
148157

149-
function getSingleViewFromViewRef(viewRef: EmbeddedViewRef<any>): View {
150-
var getSingleViewRecursive = (nodes: Array<any>, nestLevel: number) => {
151-
var actualNodes = nodes.filter((n) => !!n && n.nodeName !== "#text");
152158

153-
if (actualNodes.length === 0) {
154-
throw new Error("No suitable views found in list template! Nesting level: " + nestLevel);
155-
}
156-
else if (actualNodes.length > 1) {
157-
throw new Error("More than one view found in list template! Nesting level: " + nestLevel);
159+
function getSingleViewRecursive(nodes: Array<any>, nestLevel: number) {
160+
const actualNodes = nodes.filter((n) => !!n && n.nodeName !== "#text");
161+
162+
if (actualNodes.length === 0) {
163+
throw new Error("No suitable views found in list template! Nesting level: " + nestLevel);
164+
}
165+
else if (actualNodes.length > 1) {
166+
throw new Error("More than one view found in list template! Nesting level: " + nestLevel);
167+
}
168+
else {
169+
if (actualNodes[0]) {
170+
let parentLayout = actualNodes[0].parent;
171+
if (parentLayout instanceof LayoutBase) {
172+
parentLayout.removeChild(actualNodes[0]);
173+
}
174+
return actualNodes[0];
158175
}
159176
else {
160-
if (actualNodes[0]) {
161-
let parentLayout = actualNodes[0].parent;
162-
if (parentLayout instanceof LayoutBase) {
163-
parentLayout.removeChild(actualNodes[0]);
164-
}
165-
return actualNodes[0];
166-
}
167-
else {
168-
return getSingleViewRecursive(actualNodes[0].children, nestLevel + 1)
169-
}
177+
return getSingleViewRecursive(actualNodes[0].children, nestLevel + 1)
170178
}
171179
}
180+
}
172181

182+
function getSingleViewFromViewRef(viewRef: EmbeddedViewRef<any>): View {
173183
return getSingleViewRecursive(viewRef.rootNodes, 0);
174184
}
185+
186+
const changeDetectorMode = ["CheckOnce", "Checked", "CheckAlways", "Detached", "OnPush", "Default"];
187+
const changeDetectorStates = ["Never", "CheckedBefore", "Error"];
188+
function getChangeDetectorState(cdr: any) {
189+
return "Mode: " + changeDetectorMode[parseInt(cdr.cdMode)] + " State: " + changeDetectorStates[parseInt(cdr.cdState)];
190+
}

Diff for: nativescript-angular/trace.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {write, categories, messageType} from "trace";
22

33
export const rendererTraceCategory = "ns-renderer";
44
export const routerTraceCategory = "ns-router";
5+
export const listViewTraceCategory = "ns-list-view";
56

67
export function rendererLog(msg): void {
78
write(msg, rendererTraceCategory);
@@ -18,3 +19,7 @@ export function routerLog(message: string): void {
1819
export function styleError(message: string): void {
1920
write(message, categories.Style, messageType.error);
2021
}
22+
23+
export function listViewLog(message: string): void {
24+
write(message, listViewTraceCategory);
25+
}

Diff for: ng-sample/app/app.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88
// this import should be first in order to load some required settings (like globals and reflect-metadata)
99
import { nativeScriptBootstrap } from "nativescript-angular/application";
1010
import { NS_ROUTER_PROVIDERS } from "nativescript-angular/router";
11-
import { rendererTraceCategory, routerTraceCategory } from "nativescript-angular/trace";
11+
import { rendererTraceCategory, routerTraceCategory, listViewTraceCategory } from "nativescript-angular/trace";
1212

1313
import trace = require("trace");
14-
trace.setCategories(routerTraceCategory);
14+
// trace.setCategories(rendererTraceCategory);
15+
// trace.setCategories(routerTraceCategory);
16+
trace.setCategories(listViewTraceCategory);
1517
trace.enable();
1618

1719
import {RendererTest} from './examples/renderer-test';
1820
import {TabViewTest} from './examples/tab-view/tab-view-test';
1921
import {Benchmark} from './performance/benchmark';
2022
import {ListTest} from './examples/list/list-test';
21-
import {ListTestAsync} from "./examples/list/list-test-async";
23+
import {ListTestAsync, ListTestFilterAsync} from "./examples/list/list-test-async";
2224
import {ImageTest} from "./examples/image/image-test";
2325
import {NavigationTest} from "./examples/navigation/navigation-test";
2426
import {ActionBarTest} from "./examples/action-bar/action-bar-test";
@@ -30,13 +32,14 @@ import {LoginTest} from "./examples/navigation/login-test";
3032
//nativeScriptBootstrap(RendererTest);
3133
//nativeScriptBootstrap(TabViewTest);
3234
//nativeScriptBootstrap(Benchmark);
33-
//nativeScriptBootstrap(ListTest);
34-
//nativeScriptBootstrap(ListTestAsync);
35+
// nativeScriptBootstrap(ListTest);
36+
// nativeScriptBootstrap(ListTestAsync);
37+
nativeScriptBootstrap(ListTestFilterAsync);
3538
//nativeScriptBootstrap(ImageTest);
3639
//nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS]);
3740
//nativeScriptBootstrap(ActionBarTest, [NS_ROUTER_PROVIDERS], { startPageActionBarHidden: false });
3841
//nativeScriptBootstrap(ActionBarTest, [NS_ROUTER_PROVIDERS]);
3942
//nativeScriptBootstrap(ModalTest);
4043
//nativeScriptBootstrap(PlatfromDirectivesTest);
4144
//nativeScriptBootstrap(RouterOutletTest, [NS_ROUTER_PROVIDERS]);
42-
nativeScriptBootstrap(LoginTest, [NS_ROUTER_PROVIDERS]);
45+
// nativeScriptBootstrap(LoginTest, [NS_ROUTER_PROVIDERS]);

Diff for: ng-sample/app/examples/list/data.service.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Injectable } from '@angular/core';
2+
import { BehaviorSubject } from "rxjs/BehaviorSubject";
3+
4+
export class DataItem {
5+
constructor(public id: number, public name: string) { }
6+
}
7+
8+
@Injectable()
9+
export class DataService {
10+
private _intervalId;
11+
private _counter = 0;
12+
private _currentItems: Array<DataItem>;
13+
14+
public items$: BehaviorSubject<Array<DataItem>>;
15+
16+
constructor() {
17+
this._currentItems = [];
18+
for (var i = 0; i < 3; i++) {
19+
this.appendItem()
20+
}
21+
22+
this.items$ = new BehaviorSubject(this._currentItems);
23+
}
24+
25+
public startAsyncUpdates() {
26+
if (this._intervalId) {
27+
throw new Error("Updates are already started");
28+
}
29+
30+
this._intervalId = setInterval(() => {
31+
this.appendItem();
32+
this.publishUpdates();
33+
}, 200);
34+
35+
}
36+
37+
public stopAsyncUpdates() {
38+
if (!this._intervalId) {
39+
throw new Error("Updates are not started");
40+
}
41+
42+
clearInterval(this._intervalId);
43+
this._intervalId = undefined;
44+
}
45+
46+
private publishUpdates() {
47+
this.items$.next([...this._currentItems]);
48+
}
49+
50+
private appendItem() {
51+
this._currentItems.push(new DataItem(this._counter, "data item " + this._counter));
52+
this._counter++;
53+
}
54+
}

Diff for: ng-sample/app/examples/list/list-test-async.css

-3
This file was deleted.

0 commit comments

Comments
 (0)