Nativescript-angular: Change detection issue in ListView

Created on 28 Jul 2016  路  31Comments  路  Source: NativeScript/nativescript-angular

Conditional styling in ListView templates don't get consistently applied. Most times they require the item to go off screen and back in, in order for the new status to be reflected.

Gif
repro

Here is a sample to reproduce:

Component:

import {Component} from "@angular/core";
@Component({
    selector: "my-app",
    templateUrl: "app.component.html",
})
export class AppComponent {
    public options: string[] = ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"];

    public selectedOption: string = "";

    public onItemTap(args) {
        this.selectedOption = this.options[args.index];
    }
}

Markup:

<StackLayout>
<ListView #listview [items]="options" separatorColor="white" (itemTap)="onItemTap($event)">
    <template let-item="item" let-i="index" let-odd="odd" let-even="even">
        <GridLayout columns="44, *" height="48">
            <StackLayout colSpan="2" verticalAlignment="bottom" class="line"></StackLayout>      
            <Label col="1" [text]="item" style="font-size:20" [class.active]="selectedOption == item"></Label>
        </GridLayout>
    </template>
</ListView>
</StackLayout>

CSS

.active {
    color:red;
}

backlog list-view ios

Most helpful comment

After trying some modifications to my code, I could solve this issue by doing:

...
this.items[i].is_list = true;
this.items = this.items.slice();

That way, when reassigning to this.items it's own array, the UI got updated.

All 31 comments

It looks like a change detection issue. The list-view has OnPush change detection strategy (for performance reasons). Probably, thats why the items in it are note updated when selectedOption is changed.

One thing I noticed that if you use tap listener on the grid inside the template - it works as expected:

<GridLayout height="48" (tap)="selectedOption = item">

In this case, because the event is executed in the actual item - it triggers its change detector.

I ran into a very similar issue, but adding a (tap) wasn't possible in my case.

However, I did solve it by changing the instance variable type to BehaviorSubject, and using the async pipe in the template.

I am having the same issue, having a class toggled by an item's property (in a list-view).
Is there a way to manually trigger change detector for an item? In my case the property is changed after a request to an API, so I can't do the change in the layout.

Even though your problem is a little bit strange, you could try this:

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

@Component({
    selector: "my-app",
    templateUrl: "app.component.html",
})
export class AppComponent {
    public options: string[] = ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"];

    constructor(private zone: NgZone) { }

    public selectedOption: string = "";

    public onItemTap(args) {
        this.zone.run(() => {
               this.selectedOption = this.options[args.index];
        });
    }
}

Once I had a problem like that and I fixed it by running the code inside the NgZone.

Trying with NgZone didn't work for me.
With RadListView, changing an item's property updates immediately the UI, but since it has a memory leak (an issue was created on telerik/nativescript-ui-feedback) and kills my app after a while, I can't use it.
With ListView, the UI is not updated. Is there any alternative to NgZone that could work?

After trying some modifications to my code, I could solve this issue by doing:

...
this.items[i].is_list = true;
this.items = this.items.slice();

That way, when reassigning to this.items it's own array, the UI got updated.

Same problem. Thanks @eduardoturconi , that worked for me.

Worth noting: This isnt just on angular, I am experiencing this in a Typescript/vanilla app. I fix it by calling refresh() on the listview, though that shouldnt be necessary.

This happens only on iOS, works correctly on Android.

this is really a big problem on iOS, I've tried with NgZone.run, it did't work, also tried with refresh() also don't work at all.

but on android, I don't need NgZone nor refresh to make this kind of change work. it just work perfectly.

When could this be fixed?

Also experience this issue on IOS...seems like it doing the buggy thing when bind function and works perfectly when bind to property

Experiencing the same problem. The issue is specific to IOS for me as well.

Calling refresh() on the listview was the only suggestion to resolve the problem, though i'm hopeful that it is a temporary resolution to the bigger problem. Calling refresh() is causing the view to flash, which looks odd and makes for a poor UX.
Hope this is still being looked into.

Flashing problem as described by @DillonStreator can be "solved" by overwriting the default behaviour as seen here

<ListView [items]="items" (itemLoading)="onItemLoading($event)">
onItemLoading(args: ItemEventData) {
  const iosCell = args.ios;
  iosCell.selectionStyle = UITableViewCellSelectionStyle.None;
}

Is there any possible option to fix this if the source collection doesn't change? IF i'm changing only property visibility="visible || collapsed" of the view of tapped ITEM .
I've tried @eduardoturconi .slice() method and others like:

  • listView = this.page.getViewById("listView")
    listView.refresh();
  • BehaviourSubject and BehaviourSubject with NgZone.run
  • @Dagorwolf method;
    !!! Nothing work correctly
    Please, would appreciate any ideas.

I solved this problem as described here.

Just add this:

    Object.defineProperty(View.prototype, "isLayoutValid", {
        get: function () {
            return (this._privateFlags & PFLAG_LAYOUT_REQUIRED) !== PFLAG_LAYOUT_REQUIRED;
        },
        enumerable: true,
        configurable: true
    });   

in

node_modules/tns-core-modules/ui/core/view/view.ios.js

And everything was magically fixed!

Have the same issue, tried https://angular.io/api/core/ChangeDetectorRef but no success, tried ngZone and no success. Is it related to ListView? e.g If I change to a StackView with ScrollView around would it work?

onItemLoading() does not get triggered when coming backwards with router. Only forwards

@drmistral , if Im using angular and I would change view.ios.js, wouldn't it override this file on build?

@ShyshkovOleg, I'm not using angular but anyway I put view.ios.js under git control, just to detect any overwriting.
Of course the best solution would be fix this problem at the root but meanwhile...

We're seeing this on Android when using the async pipe. A workaround we found was using the itemLoading event on ListView, and then do a markForCheck() inside a setTimeout.

So in template
<ListView [items]="data | promisePipe | async" (itemLoading)="refresh()"></ListView>
In TS file:

refresh() {
   setTimeout(() => this.changeDetectorRef.markForCheck(), 0); // Queue macro event
}

Is there any news to this story? I'm having similar issues on IOS. I have tried so many different things now and nothing helps.

well, I believe this is why I finally give up on the nativescript, a serious bug exists nearly 3 years.

I encountered the same issue. I tried to solve it by re-assigning my array to the ListView component, but once every n times my app crashes returning the following error:
"TypeError: null is not an object (evaluating 'cell.view')"

Anybody knows what the source of the problem may be?

I have been feeling the same thing, my employer wanted me to try to build an app with a javascript library to test if it is a way to build other apps in the future. And when I get stuck on things like this I'm wondering if there are more holes to fall down.

I actually found a way to "solve" my issue, well it did not solve the issue but it is a way to list things without using the list view. You can see the issue here.

Is switching to RadListView a viable solution to this issue?

If you commented on my comment, I have been using radlistview from the beginning.

This is a major issue. Is there any way to get it moved off of the backlog? @NickIliev @tsonevn ?

Thank you for your consideration.

@Roar1827 or @Kraften, do you have a repository that shows this issue?

From my comment back in Dec 2016, it was able to get around this issue by using RxJS. I was about to build a little demo for you, but when I copy/pasted the code from the original post...it's working without resorting to Observables.

Here's my component, the only change I needed to make from the above code was to change <template/> to <ng-template/>: https://github.com/jzgoda/tns-listview-change-detection/tree/master/src/app

@jzgoda @Roar1827 @Kraften @Zampa98 @steve3d @larssn @ignaciofuentes @ShyshkovOleg

I might be missing something on the way, but while using the original snippet with the latest tns-core-modules and nativescript-angular and I am not experiencing this issue. Here is a Playground demo showing the case - tested on both iOS and Android (all the related code is in app.component) and on both the itemTap is triggering the change detection. Please do let me know if you guys have a different case and share a Playground (or sample app) that demonstrates the issue.

Update: the issue was also tested with RadListView and again it seems to that everything is working as expected.

Thank you for looking into this @NickIliev, I appreciate it very much.

I've tried to create a simple playground demo from extending your playground.

The problem occurs when we use [visibility] to conditionally display something when a list item is selected. Perhaps we are doing this the wrong way? Calling refresh() on the listView worked in NS 4.1 but no longer seemed to work @latest

In the demo, if you scroll down in the list view to put the item off screen, then scroll back up, the item will display correctly. When first tapping an item the height of the newly displayed item doesn't display properly.

@Roar1827 indeed you are dealing with a totally different problem which is not related to this issue or the Angular state management but to the way you are handling the state of each ListView cell. You still need to call refresh on iOS but you also need to change an individual boolean flag for each item and not a global boolean variable.

Here is a Playground demo demonstrating on how you could approach the problem with creating a boolean flag (isExpanded) for each item of your list. Then use this boolean flag to change only tapped cell (based on the passed index). Due to specifics with the measurements on iOS, you will still need to call refresh method on the listview (see the onListViewLoaded method used to get a reference for the ListView).

Closing the issue as resolved (see this comment) - if anyone still experiences a similar problem please ping me with the details needed to investigate the case (including a sample app or Playground demo)

Was this page helpful?
0 / 5 - 0 ratings