The page scrolls by itself at the bottom when sort an array

Hi everyone!

I have an issue with my code. It’s not an error really. It’s just a little bit annoying.

I have an array of objects.

{
  name: item.name,
  checked: false
}

My template is basically this:

<ion-list>
  <ion-item *ngFor="let item of items">
    <ion-label>{{item.name}}</ion-label>
    <ion-checkbox
      color="primary"
      (ionChange)="checkItem(item, $event)"
      [(ngModel)]="item.checked"
    ></ion-checkbox>
</ion-item>

Perfect. Everything is pretty straightforward.

checkItem (item, ev) {
  this.items.sort((a, b) =>
    a.checked < b.checked ? -1 : a.checked > b.checked ? 1 : 0
  );
}

What happens is that, whenever the view is not at the very top of the page, and I trigger the checkItem () function, the item.checked gets the oposite value. Therefore, as is sorted out, it is carried at the bottom of the page. What is deeply annoying is that the view is carried at the bottom as well.

If you want to try it out, here is a dummy long array so you can test it out:

items = [
    {
        name: 'First',
        checked: false
    },
    {
        name: 'Sec',
        checked: false
    },
    {
        name: 'Third',
        checked: false
    },
    {
        name: 'Fourth',
        checked: false
    },
    {
        name: 'Hello',
        checked: false
    },
    {
        name: 'World',
        checked: false
    },
    {
        name: 'Some',
        checked: false
    },
    {
        name: 'words',
        checked: false
    },
    {
        name: 'to',
        checked: false
    },
    {
        name: 'fill',
        checked: false
    },
    {
        name: 'this',
        checked: false
    },
    {
        name: 'Example',
        checked: false
    },
    {
        name: 'Hey',
        checked: false
    },
    {
        name: 'You',
        checked: false
    },
    {
        name: 'Are',
        checked: false
    },
    {
        name: 'Not',
        checked: false
    },
    {
        name: 'Reading',
        checked: false
    },
    {
        name: 'This',
        checked: false
    },
    {
        name: 'Right',
        checked: false
    },
    {
        name: '?',
        checked: false
    },
]

Thanks in advance!

I’ll start by saying that I don’t think I can reproduce your error. I get no view scrolling whatsoever, although I am running in a browser under a tiling window manager, so maybe that inoculates me.

I’m going to assume that you blurred items and products accidentally, and they’re interchangeable.

That being said, two things I would do differently:

At a bare minimum, do not have more than one binding on any given property in any given direction. Specifically, get rid of the () binding on ngModel, because it is duplicated by the (ionChange).

I’m going to assume you have a good reason for shunting checked things down, which typically would put them out of view, when they get checked. I’m not a huge fan of this UI in general. Even given the few minutes I spent playing around with it, I got disoriented wondering where my items were going. What I would do instead is to conjure up memories of the old Font/DA Mover in 1980s MacOS and have two side-by-side list boxes; one for selected products and one for unselected ones.

Even if you don’t want to go that far, that’s how I would organize the backing code: with two separate lists and no resorting:

    <ion-list>
      <ion-item *ngFor="let prod of unselectedProducts; index as i">
        <ion-label>{{prod.name}}</ion-label>
        <ion-checkbox
            color="primary"
            (click)="selectProduct(i)"
            checked="false"
        ></ion-checkbox>
      </ion-item>
      <ion-item *ngFor="let prod of selectedProducts; index as i">
        <ion-label>{{prod.name}}</ion-label>
        <ion-checkbox
            color="primary"
            (click)="deselectProduct(i)"
            checked="true"
        ></ion-checkbox>
      </ion-item>
    </ion-list>
  selectProduct(i: number): void {
    let prod = this.unselectedProducts[i];
    this.unselectedProducts.splice(i, 1);
    this.selectedProducts.push(prod);
  }

  deselectProduct(i: number): void {
    let prod = this.selectedProducts[i];
    this.selectedProducts.splice(i, 1);
    this.unselectedProducts.push(prod);
  }

Thanks for your answer, man. Great idea, but I am facing some difficulties with it.

What happens is that it is actually a subscription where I get the whole array from a db. Think of it as an ordered grocery list. Whenever you buy an item, it should disappear. If somebody checks a product from somewhere else, all my items would disappear and appear again. (i’ve tried that already). Plus, I have an ion-search to look for a specific product.

I need the [(ngModel)] and the (ionChange) because one is to get the ion-checkbox value and the other is to update the value in the db and sort the array again.

To test the error maybe you need to get the array bigger in order to make the list bigger than the viewport.

Sorry, but I’m having trouble following you.

First off, can I assume that there is only one “me” here? We’re not talking a massive multiplayer situation where if I buy the last apple in the store, then everybody else worldwide has to have “apples” come out of their possible virtual shelf?

So, if that’s the case, then when I buy an item, what exactly does “disappear” mean here? Because in the code you originally posted, nothing really “disappears”, it just gets shifted around in the list.

Where would “somewhere else” be here? Also, what does “check” mean? Is this the “check” of “checkbox” or the “check” as a synonym for “investigate”?

What does this mean? Did you try the code I posted? I didn’t see any flickering or disappearing and reappearing. There is one virtual list that is simply a concatenation of an ngFor loop across two distinct backing lists.

No, you don’t. You may need [ngModel] and (ionChange) for those dual purposes, but remember that “banana-in-a-box” is shorthand. What [(ngModel)] means is effectively
[ngModel]="foo" (ngModelChange)="foo = $event". It’s both an input and an output binding. You do not need the banana binding on ngModel because you are doing equivalent work in (ionChange).

The main point I want to emphasize here is that fundamentally, you have a shopping cart (even more true if you’re running a grocery store). It strikes me as much more logical to group items into “stuff in my cart” and “stuff sitting on shelves” buckets, instead of going through every aisle in the store and putting a sticker on the product saying “this is in Raul’s cart” or “nope, not in Raul’s cart”.

Now, if you do in fact have a massive multiplayer situation, then there are much more thorny concurrency issues in play, but they exist far from the realm of UI that we’re talking about here, and when it comes to UI, I think that carrying the data as two separate buckets will be easier to both manage and present than carrying it as one. One ancillary benefit would be that you no longer need the checked property on products at all.

By “disappear” I mean from the display. I made a gif with dummy data so you can see it what I mean.

As you see in the gif, the view follows the checked - as checkbox - item. I need the display to keep it still, wherever it is.

Lets forget about the rest for a sec. But I’m glad that you pointed it out. Sure I’m gonna research about it.

I just try your splice and push method, I get the very same behavior.

I fear the cause of this lies outside what you’ve posted here so far, because I still can’t reproduce this, but what happens if you split the selected products out into a separate <ion-list>?

    <ion-list>
      <ion-item *ngFor="let prod of unselectedProducts; index as i">
        <ion-label>{{prod.name}}</ion-label>
        <ion-checkbox
            color="primary"
            (click)="selectProduct(i)"
            checked="false"
        ></ion-checkbox>
      </ion-item>
    </ion-list>
    <ion-list>
      <ion-item *ngFor="let prod of selectedProducts; index as i">
        <ion-label>{{prod.name}}</ion-label>
        <ion-checkbox
            color="primary"
            (click)="deselectProduct(i)"
            checked="true"
        ></ion-checkbox>
      </ion-item>
    </ion-list>

It works.

A very important thing is to scroll just a little bit. If you are right at the top of the page, the screen does not move. But if you scroll just a little, the screen follows the ion-checkbox item.

I just reproduced it with the code I shared.

Sorry, but nope, not in my environment (which I do admit is not likely your environment, but it’s what I have). Chromium on Debian sid under i3wm.

From a user perspective, I would think having an ion-list fixed within the header or footer would make for a better experience as they would be able to see what had been selected and adjust the list, if needed.