diff --git a/nativescript-angular/directives/list-view-comp.ts b/nativescript-angular/directives/list-view-comp.ts index 5d35d3df2..613e1bc9e 100644 --- a/nativescript-angular/directives/list-view-comp.ts +++ b/nativescript-angular/directives/list-view-comp.ts @@ -1,28 +1,27 @@ import { - Component, - DoCheck, - ElementRef, + Component, + DoCheck, + OnDestroy, + ElementRef, ViewContainerRef, - TemplateRef, - ContentChild, + TemplateRef, + ContentChild, EmbeddedViewRef, - HostListener, - IterableDiffers, + IterableDiffers, IterableDiffer, ChangeDetectorRef, EventEmitter, ViewChild, Output, - ChangeDetectionStrategy} from '@angular/core'; + ChangeDetectionStrategy } from '@angular/core'; import {isBlank} from '@angular/core/src/facade/lang'; import {isListLikeIterable} from '@angular/core/src/facade/collection'; -import {Observable as RxObservable} from 'rxjs' import {ListView} from 'ui/list-view'; import {View} from 'ui/core/view'; -import {NgView} from '../view-util'; import {ObservableArray} from 'data/observable-array'; import {LayoutBase} from 'ui/layouts/layout-base'; -import {rendererLog, rendererError} from "../trace"; +import {listViewLog} from "../trace"; + const NG_VIEW = "_ngViewRef"; export class ListItemContext { @@ -51,15 +50,15 @@ export interface SetupItemViewArgs { inputs: ['items'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ListViewComponent { +export class ListViewComponent implements DoCheck, OnDestroy { public get nativeElement(): ListView { return this.listView; } - + private listView: ListView; private _items: any; private _differ: IterableDiffer; - + @ViewChild('loader', { read: ViewContainerRef }) loader: ViewContainerRef; @Output() public setupItemView: EventEmitter = new EventEmitter(); @@ -73,21 +72,24 @@ export class ListViewComponent { needDiffer = false; } if (needDiffer && !this._differ && isListLikeIterable(value)) { - this._differ = this._iterableDiffers.find(this._items).create(this._cdr, (index, item) => { return item;}); + this._differ = this._iterableDiffers.find(this._items).create(this._cdr, (index, item) => { return item; }); } + this.listView.items = this._items; } - private timerId: number; - private doCheckDelay = 5; - constructor(private _elementRef: ElementRef, - private _iterableDiffers: IterableDiffers, - private _cdr: ChangeDetectorRef) { + private _iterableDiffers: IterableDiffers, + private _cdr: ChangeDetectorRef) { this.listView = _elementRef.nativeElement; + + this.listView.on("itemLoading", this.onItemLoading, this); + } + + ngOnDestroy() { + this.listView.off("itemLoading", this.onItemLoading, this); } - @HostListener("itemLoading", ['$event']) public onItemLoading(args) { if (!this.itemTemplate) { return; @@ -99,7 +101,7 @@ export class ListViewComponent { let viewRef: EmbeddedViewRef; if (args.view) { - rendererLog("ListView.onItemLoading: " + index + " - Reusing existing view"); + listViewLog("onItemLoading: " + index + " - Reusing existing view"); viewRef = args.view[NG_VIEW]; // getting angular view from original element (in cases when ProxyViewContainer is used NativeScript internally wraps it in a StackLayout) if (!viewRef) { @@ -107,12 +109,14 @@ export class ListViewComponent { } } else { - rendererLog("ListView.onItemLoading: " + index + " - Creating view from template"); + listViewLog("onItemLoading: " + index + " - Creating view from template"); viewRef = this.loader.createEmbeddedView(this.itemTemplate, new ListItemContext(), 0); args.view = getSingleViewFromViewRef(viewRef); args.view[NG_VIEW] = viewRef; } this.setupViewRef(viewRef, currentItem, index); + + this.detectChangesOnChild(viewRef, index); } public setupViewRef(viewRef: EmbeddedViewRef, data: any, index: number): void { @@ -126,49 +130,61 @@ export class ListViewComponent { context.even = (index % 2 == 0); context.odd = !context.even; - this.setupItemView.next({view: viewRef, data: data, index: index, context: context}); + this.setupItemView.next({ view: viewRef, data: data, index: index, context: context }); } - ngDoCheck() { - if (this.timerId) { - clearTimeout(this.timerId); - } + private detectChangesOnChild(viewRef: EmbeddedViewRef, index: number) { + // Manually detect changes in child view ref + // TODO: Is there a better way of getting viewRef's change detector + const childChangeDetector = (viewRef); - this.timerId = setTimeout(() => { - clearTimeout(this.timerId); - if (this._differ) { - var changes = this._differ.diff(this._items); - if (changes) { - this.listView.refresh(); - } + listViewLog("Manually detect changes in child: " + index) + childChangeDetector.markForCheck(); + childChangeDetector.detectChanges(); + } + + ngDoCheck() { + if (this._differ) { + listViewLog("ngDoCheck() - execute differ") + const changes = this._differ.diff(this._items); + if (changes) { + listViewLog("ngDoCheck() - refresh") + this.listView.refresh(); } - }, this.doCheckDelay); + } } } -function getSingleViewFromViewRef(viewRef: EmbeddedViewRef): View { - var getSingleViewRecursive = (nodes: Array, nestLevel: number) => { - var actualNodes = nodes.filter((n) => !!n && n.nodeName !== "#text"); - if (actualNodes.length === 0) { - throw new Error("No suitable views found in list template! Nesting level: " + nestLevel); - } - else if (actualNodes.length > 1) { - throw new Error("More than one view found in list template! Nesting level: " + nestLevel); +function getSingleViewRecursive(nodes: Array, nestLevel: number) { + const actualNodes = nodes.filter((n) => !!n && n.nodeName !== "#text"); + + if (actualNodes.length === 0) { + throw new Error("No suitable views found in list template! Nesting level: " + nestLevel); + } + else if (actualNodes.length > 1) { + throw new Error("More than one view found in list template! Nesting level: " + nestLevel); + } + else { + if (actualNodes[0]) { + let parentLayout = actualNodes[0].parent; + if (parentLayout instanceof LayoutBase) { + parentLayout.removeChild(actualNodes[0]); + } + return actualNodes[0]; } else { - if (actualNodes[0]) { - let parentLayout = actualNodes[0].parent; - if (parentLayout instanceof LayoutBase) { - parentLayout.removeChild(actualNodes[0]); - } - return actualNodes[0]; - } - else { - return getSingleViewRecursive(actualNodes[0].children, nestLevel + 1) - } + return getSingleViewRecursive(actualNodes[0].children, nestLevel + 1) } } +} +function getSingleViewFromViewRef(viewRef: EmbeddedViewRef): View { return getSingleViewRecursive(viewRef.rootNodes, 0); } + +const changeDetectorMode = ["CheckOnce", "Checked", "CheckAlways", "Detached", "OnPush", "Default"]; +const changeDetectorStates = ["Never", "CheckedBefore", "Error"]; +function getChangeDetectorState(cdr: any) { + return "Mode: " + changeDetectorMode[parseInt(cdr.cdMode)] + " State: " + changeDetectorStates[parseInt(cdr.cdState)]; +} diff --git a/nativescript-angular/trace.ts b/nativescript-angular/trace.ts index bd2b695ab..07bf6d120 100644 --- a/nativescript-angular/trace.ts +++ b/nativescript-angular/trace.ts @@ -2,6 +2,7 @@ import {write, categories, messageType} from "trace"; export const rendererTraceCategory = "ns-renderer"; export const routerTraceCategory = "ns-router"; +export const listViewTraceCategory = "ns-list-view"; export function rendererLog(msg): void { write(msg, rendererTraceCategory); @@ -18,3 +19,7 @@ export function routerLog(message: string): void { export function styleError(message: string): void { write(message, categories.Style, messageType.error); } + +export function listViewLog(message: string): void { + write(message, listViewTraceCategory); +} diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 3659877ae..a2c35326d 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -8,17 +8,19 @@ // this import should be first in order to load some required settings (like globals and reflect-metadata) import { nativeScriptBootstrap } from "nativescript-angular/application"; import { NS_ROUTER_PROVIDERS } from "nativescript-angular/router"; -import { rendererTraceCategory, routerTraceCategory } from "nativescript-angular/trace"; +import { rendererTraceCategory, routerTraceCategory, listViewTraceCategory } from "nativescript-angular/trace"; import trace = require("trace"); -trace.setCategories(routerTraceCategory); +// trace.setCategories(rendererTraceCategory); +// trace.setCategories(routerTraceCategory); +trace.setCategories(listViewTraceCategory); trace.enable(); import {RendererTest} from './examples/renderer-test'; import {TabViewTest} from './examples/tab-view/tab-view-test'; import {Benchmark} from './performance/benchmark'; import {ListTest} from './examples/list/list-test'; -import {ListTestAsync} from "./examples/list/list-test-async"; +import {ListTestAsync, ListTestFilterAsync} from "./examples/list/list-test-async"; import {ImageTest} from "./examples/image/image-test"; import {NavigationTest} from "./examples/navigation/navigation-test"; import {ActionBarTest} from "./examples/action-bar/action-bar-test"; @@ -30,8 +32,9 @@ import {LoginTest} from "./examples/navigation/login-test"; //nativeScriptBootstrap(RendererTest); //nativeScriptBootstrap(TabViewTest); //nativeScriptBootstrap(Benchmark); -//nativeScriptBootstrap(ListTest); -//nativeScriptBootstrap(ListTestAsync); +// nativeScriptBootstrap(ListTest); +// nativeScriptBootstrap(ListTestAsync); +nativeScriptBootstrap(ListTestFilterAsync); //nativeScriptBootstrap(ImageTest); //nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS]); //nativeScriptBootstrap(ActionBarTest, [NS_ROUTER_PROVIDERS], { startPageActionBarHidden: false }); @@ -39,4 +42,4 @@ import {LoginTest} from "./examples/navigation/login-test"; //nativeScriptBootstrap(ModalTest); //nativeScriptBootstrap(PlatfromDirectivesTest); //nativeScriptBootstrap(RouterOutletTest, [NS_ROUTER_PROVIDERS]); -nativeScriptBootstrap(LoginTest, [NS_ROUTER_PROVIDERS]); +// nativeScriptBootstrap(LoginTest, [NS_ROUTER_PROVIDERS]); diff --git a/ng-sample/app/examples/list/data.service.ts b/ng-sample/app/examples/list/data.service.ts new file mode 100644 index 000000000..e7139138b --- /dev/null +++ b/ng-sample/app/examples/list/data.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from "rxjs/BehaviorSubject"; + +export class DataItem { + constructor(public id: number, public name: string) { } +} + +@Injectable() +export class DataService { + private _intervalId; + private _counter = 0; + private _currentItems: Array; + + public items$: BehaviorSubject>; + + constructor() { + this._currentItems = []; + for (var i = 0; i < 3; i++) { + this.appendItem() + } + + this.items$ = new BehaviorSubject(this._currentItems); + } + + public startAsyncUpdates() { + if (this._intervalId) { + throw new Error("Updates are already started"); + } + + this._intervalId = setInterval(() => { + this.appendItem(); + this.publishUpdates(); + }, 200); + + } + + public stopAsyncUpdates() { + if (!this._intervalId) { + throw new Error("Updates are not started"); + } + + clearInterval(this._intervalId); + this._intervalId = undefined; + } + + private publishUpdates() { + this.items$.next([...this._currentItems]); + } + + private appendItem() { + this._currentItems.push(new DataItem(this._counter, "data item " + this._counter)); + this._counter++; + } +} \ No newline at end of file diff --git a/ng-sample/app/examples/list/list-test-async.css b/ng-sample/app/examples/list/list-test-async.css deleted file mode 100644 index 8853405b7..000000000 --- a/ng-sample/app/examples/list/list-test-async.css +++ /dev/null @@ -1,3 +0,0 @@ -.test { - background-color: cornflowerblue; -} \ No newline at end of file diff --git a/ng-sample/app/examples/list/list-test-async.ts b/ng-sample/app/examples/list/list-test-async.ts index 5631d22dc..423a4a59f 100644 --- a/ng-sample/app/examples/list/list-test-async.ts +++ b/ng-sample/app/examples/list/list-test-async.ts @@ -1,58 +1,117 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; -import { Observable as RxObservable } from 'rxjs/Observable'; - -export class DataItem { - constructor(public id: number, public name: string) { } -} +import * as Rx from 'rxjs/Observable'; +import { combineLatestStatic } from 'rxjs/operator/combineLatest'; +import { BehaviorSubject } from "rxjs/BehaviorSubject"; +import { DataItem, DataService } from "./data.service" @Component({ selector: 'list-test-async', - styleUrls: ['examples/list/list-test-async.css'], + styleUrls: ['examples/list/styles.css'], + providers: [DataService], + changeDetection: ChangeDetectionStrategy.OnPush, template: ` - - + + + + + + + + + + + + + - `, - changeDetection: ChangeDetectionStrategy.OnPush + ` }) export class ListTestAsync { - public myItems: RxObservable>; + public isUpdating: boolean = false; + constructor(private service: DataService) { + } + + public onItemTap(args) { + console.log("--> ItemTapped: " + args.index); + } - constructor() { - var items = []; - for (var i = 0; i < 3; i++) { - items.push(new DataItem(i, "data item " + i)); + public toggleAsyncUpdates() { + if (this.isUpdating) { + this.service.stopAsyncUpdates(); + } else { + this.service.startAsyncUpdates(); } - - var subscr; - this.myItems = RxObservable.create(subscriber => { - subscr = subscriber; - subscriber.next(items); - return function () { - console.log("Unsubscribe called!!!"); - } - }); - let counter = 2; - let intervalId = setInterval(() => { - counter++; - console.log("Adding " + counter + "-th item"); - items.push(new DataItem(counter, "data item " + counter)); - subscr.next(items); - }, 1000); - - setTimeout(() => { - clearInterval(intervalId); - }, 15000); + this.isUpdating = !this.isUpdating; + } +} + +@Component({ + selector: 'list-test-async-filter', + styleUrls: ['examples/list/styles.css'], + providers: [DataService], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + + + + + + + + + + + + ` +}) +export class ListTestFilterAsync { + public isUpdating: boolean = false; + public filteredItems$: Rx.Observable>; + private filter$ = new BehaviorSubject(false); + + constructor(private service: DataService) { + // Create filteredItems$ by combining the service.items$ and filter$ + this.filteredItems$ = combineLatestStatic(this.service.items$, this.filter$, (data, filter) => { + return filter ? data.filter(v => v.id % 2 === 0) : data; + }); } public onItemTap(args) { - console.log("------------------------ ItemTapped: " + args.index); + console.log("--> ItemTapped: " + args.index); } -} + + public toggleAsyncUpdates() { + if (this.isUpdating) { + this.service.stopAsyncUpdates(); + } else { + this.service.startAsyncUpdates(); + } + + this.isUpdating = !this.isUpdating; + } + + public toogleFilter() { + this.filter$.next(!this.filter$.value); + } +} \ No newline at end of file diff --git a/ng-sample/app/examples/list/list-test.ts b/ng-sample/app/examples/list/list-test.ts index 1b9e669cc..ad3f099c1 100644 --- a/ng-sample/app/examples/list/list-test.ts +++ b/ng-sample/app/examples/list/list-test.ts @@ -1,4 +1,4 @@ -import {Component, Input, WrappedValue, ChangeDetectionStrategy} from '@angular/core'; +import {Component, Input, WrappedValue, ChangeDetectionStrategy, AfterViewChecked, DoCheck} from '@angular/core'; import {Label} from 'ui/label'; import {ObservableArray} from 'data/observable-array'; @@ -8,81 +8,81 @@ class DataItem { @Component({ selector: 'item-component', + styleUrls: ['examples/list/styles.css'], + changeDetection: ChangeDetectionStrategy.OnPush, template: ` - - + + ` }) -export class ItemComponent { +export class ItemComponent implements AfterViewChecked, DoCheck { @Input() data: DataItem; @Input() odd: boolean; @Input() even: boolean; + @Input() index: boolean; constructor() { } + + ngDoCheck() { + console.log("ItemComponent.ngDoCheck: " + this.data.id); + } + + ngAfterViewChecked() { + console.log("ItemComponent.ngAfterViewChecked: " + this.data.id); + } } @Component({ selector: 'list-test', + styleUrls: ['examples/list/styles.css'], directives: [ItemComponent], + changeDetection: ChangeDetectionStrategy.OnPush, template: ` - - - - - - - - - - - - + + + + + + + + - `, - changeDetection: ChangeDetectionStrategy.OnPush + ` // TEMPLATE WITH COMPONENT // - + // IN-PLACE TEMPLATE - //