We have a long article list that are created every day like medium
& we also care about the UX.
We are also using:
- SSR approach, that mean it reloads the page while navigating
- Infinite scroll, that means we have paging & append to the end of the list
Everything went right for the first time till we realized a problem that make poor UX. The normal user behaviors something like:
- Access article list screen
- Select an article to read
- Back to step 1
What if we select an article at page 5, that makes no sense since we have to scroll to page 5 & continue our work.
It's bad UX for the current behavior, right? I can see this problem can easily solve with SPA approach & it's too late to modify something at the architecture level. To overcome this bad UX, we keep monitoring the current position on GUI & scroll to that position if any.
OK, let's move on to the implementation details
First, monitor the scroll position while scrolling & scroll to that position after loading page
function _trackViewedItem(item) {
window.ggjLastNaviItem = {id: item.id, page: item.page}
let sp = new URLSearchParams(location.search)
sp.set('tr', `${item.id}_${item.page}`)
history.replaceState(
{},
'',
location.pathname + '?' + sp.toString())
}
function ioNaviItem(itemCls, itemImgCls, matchPoint = .5, threshold = [.5, .75, 1.0]) {
this.$nextTick(() => {
let io = new IntersectionObserver(entries => {
if (window.ggjPreventScrollEvent) return
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
let ir = entries[0].intersectionRatio,
direction = this.lastIntersectionRatio < ir
if (ir >= matchPoint && ((window.ggjScrollDirection == 'down' && direction) ||
(window.ggjScrollDirection == 'up' && !direction))) {
_trackViewedItem(this.item)
}
this.lastIntersectionRatio = ir
}, {threshold})
// start observing element
io.observe(this.$el)
// try to scroll to that element after this element mounted into dom
function scrollToEl() {
let tr = new URLSearchParams(location.search).get('tr')
if (!window.ggjPreventScrollEvent && tr && /^\d+_\d+$/.test(tr) && tr.split('_')[0] == this.item.id) {
window.ggjPreventScrollEvent = true
let el = $(this.$el)
window.scrollTo(0, el.find(itemCls).offset().top - el.find(itemImgCls).height())
this.$el.scrollIntoView()
setTimeout(() => {
window.ggjPreventScrollEvent = false
})
}
}
scrollToEl.call(this)
// special treatment for safari on mac
// safari does not reload page when using back button
// need to scroll to el manually
if (/mac/i.test(navigator.userAgent) && /safari/i.test(navigator.userAgent)) {
window.addEventListener('pageshow', event => {
if (event.persisted) {
scrollToEl.call(this)
}
})
}
})
}
It watches each article item & up-to-date value at query param
IntersectionObserver
observe each article element visible on GUI-
window.history
retain the info at query param, it contains 2 infos:- Current page: query data with that page
- Current article item: scroll to that article
- Example: ?tr={id}_{page}
- There is a special treatment for Safari on macOS, our idea rely much on something like
load
event & normal browser will reload the page whenever you click back button to move back the list screen, Safari did not work as expected, it will load from cache instead of actually reload the page, by usingpageshow
event we can deal with it.
Finally, we need to deal with receiving data & appending data when scrolling. The idea behind, we load only data for the current page, then load page +/- 1
in case scroll up/down.
The algorithm to detect scroll up/down is quite simple, by comparing between previous & current scroll position
$(window).on('scroll', scrollDirection)
...
function scrollDirection() {
// detect scroll direction
let st = window.pageYOffset || document.documentElement.scrollTop
// value can be true downscroll | false upscroll
window.ggjScrollDirection = st > window.ggjLastScrollY ? 'down' : 'up'
// For Mobile or negative scrolling
window.ggjLastScrollY = st <= 0 ? 0 : st
}
then, we will define 2 boundaries, whenever user scrolls exceed these boundaries, it will trigger to get data according to that page
- at the top: get data for
page - 1
- at the bottom: get data for
page + 1