eakoriakin / ionic-selectable

Ionic Selectable is an Ionic versatile and highly customizable component that serves as a replacement to Ionic Select, and allows to search items, including async search, create items, customize the layout with templates and much more. It provides an intuitive API and is easy to set up and use.
MIT License
552 stars 125 forks source link

4.4.0 infiniteScroll flickers and resets scroll position #164

Open kyleabens opened 5 years ago

kyleabens commented 5 years ago

I just dowloaded v4.4.0 and implemented infinite scroll and noticed that it flickers when I add new data to event.components.items. Doesn't matter if I push or concat the data. I also noticed that every time onInfiniteScroll fires it resets the scroll position to the very top which is probably connected to the flicker in some way. You can see what I'm talking about below.

ion-selectable-infinitescroll

gabriel17carmo commented 5 years ago

It's happening the same here. Same version. Did you find any workaround on it?

tiagosilveiraa commented 5 years ago

I'm facing the same Issue in my case the Screen is blinking each time I Increment the array of items, the fact of the scroll backs to the TOP only happens if Focus in other INPUT, I don't know if it's relevant, but I'm using ReactiveForms

VERSION: ^4.4.1 selectable

MyCode TS:

  async getIcones() {
    this.iconeSub = this.baseService.fillSelect('icones', this.iconesPage).subscribe((icones: any[]) => {
      this.icones = this.icones.concat(icones);
    }, (e) => {
      this.utils.showError(e);
    });
  }
  async getMaisIcones(event: {
    component: IonicSelectableComponent,
  }) {
    this.iconesPage++;
    await this.getIcones();
    event.component.endInfiniteScroll();
  }

MyCode HTML:

<ionic-selectable slot="end" 
              formControlName="icone" 
              [items]="fill.icones" 
              itemValueField="id"
              itemTextField="nome_icone"
              closeButtonText="Fechar" 
              [hasInfiniteScroll]="true" 
              (onInfiniteScroll)="fill.getMaisIcones($event)" 
              (onClose)="fill.iconeSub.unsubscribe(); 
              fill.icones = []; 
              fill.iconesPage = 0">
              <ng-template ionicSelectableItemEndTemplate let-icone="item">
                <ion-icon [name]="icone.nome_icone"></ion-icon>
              </ng-template>
            </ionic-selectable>

I'm using a shared service, and the itens is there, I tryed to use event.component.concat(bla), and the same happens

abhideshmukh commented 4 years ago

Is this issue resolved? or did you guys find any workaround?

vagnersabadi commented 3 years ago

Error persists in version "ionic-selectable": "4.7.1",

One thing that night was: When I run in debug mode it does not return to the beginning, it works as expected. But when I generate the build the problem happens

clonixdev commented 3 years ago

is this resolved? i have this error in version 4.9.0

4-life commented 3 years ago

Re-open please. It's still happens in 4.9.0. @eakoriakin @edy-ap

cunha20 commented 3 years ago

Same problem here. Ionic-selectable 4.9.0 and @angular/core 12.2.13. Re-open please.

https://user-images.githubusercontent.com/33962782/141646665-1ea9aa7d-cb28-44b5-ae2c-5643ce827794.mp4

4-life commented 3 years ago

@cunha20 i think nobody cares =(

eakoriakin commented 3 years ago

Guys, this is an opensource project, @edy-ap and myself have put lots of efforts into it getting nothing back. If you find an issue, I'd recommend you clone the project, deploy it locally and try to fix it. Then you can submit a PR.

4-life commented 3 years ago

Guys, this is an opensource project, @edy-ap and myself have put lots of efforts into it getting nothing back. If you find an issue, I'd recommend you clone the project, deploy it locally and try to fix it. Then you can submit a PR.

we know but we just say that you should re-open the issue because bug is still exists. Maybe somebody will find a solution for this

@eakoriakin

cunha20 commented 2 years ago

Hi guys, I analyzed the project, but I couldn't find the root cause of the problem. To move forward, I've implemented the function below for non-iOS devices. It's not the best solution, but it's working.

image

sfox-developer commented 2 years ago

Hey there, anyone got a solution or idea for this? Facing this issue and hoping for an answer.

Cheers!

Update

I just applied a custom workaround now, which is kind of based on the aproach of @cunha20. Thanks for this input.

It is far away from being optimized, but i just paste it here for anyone who is interested. Feel free to ping me, if you have further adjustments or ideas.

care

Description I do listen to the change of the ion-list element height and apply the solution @canha20 posted here. So instead of adding a timeout, we got the observer. Also i do save the current scroll position and get back to it once the height got updated. So there is no fixed height numbers.


observer: ResizeObserver;
  scrollHeight : number = 0;
  scrollTopPosition: number = 0;
  modalScrollElement: HTMLElement

onCloseSelect() {
    this.observer.unobserve(this.modalScrollElement);
  }

/**
   * We do lose the observer here, because the element is resetted.
   * STILL: the scroll jump issue does not appear after a search.
   * MAYBE: We could trigger the method tryRegisterObserver() once a search is completed.
   * ADDITION: If the issue does not appear after a scroll, maybe this is also fixable by just calling a focus or inital emptysearch
   */
  searchEvent(event: { text: string }) {
    this.scrollHeight = 0;
    this.scrollTopPosition = 0;

    this.search(event)
  }

  onOpen() {
    setTimeout(() => {
      this.tryRegisterObserver();
    }, 1000);
  }

  /**
   * TODO: Better also check if
   * this.selectComponent._modalComponent._content exists, or retry
   * Somtimes the entries need more time to load, so using a timeout is not the best solution
   */
  tryRegisterObserver() {
    if(!this.selectComponent.isOpened) {
      setTimeout(() => {
        this.tryRegisterObserver()
      }, 1000);
    } else {
      this.registerModalObserver()
    }
  }

  registerModalObserver() {
    this.observer = new ResizeObserver(entries => {
      this.zone.run(() => {
        const newHeight = entries[0].contentRect.height;

        if(this.scrollHeight < newHeight) {
          this.scrollHeight = newHeight;
          this.selectComponent._modalComponent._content.scrollToPoint(0, this.scrollTopPosition)
        }
      });
    });

    /**
     * TODO: Check if element is accesable, otherwise try register observer once more
     * child is: ion-content -> ion-list
     */
    this.modalScrollElement = this.selectComponent._modalComponent._element.nativeElement.children[1].children[0]

    this.selectComponent._modalComponent._content.getScrollElement().then((element: HTMLElement) => {
      element.addEventListener("scroll", (event) => {
        if(this.scrollTopPosition < element.scrollTop) {
          this.scrollTopPosition = element.scrollTop
        }
      })
    })

    this.observer.observe(this.modalScrollElement);

  }
git-rlagos commented 2 years ago

Or simply move scroll to bottom in search method with: event.component._modalComponent._content.scrollToBottom(1500); (tested in ionic-selectable 4.9.0)

html:

    <ionic-selectable item-content [(ngModel)]="port" itemValueField="id"
      itemTextField="name" [items]="ports" [canSearch]="true"
      [isMultiple]="false" [hasInfiniteScroll]="true"
      (onSearch)="searchPorts($event)"
      (onInfiniteScroll)="getMorePorts($event)">
    </ionic-selectable>

ts:

searchPorts(event: { component: IonicSelectableComponent; text: string }) {

    event.component._modalComponent._content.scrollToBottom(1500);

    ...
}

https://user-images.githubusercontent.com/14658236/201833440-ed81bf99-9f80-4737-9ede-8154035c886c.mp4

neverlose-lv commented 2 years ago

This happens exactly when endInfiniteScroll() is called after the async call

git-rlagos commented 2 years ago

This happens exactly when endInfiniteScroll() is called after the async call

But do you suggest removing the endInfiniteScroll()? Or do I have to add some code before or after? Did you fix it?

neverlose-lv commented 2 years ago

But do you suggest removing the endInfiniteScroll()? Or do I have to add some code before or after? Did you fix it?

I have tried to avoid it. And made more investigation on this problem. If you avoid the endInifniteScroll() - then new items do not appear after the very first infinite query, you have to scroll a bit upper and then to bottom, but the next ones - work ok. Also you can add the .complete() method to the internal ion-inifinite-scroll element.

The bug seems to be that the items does not appear - that there is no change detection. But once I add changedetection, the list behavior acts the same - it is fully redrawn, and scrolling starts from the top.

Then I investigated deeper and saw, that on infinite scroll, the items array is fully reassigned internally in the ionic-selecatable.

So the list is redrawn, that's why the scroll position is reset. It does not matter, how you set the component items, because when you call endInfiniteScroll(), it internally calls setItems() method, which copies the values you have set, to the internal array, by creating a new array. So there is no difference, how you set the items. concat/push/... etc. anyway a new array of items is created.

I think - you are not able to keep the scroll position, since the view is redrawn. (but this may be tricky, may be there is some ionic property for this?)

There should be the trackBy function for the list and groups to in the ionic-selectable to fix this issue.

Or there should be some ionic property, to keep the scroll on the same position, while the view is being redrawn.

cunha20 commented 2 years ago

To put it into production, I increased the number of items to work around the problem. Below is the code snippet. I'm using ionic-selectable 4.9.0.

image

git-rlagos commented 2 years ago

Can you show us a video of the result?

cunha20 commented 2 years ago

I put 200 items per page, so the client will hardly scroll to the end.... however the problem when reaching infinity scroll, continues...

https://user-images.githubusercontent.com/33962782/202711861-1f979bf4-cf39-4508-9d86-fedba635f644.mp4

git-rlagos commented 2 years ago

Ok, only size items. I' show you my code and result for scrolling. In file html, onInfiniteScroll have the method with $event for get IonicSelectableComponent in file TS.

html:

<ion-item *ngIf="!esMantencionPreventiva" [disabled]="!tarea?.initial_date">
        <ion-label position="floating">Tipo falla</ion-label>
        <ion-textarea *ngIf="tareaFirmada()" [value]="tarea?.types_description" auto-grow readonly></ion-textarea>
        <ionic-selectable [disabled]="!tarea?.initial_date" *ngIf="!tareaFirmada()"                       
                          closeButtonSlot="end" #tipoFallaComponent item-content
                          [(ngModel)]="tipoFalla" [items]="dataService.tipoFallas" 
                          itemValueField="description" itemTextField="description" 
                          [canSearch]="true" [canSaveItem]="true"
                          (onOpen)="onOpen()" (click)="onClick()"
                          [canClear]="true"                      
                          (onSearchFail)="onSearchFail('tipoFalla', $event)"
                          (onSearchSuccess)="onSearchSuccess($event)"
                          [hasInfiniteScroll]="true"
                          (onInfiniteScroll)="onInfiniteScroll('tipoFalla', $event)"
                          (onSearch)="searchType('tipoFalla', $event)"
                          (onChange)="onSelectedChange('tipoFalla', $event)">
          <ng-template ionicSelectableCloseButtonTemplate>
            <ion-text color="danger">{{paginaTipoFallo}}/{{getNumeroPaginas('tipoFalla')}} &nbsp; </ion-text>
            <ion-icon name="close-circle" style="font-size: 24px;"></ion-icon>
          </ng-template>
          <ng-template ionicSelectableItemTemplate let-port="item">
            {{port.description}}
          </ng-template>
          <ng-template ionicSelectableValueTemplate let-port="value">
            <div class="ion-text-wrap">{{port.description}}</div>
          </ng-template>
          <ng-template ionicSelectableAddItemTemplate let-port="item" let-isAdd="isAdd">
            <form [formGroup]="formTipoFalla" novalidate>
              <ion-list>
                <ion-item-divider>
                  {{isAdd ? 'Nuevo' : 'Editar'}} registro tipo de falla.
                </ion-item-divider>                
                <ion-item>
                  <ion-label color="tertiary">Título</ion-label>
                  <ion-textarea formControlName="description" 
                                auto-grow placeholder="Ingrese tipo..."
                                autocorrect="off" autocapitalize="none">
                  </ion-textarea>
                </ion-item>
                <ion-item color="danger" slot="error" *ngIf="formTipoFalla.get('description').hasError('tipoAlreadyExists')">
                  <ion-label>Ya existe el tipo.</ion-label>
                </ion-item>
              </ion-list>
            </form>
            <ion-footer>
              <ion-toolbar mode="ios">
                <ion-row>
                  <ion-col class="ion-text-center">
                    <ion-button ion-button full no-margin color="danger"
                                (click)="tipoFallaComponent.hideAddItemTemplate()">
                      Cancel
                    </ion-button>
                  </ion-col>
                  <ion-col class="ion-text-center">
                    <ion-button ion-button full no-margin 
                      (click)="isAdd? agregarTipo('tipoFalla') : editarTipo('tipoFalla', tipoFalla)"
                      [disabled]="!formTipoFalla.valid">
                      {{isAdd ? 'Agregar' : 'Editar'}}
                    </ion-button>
                  </ion-col>
                </ion-row>
              </ion-toolbar>
            </ion-footer>
          </ng-template>
        </ionic-selectable>
      </ion-item>

ts:

// Metodo para usar con Infinity Scroll en ionic-selectable
  onInfiniteScroll(tipo: string, 
                event: { component: IonicSelectableComponent; text: string }) {

    event.component._modalComponent._content.scrollToBottom(1500);  // <---- Fixed position

    const page = Math.round(event.component.items? (event.component.items.length / this.cantItemsPorPagina) : 1);
    const text = (event.text || '').trim().toLowerCase();

    switch(tipo){
      case 'tipoFalla': {
            this.paginaTipoFallo = page;
            if(this.paginaTipoFallo > (this.getNumeroPaginas(tipo))){
              event.component.disableInfiniteScroll();
              return;
            }

            this.dataService.getTipoFallaAsync(++this.paginaTipoFallo, this.cantItemsPorPagina)
            .subscribe((tipos) => {
              tipos = event.component.items.concat(tipos);

              if (text) {
                tipos = this.filterRecordsTipoFalla(tipos, text);
              }

              event.component.items = tipos;
              event.component.endInfiniteScroll();
            });
            break;
      }
     //.... more code case
    }    
  }

  filterRecordsTipoFalla(tipos: TipoFalla[], text: string): TipoFalla[] {
    return tipos.filter((tipo) => {
      return (
        tipo.description.toLowerCase().indexOf(text) !== -1 
      );
    });
  }

https://user-images.githubusercontent.com/14658236/202734562-f67c272b-57c5-49e7-90b9-fde878c7f61e.mp4

EremesNG commented 6 months ago

In my case, I implemented a workaround, I removed the ‘ion-item-group’ part from ionic-selectable-modal.component.html because I don't need it.

<ion-list class="ion-no-margin" *ngIf="selectComponent._hasFilteredItems">
    <ion-item-group *ngFor="let group of selectComponent._filteredGroups" class="ionic-selectable-group">
      <ion-item-divider *ngIf="selectComponent._hasGroups"
        [color]="selectComponent.groupColor ? selectComponent.groupColor : null">
        <!-- Need span for for text ellipsis. -->
        <span *ngIf="selectComponent.groupTemplate" [ngTemplateOutlet]="selectComponent.groupTemplate"
          [ngTemplateOutletContext]="{ group: group }">
        </span>
        <!-- Need ion-label for text ellipsis. -->
        <ion-label *ngIf="!selectComponent.groupTemplate">
          {{group.text}}
        </ion-label>
        <div *ngIf="selectComponent.groupEndTemplate" slot="end">
          <div [ngTemplateOutlet]="selectComponent.groupEndTemplate" [ngTemplateOutletContext]="{ group: group }">
          </div>
        </div>
      </ion-item-divider>
      <ion-item button="true" detail="false" *ngFor="let item of group.items" (click)="selectComponent._select(item)"
        class="ionic-selectable-item" [ngClass]="{
          'ionic-selectable-item-is-selected': selectComponent._isItemSelected(item),
          'ionic-selectable-item-is-disabled': selectComponent._isItemDisabled(item)
        }" [disabled]="selectComponent._isItemDisabled(item)">
        <!-- Need span for text ellipsis. -->
        <span *ngIf="selectComponent.itemTemplate" [ngTemplateOutlet]="selectComponent.itemTemplate"
          [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
        <!-- Need ion-label for text ellipsis. -->
        <ion-label *ngIf="!selectComponent.itemTemplate">
          {{selectComponent._formatItem(item)}}
        </ion-label>
        <div *ngIf="selectComponent.itemEndTemplate" slot="end">
          <div [ngTemplateOutlet]="selectComponent.itemEndTemplate"
            [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
          </div>
        </div>
        <span *ngIf="selectComponent.itemIconTemplate" [ngTemplateOutlet]="selectComponent.itemIconTemplate"
          [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
        <ion-icon *ngIf="!selectComponent.itemIconTemplate"
          [name]="selectComponent._isItemSelected(item) ? 'checkmark-circle' : 'radio-button-off'"
          [color]="selectComponent._isItemSelected(item) ? 'primary' : null" [slot]="selectComponent.itemIconSlot">
        </ion-icon>
        <ion-button *ngIf="selectComponent.canSaveItem" class="ionic-selectable-item-button" slot="end" fill="outline"
          (click)="selectComponent._saveItem($event, item)">
          <ion-icon slot="icon-only" ios="create" md="create-sharp"></ion-icon>
        </ion-button>
        <ion-button *ngIf="selectComponent.canDeleteItem" class="ionic-selectable-item-button" slot="end" fill="outline"
          (click)="selectComponent._deleteItemClick($event, item)">
          <ion-icon slot="icon-only" ios="trash" md="trash-sharp"></ion-icon>
        </ion-button>
      </ion-item>
    </ion-item-group>
  </ion-list>

As a result, it now looks like this:

<ion-list class="ion-no-margin" *ngIf="selectComponent._hasFilteredItems">
    @if (selectComponent._hasGroups){
      @for (group of selectComponent._filteredGroups; track group){
        <ion-item-group class="ionic-selectable-group">
          <ion-item-divider *ngIf="selectComponent._hasGroups"
                            [color]="selectComponent.groupColor ? selectComponent.groupColor : null">
            <!-- Need span for for text ellipsis. -->
            <span *ngIf="selectComponent.groupTemplate" [ngTemplateOutlet]="selectComponent.groupTemplate"
                  [ngTemplateOutletContext]="{ group: group }">
        </span>
            <!-- Need ion-label for text ellipsis. -->
            <ion-label *ngIf="!selectComponent.groupTemplate">
              {{group.text}}
            </ion-label>
            <div *ngIf="selectComponent.groupEndTemplate" slot="end">
              <div [ngTemplateOutlet]="selectComponent.groupEndTemplate" [ngTemplateOutletContext]="{ group: group }">
              </div>
            </div>
          </ion-item-divider>
          @for (item of group.items; track item){
            <ion-item button="true" detail="false" (click)="selectComponent._select(item)"
                      class="ionic-selectable-item" [ngClass]="{
          'ionic-selectable-item-is-selected': selectComponent._isItemSelected(item),
          'ionic-selectable-item-is-disabled': selectComponent._isItemDisabled(item)
        }" [disabled]="selectComponent._isItemDisabled(item)">
              <!-- Need span for text ellipsis. -->
              <span *ngIf="selectComponent.itemTemplate" [ngTemplateOutlet]="selectComponent.itemTemplate"
                    [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
              <!-- Need ion-label for text ellipsis. -->
              <ion-label *ngIf="!selectComponent.itemTemplate">
                {{selectComponent._formatItem(item)}}
              </ion-label>
              <div *ngIf="selectComponent.itemEndTemplate" slot="end">
                <div [ngTemplateOutlet]="selectComponent.itemEndTemplate"
                     [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
                </div>
              </div>
              <span *ngIf="selectComponent.itemIconTemplate" [ngTemplateOutlet]="selectComponent.itemIconTemplate"
                    [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
              <ion-icon *ngIf="!selectComponent.itemIconTemplate"
                        [name]="selectComponent._isItemSelected(item) ? 'checkmark-circle' : 'radio-button-off'"
                        [color]="selectComponent._isItemSelected(item) ? 'primary' : null" [slot]="selectComponent.itemIconSlot">
              </ion-icon>
              <ion-button *ngIf="selectComponent.canSaveItem" class="ionic-selectable-item-button" slot="end" fill="outline"
                          (click)="selectComponent._saveItem($event, item)">
                <ion-icon slot="icon-only" ios="create" md="create-sharp"></ion-icon>
              </ion-button>
              <ion-button *ngIf="selectComponent.canDeleteItem" class="ionic-selectable-item-button" slot="end" fill="outline"
                          (click)="selectComponent._deleteItemClick($event, item)">
                <ion-icon slot="icon-only" ios="trash" md="trash-sharp"></ion-icon>
              </ion-button>
            </ion-item>
          }
        </ion-item-group>
      }
    } @else {
      @for (item of selectComponent.items; track item){
        <ion-item button="true" detail="false" (click)="selectComponent._select(item)"
                  class="ionic-selectable-item" [ngClass]="{
          'ionic-selectable-item-is-selected': selectComponent._isItemSelected(item),
          'ionic-selectable-item-is-disabled': selectComponent._isItemDisabled(item)
        }" [disabled]="selectComponent._isItemDisabled(item)">
          <!-- Need span for text ellipsis. -->
          <span *ngIf="selectComponent.itemTemplate" [ngTemplateOutlet]="selectComponent.itemTemplate"
                [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
          <!-- Need ion-label for text ellipsis. -->
          <ion-label *ngIf="!selectComponent.itemTemplate">
            {{selectComponent._formatItem(item)}}
          </ion-label>
          <div *ngIf="selectComponent.itemEndTemplate" slot="end">
            <div [ngTemplateOutlet]="selectComponent.itemEndTemplate"
                 [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
            </div>
          </div>
          <span *ngIf="selectComponent.itemIconTemplate" [ngTemplateOutlet]="selectComponent.itemIconTemplate"
                [ngTemplateOutletContext]="{ item: item, isItemSelected: selectComponent._isItemSelected(item) }">
        </span>
          <ion-icon *ngIf="!selectComponent.itemIconTemplate"
                    [name]="selectComponent._isItemSelected(item) ? 'checkmark-circle' : 'radio-button-off'"
                    [color]="selectComponent._isItemSelected(item) ? 'primary' : null" [slot]="selectComponent.itemIconSlot">
          </ion-icon>
          <ion-button *ngIf="selectComponent.canSaveItem" class="ionic-selectable-item-button" slot="end" fill="outline"
                      (click)="selectComponent._saveItem($event, item)">
            <ion-icon slot="icon-only" ios="create" md="create-sharp"></ion-icon>
          </ion-button>
          <ion-button *ngIf="selectComponent.canDeleteItem" class="ionic-selectable-item-button" slot="end" fill="outline"
                      (click)="selectComponent._deleteItemClick($event, item)">
            <ion-icon slot="icon-only" ios="trash" md="trash-sharp"></ion-icon>
          </ion-button>
        </ion-item>
      }
    }
  </ion-list>

I am developing in Ionic 8 and Angular 17, which is why I switched from *ngFor and *ngIf to @for and @if.

msedge_k3VcZ7CdB8