RadListView input element value jumps when scrolling


#1

Hi,

I am trying to build a list using RadListView with TextField and TextView in each row/cell. The list displays correctly but when the list is long enough to scroll and I enter anything in the input fields, such as “hallo” and I scroll, the “hallo” will randomly move around to a different item.

Example:
List with 50 rows. I enter “hallo” en the textfield of row 1. I scroll down so that row 1 is not visible any more. “hallo” appears in textfield of row 12. I scroll back to row 1 and text field is empty. I scroll down to row 12 and its empty but “hallo” now shows up in textfield of row 18…etc

Here is the code:

import { Component } from "@angular/core";


class DataItem {
    constructor(public id: number, public name: string) { }
}

@Component({
    selector: "orderpage_rad",
    template: `
    <StackLayout>
    <RadListView [items]="myItems">
        <ng-template tkListItemTemplate let-item="item" let-i="index">
        <StackLayout>
            <Label [text]='"index: " + i'></Label>
            <Label [text]='"name: " + item.name'></Label>
            <TextField keyboardType="number" hint="quantity"></TextField>            
            <TextView hint="enter text" editable="true"></TextView>             
        </StackLayout>
        </ng-template>                
    </RadListView>
    </StackLayout>
    `    
})

export class orderComponent_rad {


  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;
        }            
    }

}

I am using Angular with Typescript and so far only testing on Android:
tns --version: 3.1.2
Cross-platform modules version in node_modules/tns-core-modules/package.json: 3.1.0


#2

hi @deyan - wonder if you have an idea on this? I just feel like I’ve seen it before but can’t put a finger on the solution.


#3

A temporary workaround is to swap to the free ListView. I’ve never looked into the error as I haven’t ever had time but i remember the same issue in my app. Swapping to the free version fixed that for me


#4

Hi,
Actually I am having the same issue with ListView I first tried ListView and swapped to RadListView in the hopes that it would fix it.

If you use the same code I pasted above and just change RadlistView with ListView and remove tkListItemTemplate. I get the same result.


#5

Strange, that fixed it for me? I’m not sure then, sorry


#6

Hello @bobster,

The reason for seeing your input assigned to random cells is simple - the List component - be it ListView or RadListView - randomly assigns visual cells to data items while scrolling. This is called UI Virtualization and allows the component to visualize large amounts of data without creating a visual element for each cell. This is done for saving memory (imagine having 1000 items and 1000 Visual Cells for each one of them on a mobile device…).

To prevent this from happening you should define a property on your data item that would contain the input for the TextField and bind the TEXT property of it to that property on the data item.

Does that make sense?

Let me know should you have additional questions.


#7

Hi Deyan,

Thank you very much for the reply. I am sorry to say but I am new to nativescript/angular and your response went over my head :frowning:

Do you mean I should programmatically assign a variable to the TextField and bind to it? Could you perhaps demonstrate what you mean with an example using the code I pasted above?

Is it considered bad practice to have input fields such as a TextField in a list component? If so I should rather change my programming logic in this situation?


#8

@bobster,

My point is that you need to initialize the value of the editor according to the bussines object that it is bound to just as you do with the Labels:

See that the text property is bound to item.name. This makes sure that each time this Label is loaded it visualizes the current item’s name property. The same applies for the TextField. When you type text into it the TextField element is reused for other items as well and your input remains. Therefore you should store the input on your item and bind the TextField’s text to to the data from your source.

I am not quite sure what your scenario is to be able to assess it as a good or bad practice. Maybe if you shed some more light on it I will be able to help.


#9

Hi,

I think I understand what you mean. I changed the code a bit to help me better see what you explained:

import { Component } from "@angular/core";
import { TextField } from "ui/text-field"; //to modify text of textfield by getting id

class DataItem {
    constructor(public id: number, public name: string, public textvalue) { }
}

@Component({
    selector: "orderpage_rad",
    template: `
    <StackLayout>
    <RadListView [items]="myItems">
        <ng-template tkListItemTemplate let-item="item" let-i="index">
        <StackLayout>
            <Label [text]='"index: " + i'></Label>
            <Label [text]='"name: " + item.name'></Label>
            <TextField keyboardType="number" [hint]="'quantity' + i" [id]='"f" + i' [text]="item.textvalue" (textChange)="whatamI($event)"></TextField>            
            <TextView hint="enter text" editable="true" [id]='"v" + i'></TextView>             
        </StackLayout>
        </ng-template>                
    </RadListView>
    </StackLayout>
    `    
})

export class orderComponent_rad {


  public myItems: Array<DataItem>;
  private counter: number;
  private help = "";

    constructor(){

        this.myItems = [];
        this.counter = 0;
        for (var i = 0; i < 50; i++) {
            this.myItems.push(new DataItem(i, "data item " + i, i));
            this.counter = i;
        }            
    }

    private whatamI(args)
    {
        let textField = <TextField>args.object;
        console.log(textField.text);
    }

}

I added textvalue to DataItem and gave it a value in the for loop in the constructor and the use that value as the text property in the TextField. With the function I call on textChange I can clearly see how the elements are being rendered only once they come into view like you explained.

What I now find is that when I change a value in a TextField and scroll away and then back again it will show the value of the textvalue that was assigned in the for loop in the constructor. So am I right in assuming that I now need to find a way to update item.textvalue in the RadListView? Or I need to dynamically create variables and do 2way binding?

As for the good or bad practice using TextFields in listviews. What I want to achieve is this, I get a list of products from a database with quantities and display it in a listview. The TextFields is then used to be able to enter a new quantity for that product should the quantity have changed. At the end of the list there is a submit button that will then take the values entered in each TextField and update the quantity in the database accordingly.

Thanks for all your assistance so far!! :wink:


#10

Hi,

So I finally found a solution with the help from, Hamdi W. All credit goes to him.

Here it is for anybody that might have the same issue in the future:

import { Component, OnInit } from "@angular/core";
import {TextField} from 'tns-core-modules/ui/text-field';

class ProductItem {
    constructor(
        public id: number,
        public name: string, 
        public quantity: number
    ) { }
}

@Component({
    selector: "Home",
    moduleId: module.id,
    template: `
        <StackLayout>
            <Label text='submit' (tap)="submit()"></Label>
            <ListView [items]="products">
                <ng-template let-item="item" let-i="index">
                    <StackLayout>
                        <Label [text]='"name: " + item.name'></Label>
                        <TextField keyboardType="number"
                                   [id]='item.id'
                                   [hint]="'quantity' + i"
                                   [text]='item.quantity'
                                   (returnPress)="onReturn($event)"></TextField>
                        <Label [text]='item.quantity'></Label>
                    </StackLayout>
                </ng-template>
            </ListView>
        </StackLayout>
    `
})
export class HomeComponent{
    public products: Array<ProductItem>;

    constructor(){
        this.products = [];
        for (var i = 0; i < 50; i++) {
            this.products.push(new ProductItem(i, "data item " + i, i));
        }
    }

    private submit()
    {
        //send to server this.products
    }

    public onReturn(args) {
        const {id, text} = <TextField>args.object;

        this.products = this.products.map(product => {
            if(product.id === parseInt(id)){
                product.quantity = parseInt(text);
            }
            return product;
        });
    }
}

The solution is the onReturn() function and you can change (returnPress)=“onReturn($event)”> to (textChange)=“onReturn($event)”


#11

Here is a much simpler answer I found, we can get the same using Nativescript 2 way binding:

We remove this function: (returnPress)=“onReturn($event)” and this [text]=‘item.quantity’ and replace with [(ngModel)]=“products[i].quantity”

Now any changes made in the TextField will automatically update the object and the listview will use the new value when it needs to render the element again.

Here is the code:

import { Component, OnInit } from "@angular/core";
import {TextField} from 'tns-core-modules/ui/text-field';

class ProductItem {
    constructor(
        public id: number,
        public name: string, 
        public quantity: number
    ) { }
}

@Component({
    selector: "orderpage_rad",
    moduleId: module.id,
    template: `
        <StackLayout>
            <Label text='submit' (tap)="submit()"></Label>
            <ListView [items]="products">
                <ng-template let-item="item" let-i="index">
                    <StackLayout>
                        <Label [text]='"name: " + item.name'></Label>
                        <TextField keyboardType="number"
                                   [id]='item.id'
                                   [hint]="'quantity' + i"
                                   [(ngModel)]="products[i].quantity">
                        </TextField>
                        <Label [text]='item.quantity'></Label>
                    </StackLayout>
                </ng-template>
            </ListView>
        </StackLayout>
    `
})
export class orderComponent_rad{
    public products: Array<ProductItem>;

    constructor(){
        this.products = [];
        for (var i = 0; i < 50; i++) {
            this.products.push(new ProductItem(i, "data item " + i, i));
        }
    }

}