List View
Using a ListView
control inside Angular app
requires some special attention due to the complexity of the
NativeScript control like custom item template, bindings and so
on.
In this article we will cover the following topics:
Using the ListView Component
NativeScript-angular plugin provides a custom Angular component which simplifies the way native ListView should be used. Following is an example of how to add ListView to your page (with some clarifications later):
// list-test.html
<ListView [items]="myItems" (itemTap)="onItemTap($event)">
<ng-template let-item="item" let-i="index" let-odd="odd" let-even="even">
<StackLayout [class.odd]="odd" [class.even]="even">
<Label [text]='"index: " + i'></Label>
<Label [text]='"[" + item.id +"] " + item.name'></Label>
</StackLayout>
</ng-template>
</ListView>
import {Component, Input, ChangeDetectionStrategy} from '@angular/core';
class DataItem {
constructor(public id: number, public name: string) { }
}
@Component({
selector: 'list-test',
styleUrls: ['list-test.css'],
templateUrl: 'list-test.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListTest {
public myItems: Array<DataItem>;
private counter: number;
constructor() {
this.myItems = [];
this.counter = 0;
for (var i = 0; i < 50; i++) {
this.myItems.push(new DataItem(i, "data item " + i));
this.counter = i;
}
}
public onItemTap(args) {
console.log("------------------------ ItemTapped: " + args.index);
}
}
// list-test.css
.odd {
background-color: red;
}
.even {
background-color: blue;
}
As shown there is nothing complex in a way ListView component is used, but some points need clarifications.
-
items - The
items
property is bound in a standard way to a ordinary JavaScript Array. Since the JavaScript Array object does not have observable or change notifications capabilities, supporting such a scenario counts on Angular's change detection mechanism for notification that something has changed. Be aware that the process of checking that anything is changed within an Array could take a lot of time on large arrays (including a memory issue) leading to a possible performance issue. So consider using another kind of source with large collections. A great example of this kind of data source is the NativeScript ObservableArray. -
template - The template tag is used to define a template which will be used for the User Interface of every ListView item. As shown there are some standard Angular optional variables marked with
let-
that are preset for every data item:let-item
- the data item itself.-
let-i
- the index of the data item (inside data source) -
let-odd
- represents if the index of the data item is an odd number -
let-even
- represents if the index of the data item is an even number - Inside the actual template it is shown how to use these variables.
-
itemTap event -
itemTap
event is an event that comes from the NativeScript ListView (the underlying control behind the NativeScript-Angular ListView component). There is nothing special here—just a normal one-way to source binding with a corresponding functiononItemTap
inside the code-behind file.
This is a typical usage of the ListView component, however if the business case requires it, there are a few options for customizations.
Customizing the ListView
The most common customization of ListView control is customizing
the item template. Everything inside the
<ng-template>
tag will be used as the item
template and will be generated for each item. Another possible
customization is connected with the creation of a different
item. Usually with a pure NativeScript application, the
itemLoading
event could be used to accomplish this
customization. Unfortunately this event cannot be used with a
NativeScript-Angular app, since the NativeScript-Angular plugin
uses this event to create an Angular view which will be inserted
into the Angular virtual dom. However, the NativeScript-Angular
ListView component provides an option to customize the created
Angular view before adding it to the visual tree. This option is
available via the setupItemView
event. Here is a
small example how to use this event:
<GridLayout rows="*">
<ListView [items]="myItems" (setupItemView)="onSetupItemView($event)">
<ng-template let-item="item" let-i="index" let-third="third">
<StackLayout [class.third]="third">
<Label [text]='"index: " + i'></Label>
<Label [text]='"[" + item.id +"] " + item.name'></Label>
</StackLayout>
</ng-template>
</ListView>
</GridLayout>
import {SetupItemViewArgs} from "nativescript-angular/directives";
...
onSetupItemView(args: SetupItemViewArgs) {
args.view.context.third = (args.index % 3 === 0);
}
In order to see the result just add third
css class
in app.css or in styles of your custom component:
.third {
background-color: lime;
}
And the result is:
Using an Item Template
Another popular scenario is using a separate component for the ListView template. Using a custom control within a ListView actually is very simple.
@Component({
selector: 'item-component',
template: `
<StackLayout>
<Label *ngFor="let element of data.list" [text]="element.text"></Label>
</StackLayout>
`
})
export class ItemComponent {
@Input() data: any;
constructor() { }
}
@Component({
selector: 'list-test',
template: `
<GridLayout rows="*">
<ListView [items]="myItems">
<ng-template let-item="item">
<item-component [data]="item"></item-component>
</ng-template>
</ListView>
</GridLayout>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListTest {
public myItems: Array<any>;
private counter: number;
constructor() {
var list = [{"text": "a"}, {"text": "b"}];
var list1 = [{"text": "c"}, {"text": "d"}];
this.myItems = [{"list": list}, {"list": list1}];
}
}
As shown, just create a custom component and add it to the
directives of the host component. Another interesting part is
how data
is passed to the child control (via @Input
decorator).
Using Multiple Item Templates
There are scenarios when you want to use different item templates based on the type of the current item (or some other condition). Here is how to do that:
-
Define a list view with multiple templates, giving each one of
them a key using the
nsTemplateKey
directive. -
Set the
itemTemplateSelector
callback for theListView
. This is a function that will be called when each item is rendered and should return the name of the template that should be used for it.
import { Component, Input, Injectable } from "@angular/core";
class DataItem {
private static count = 0;
public id: number;
constructor(public name: string, public isHeader: boolean) {
this.id = DataItem.count++;
}
}
@Component({
selector: "header-component",
template: `<Label [text]='"HEADER: " +data.name'></Label>`
})
export class HeaderComponent {
@Input() data: DataItem;
}
@Component({
selector: "item-component",
template: `<Label [text]='"ITEM: " + data.name'></Label>`
})
export class ItemComponent {
@Input() data: DataItem;
}
@Injectable()
export class DataService {
public getItems(): DataItem[] {
const result = [];
for (let headerIndex = 0; headerIndex < 10; headerIndex++) {
result.push(new DataItem("Header " + headerIndex, true));
for (let i = 1; i < 10; i++) {
result.push(new DataItem(`item ${headerIndex}.${i}`, false));
}
}
return result;
}
}
@Component({
moduleId: module.id,
selector: "list-test",
templateUrl: "./template-selector.component.html"
})
export class ListTemplateSelectorTest {
public myItems: Array<DataItem>;
public templateSelector = (item: DataItem, index: number, items: any) => {
return item.isHeader ? "header" : "item";
}
constructor(private dataService: DataService) {
}
ngOnInit() {
this.myItems = this.dataService.getItems();
}
}
<ListView [items]="myItems" [itemTemplateSelector]="templateSelector">
<ng-template nsTemplateKey="header" let-item="item">
<header-component [data]="item"></header-component>
</ng-template>
<ng-template nsTemplateKey="item" let-item="item">
<item-component [data]="item"></item-component>
</ng-template>
</ListView>
The itemTemplateSelector
property of the
ListView
is not an event. It is
just a property that accepts a callback function, so the regular
property binding syntax
([itemTemplateSelector]="callbackFn
) is used to
bind it to a function in the component. The
itemTemplateSelector
is not implemented as an
EventEmitter
for performance reasons - firing
events triggers angular change detection. Doing this for each
shown item is not necessary, given that the template selector
callback should not have side effects.
Using different item templates for different item types is much
more performant than having an *ngIf
or
ngSwitch
inside a single template. This is because
the actual views used for the different templates are recycled
and reused for each template key. When using *ngIf
,
the actual views are created only after the context (the
data-item) for the element is known so there is no way for the
ListView
component to reuse them.
Using Async Pipe
Generally according to Angular documentation, a pipe is a simple
display-value transformation that can be declared in HTML. A
pipe takes an input and transforms it to a desired output. One
of the built-in Angular pipes is very commonly used with
ListView like controls. This is the async
pipe. The
input of this pipe is either
Promise<Array>
or
Observable<Array>
(Observable actually stands
for
RxJS.Observable. This pipe subscribes to the observable and returns the value
inside it as property value. Following is a simple example of
using async pipe with NativeScript-Angular ListView.
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { Observable as RxObservable } from 'rxjs/Observable';
export class DataItem {
constructor(public id: number, public name: string) { }
}
@Component({
selector: 'list-test-async',
template: `
<GridLayout>
<ListView [items]="myItems | async">
<ng-template let-item="item" let-i="index" let-odd="odd" let-even="even">
<StackLayout [class.odd]="odd" [class.even]="even">
<Label [text]='"index: " + item.name'></Label>
</StackLayout>
</ng-template>
</ListView>
</GridLayout>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListTestAsync {
public myItems: RxObservable<Array<DataItem>>;
constructor() {
var items = [];
for (var i = 0; i < 3; i++) {
items.push(new DataItem(i, "data item " + i));
}
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++;
items.push(new DataItem(counter, "data item " + counter));
subscr.next(items);
}, 1000);
setTimeout(() => {
clearInterval(intervalId);
}, 15000);
}
}
Load More Items
The built-in loadMoreItemsEvent can be used to implement infinite scrolling in your application. Infinite scrolling allows you to load content on demand without the need for pagination.
// list-test.html
<ListView [items]="myItems" (loadMoreItems)="loadMoreItems()">
<ng-template let-item="item" let-i="index">
<Label [text]="item"></Label>
</ng-template>
</ListView>
import { Component } from "@angular/core";
import { EventData } from "tns-core-modules/data/observable";
@Component({
selector: 'list-test',
styleUrls: ['list-test.css'],
template: 'list-test.html'
})
export class ListTest {
public myItems: string[] = [];
public counter = 0;
constructor() {
this.myItems = [];
for (var i = 0; i < 50; i++) {
this.myItems.push("data item " + i);
this.counter = i;
}
}
loadMoreItems() {
// Load more items here.
this.myItems.push("data item " + this.counter)
this.counter += 1;
}
}
Estimated Row Height
The NativeScript property iosEstimatedRowHeight
is
used to improve the scrolling performance on lists with flexible
row heights on the iOS
platform. It comes with a
default value so make sure to set it to something appropriate.
In case you're using flexible row heights in conjunction with
dynamic list contents (which is the case when providing load on
demand functionality) you'll want to set the
iosEstimatedRowHeight
property to 0
.
This will force a precise row height calculation of currently
out of view cells. (Which fixes possible scroll position jumps)