NativeScript Core

List View

Using a ListView control inside NativeScript app requires some special attention due to the complexity of the NativeScript implementation of the component, with custom item templates, bindings and so on.

The NativeScript modules provides a custom component which simplifies the way native ListView is used.

const listViewModule = require("tns-core-modules/ui/list-view");
import { ItemEventData, ListView } from "tns-core-modules/ui/list-view";

Note: Using the ListView component inside a ScrollView or ScrollView inside the ListView's items can lead to a poor user interface performance and can reflect the user experience. For avoiding those issues, we should specify the height explicitly for the ListView in the scenario when the ListView is nested in ScrollView and the ScrollView's height - when the component is used inside the ListView. Example 1 (ListView in ScrollView):

<ScrollView>
    <StackLayout>
        <ListView height="150" items="">
                <ListView.itemTemplate>
                    <!-- ....... -->
                </ListView.itemTemplate>
        </ListView>
    </StackLayout>
</ScrollView>

Example 2 (ScrollView in ListView):

<ListView items="">
    <ListView.itemTemplate>
        <StackLayout class="list-group-item">
            <ScrollView height="150" >
                <!-- ....... -->
            </ScrollView>
        </StackLayout>
    </ListView.itemTemplate>
</ListView>

Basics

The example demonstrates how to set a ListView component in XML page and how to bind its items property to a collection in the view model. In the sample is shown how to define custom UI while using ListView's itemTemplate.

<ListView items="{{ myTitles }}" 
          itemTap="onItemTap" 
          loaded="{{ onListViewLoaded }}" 
          separatorColor="orangered" rowHeight="50"
          class="list-group" id="listView" row="2">
    <ListView.itemTemplate>
        <!-- The item template can only have a single root view container (e.g. GriLayout, StackLayout, etc.) -->
        <StackLayout class="list-group-item">
            <Label text="{{ title || 'Downloading...' }}" textWrap="true" class="title" />
        </StackLayout>
    </ListView.itemTemplate>
</ListView>
const fromObject = require("tns-core-modules/data/observable").fromObject;

function onNavigatingTo(args) {
    const page = args.object;
    const vm = fromObject({
        // Setting the listview binding source
        myTitles: [
            { title: "The Da Vinci Code" },
            { title: "Harry Potter and the Chamber of Secrets" },
            { title: "The Alchemist" },
            { title: "The Godfather" },
            { title: "Goodnight Moon" },
            { title: "The Hobbit" }
        ]
    });
    page.bindingContext = vm;
}
exports.onNavigatingTo = onNavigatingTo;

function onListViewLoaded(args) {
    const listView = args.object;
}
exports.onListViewLoaded = onListViewLoaded;

function onItemTap(args) {
    const index = args.index;
    console.log(`Second ListView item tap ${index}`);
}
exports.onItemTap = onItemTap;
import { EventData, fromObject } from "tns-core-modules/data/observable";
import { ListView, ItemEventData } from "tns-core-modules/ui/list-view";
import { Page } from "tns-core-modules/ui/page";

export function onNavigatingTo(args: EventData) {
    const page = <Page>args.object;
    const vm = fromObject({
        // Setting the listview binding source
        myTitles: [
            { title: "The Da Vinci Code" },
            { title: "Harry Potter and the Chamber of Secrets" },
            { title: "The Alchemist" },
            { title: "The Godfather" },
            { title: "Goodnight Moon" },
            { title: "The Hobbit" }
        ]
    });
    page.bindingContext = vm;
}

export function onListViewLoaded(args: EventData) {
    const listView = <ListView>args.object;
}

export function onItemTap(args: ItemEventData) {
    const index = args.index;
    console.log(`Second ListView item tap ${index}`);
}

Note: The ListView's item template can contain only a single root view container.

In the example above, the items source property (myTitles) is an array and its members are not observable objects. This means that adding or removing array member won't trigger a property change. To update the UI, you will need to explicitly refresh the listview after an item is added or removed while using the refresh method.

const listView = page.getViewById("listView");
page.bindingContext.myTitles.push({ title: "Game of Thrones" });
// Manually trigger the update so that the new color is shown.
listView.refresh();
const listView = page.getViewById("listView");
page.bindingContext.myTitles.push({ title: "Game of Thrones" });
// Manually trigger the update so that the new color is shown.
listView.refresh();

Tip: Instead of manually triggering the UI update with the help of ListView's refresh method, NativeScript provides the ObservableArray. Using an ObservableArray for your listview's items surce will make its members an observable objects and adding/removing an array item will automatically update the UI.

const ObservableArray = require("tns-core-modules/data/observable-array").ObservableArray;
// Change the items source from Array to the NativeScript's ObservableArray
const titlesArray = new ObservableArray([
    { title: "The Da Vinci Code" },
    { title: "Harry Potter and the Chamber of Secrets" },
    { title: "The Alchemist" },
    { title: "The Godfather" },
    { title: "Goodnight Moon" },
    { title: "The Hobbit" }
]);

Improve this document

Demo Source


Code Behind

Creating a ListView programmatically and setting up its items and UI for each item.

const container = page.getViewById("container");

const listView = new listViewModule.ListView();
listView.className = "list-group";
listView.items = listViewArray;
// The itemLoading event is used to create the UI for each item that is shown in the ListView.
listView.on(listViewModule.ListView.itemLoadingEvent, (args) => {
    if (!args.view) {
        // Create label if it is not already created.
        args.view = new Label();
        args.view.className = "list-group-item";
    }
    (args.view).text = listViewArray.getItem(args.index).title;

});
listView.on(listViewModule.ListView.itemTapEvent, (args) => {
    const tappedItemIndex = args.index;
    const tappedItemView = args.view;
    dialogs.alert(`Index: ${tappedItemIndex} View: ${tappedItemView}`)
        .then(() => {
            console.log("Dialog closed!");
        });
});

container.addChild(listView);
const listView = new ListView();
listView.className = "list-group";
listView.items = listViewArray;
// The itemLoading event is used to create the UI for each item that is shown in the ListView.
listView.on(ListView.itemLoadingEvent, (args: ItemEventData) => {
    if (!args.view) {
        // Create label if it is not already created.
        args.view = new Label();
        args.view.className = "list-group-item";
    }
    (<any>args.view).text = listViewArray.getItem(args.index).title;

});
listView.on(ListView.itemTapEvent, (args: ItemEventData) => {
    const tappedItemIndex = args.index;
    const tappedItemView = args.view;
    alert(`Index: ${tappedItemIndex} View: ${tappedItemView}`)
        .then(() => {
            console.log("Dialog closed!");
        });
});

Improve this document

Demo Source


Events

In the example is shown how to set up itemTap and loadMoreItems events as well as how to set up the needed callback methods in the Code-Behind.

<ListView row="0" items="{{ listArray }}" itemTap="onItemTap" loadMoreItems="onLoadMoreItems" class="list-group">
    <ListView.itemTemplate>
        <StackLayout class="list-group-item">
            <Label text="{{ title || 'Downloading...' }}" textWrap="true" class="title" />
        </StackLayout>
    </ListView.itemTemplate>
</ListView>  
// The event will be raise when an item inside the ListView is tapped.
function onItemTap(args) {
    const index = args.index;
    dialogs.alert(`ListView item tap ${index}`).then(() => {
        console.log("Dialog closed!");
    });
}
exports.onItemTap = onItemTap;

// The event will be raised when the ListView is scrolled so that the last item is visible. This even is intended to be used to add additional data in the ListView.
function onLoadMoreItems(args) {
    if (loadMore) {
        console.log("ListView -> LoadMoreItemsEvent");
        setTimeout(() => {
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
        }, 3000);

        loadMore = false;
    }
}
exports.onLoadMoreItems = onLoadMoreItems;
// The event will be raise when an item inside the ListView is tapped.
export function onItemTap(args: ItemEventData) {
    const index = args.index;
    alert(`ListView item tap ${index}`).then(() => {
        console.log("Dialog closed!");
    });
}
// The event will be raised when the ListView is scrolled so that the last item is visible.
// This even is intended to be used to add additional data in the ListView.
export function onLoadMoreItems(args: ItemEventData) {
    if (loadMore) {
        console.log("ListView -> LoadMoreItemsEvent");
        setTimeout(() => {
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
            listArray.push(
                moreListItems.getItem(
                    Math.floor(Math.random() * moreListItems.length)
                )
            );
        }, 3000);

        loadMore = false;
    }
}

Improve this document

Demo Source


Multiple Templates Selector Function

The example shows, how to specify the item template selector as a function in the code-behind file

In case your item template selector involves complicated logic which cannot be expressed with an expression, you can create an item template selector function in the code behind of the page in which the ListView resides. The function receives the respective data item, the row index and the entire ListView items collection as parameters. It has to return the the key of the template to be used based on the supplied information.

<ListView row="3" items="{{ listArray }}"  class="list-group" itemTemplateSelector="selectItemTemplate">
    <ListView.itemTemplates>
        <template key="even">
            <StackLayout class="list-group-item" style.backgroundColor="white">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true"/>
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
        <template key="odd">
            <StackLayout class="list-group-item" style.backgroundColor="gray">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true" />
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
    </ListView.itemTemplates>
</ListView>      
function selectItemTemplate(item, index, items) {
    return index % 2 === 0 ? "even" : "odd";
}
export function selectItemTemplate(item, index, items) {
    return index % 2 === 0 ? "even" : "odd";
}
<ListView row="1" items="{{ listArray }}"  class="list-group" itemTemplateSelector="selectItemTemplateAge">
    <ListView.itemTemplates>
        <template key="green">
            <StackLayout class="list-group-item" style.backgroundColor="green">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true"/>
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
        <template key="red">
            <StackLayout class="list-group-item" style.backgroundColor="red">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true" />
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
    </ListView.itemTemplates>
</ListView>
function selectItemTemplateAge(item, index, items) {
    return item.age > 18 ? "green" : "red";
}
export function selectItemTemplateAge(item, index, items) {
    return item.age > 18 ? "green" : "red";
}

Improve this document

Demo Source


Multiple Templates

The example shows, how to define multiple item templates and an item template selector in the XML

The itemTemplateSelector can be an expression specified directly in XML. The context of the expression is the data item for each row. You can use the special value $index in the item template selector expression which represents the row index.

<ListView row="1" items="{{ listArray }}"  class="list-group" itemTemplateSelector="$index % 2 === 0 ? 'even' : 'odd'">
    <ListView.itemTemplates>
        <template key="even">
            <StackLayout class="list-group-item" style.backgroundColor="white">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true"/>
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
        <template key="odd">
            <StackLayout class="list-group-item" style.backgroundColor="gray">
                <Label text="{{ 'Name: ' + name }}" class="h2" textWrap="true" />
                <Label text="{{ 'Age: ' + age }}"/>
            </StackLayout>
        </template>
    </ListView.itemTemplates>
</ListView>

Improve this document

Demo Source


Native Component

Android iOS
android.widget.ListView UITableView