ionic-team / ionic-v3

The repo for Ionic 3.x. For the latest version of Ionic, please see https://github.com/ionic-team/ionic
Other
127 stars 85 forks source link

ion-infinite-scroll will not load more items if initial batch does not make viewport large enough for scrolling #59

Open ionitron-bot[bot] opened 6 years ago

ionitron-bot[bot] commented 6 years ago

Short description of the problem:

The ion-infinite-scroll component works by listening for an onScroll event. However, if the initial items loaded do not trigger such an event, then the ion-infinite-scroll will never trigger an ionInfinite callback, resulting in a broken page.

What behavior are you expecting?

I would expect that, when the InfiniteScroll object is enabled (listener added), and also after any ionInfinite.complete() call, that the current onScroll check would be manually run in order to see if another ionInfinite event needs to be triggered immediately.

Currently, to workaround this, I have two lines executed after both the initial page loading and after each ionInfiniteEvent.complete() call:

setTimeout(() => {
  this.infinite._lastCheck = 0;
  this.infinite._onScroll();
}, 0);

Steps to reproduce:

  1. Put in an infinite component, but only load 1 or 2 items so that there is no scrolling on the page.
  2. See that ionInfinite never gets triggered.
  3. Dig into ionic's very clean source to figure out the workaround.

Which Ionic Version? 2.0 RC2

ggallotti commented 5 years ago

Same problem here.

nunoarruda commented 5 years ago

I have the same issue and I'm trying to solve it by detecting the scrollbar: https://forum.ionicframework.com/t/how-to-detect-if-ion-content-has-a-scrollbar/167840

YabinSong commented 5 years ago

This is still happening in ionic4 btw.

nunoarruda commented 5 years ago

Solved by detecting the scrollbar and using, for example, a button as a fallback to load more items: https://stackoverflow.com/a/58579938/2892404

MattiasMartens commented 3 years ago

Posting a Vue/TypeScript wrapper component I made to implement the fix of automatically loading elements in until they fill the page height.

Includes some cruft like requiredType() and notNullOrUndefined(); in my own context these are convenience methods I import everywhere.

Had to use my own implementation of isInScroll() which adds up the height of the content's children; I found that otherwise the scrollHeight of ion-content would be locked to the clientHeight so the condition would never trigger.

Also added an exists prop. Could be replaced with v-if but I wanted to emphasize that this thing needs to disappear when the complete set is fetched; otherwise you are open to a problem where scroll() will be called over and over if the complete list of results fails to cover the entire page.

<template>
  <IonInfiniteScroll threshold="0" @ionInfinite="doScroll" v-if="exists">
    <IonInfiniteScrollContent
      loadingSpinner="bubbles"
      loadingText="Loading…" />
  </IonInfiniteScroll>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref, onUpdated, onBeforeUnmount} from 'vue'
import {
  IonInfiniteScroll,
  IonInfiniteScrollContent
} from '@ionic/vue'

function isInScroll(element: HTMLElement) {
  let childrenScrollHeight = 0
  for (const child of element.childNodes) {
    // @ts-ignore
    childrenScrollHeight += child.scrollHeight || 0
  }

  return childrenScrollHeight > element.clientHeight
}

function requiredType<T>(
  type: any
) {
  return {
    type: type as () => T,
    required: true as true
  }
}

function notNullOrUndefined<T>(t: T | undefined | null, errorMessage = "") {
    if (t === undefined || t === null) {
        throw new Error(errorMessage || "Value was null or undefined.");
    }
    else {
        return t;
    }
}

function wrapAsyncInInfiniteScroll<T>(fn: () => Promise<T>) {
  return async function (e: { target: { complete: () => void } }) {
    try {
      await fn()
    } finally {
      e.target.complete()
    }
  }
}

export default defineComponent({
  components: {
    IonInfiniteScroll,
    IonInfiniteScrollContent
  },
  props: {
    exists: Boolean,
    scroll: requiredType<() => Promise<any>>(Function)
  },
  setup(props) {
    const isMounted = ref(false)

    const loadUntilScrollable = async () => {
      const mainPageElement = notNullOrUndefined(
        document.querySelector('ion-content')
      )

      while (!isInScroll(mainPageElement) && isMounted.value) {
        await props.scroll()
      }
    }

    onMounted(
      () => {
        isMounted.value = true
        loadUntilScrollable()
      }
    )

    onUpdated(
      loadUntilScrollable
    )

    onBeforeUnmount(
      () => isMounted.value = false
    )

    const doScroll = wrapAsyncInInfiniteScroll(props.scroll)

    return {
      doScroll
    }
  }
})

</script>
<style scoped>
.infinite-loading {
  background: rgba(255,255,255,.5);
  padding: 1rem;
}

.infinite-loading {
  margin-bottom: 0;
}

.infinite-scroll-content-md .infinite-loading-text {
  color: #333
}
</style>