GithubHelp home page GithubHelp logo

mithril-infinite's Introduction

Infinite Scroll for Mithril

A component to handle scrolling of an "infinite" list or grid, while only drawing what is on screen (plus a bit of pre-fetching), so safe to use on mobiles.

Compatible with Mithril 1.x.

Examples

Examples

Features

  • Natural scrolling using browser defaults.
  • Fast and fluent (on desktop and modern mobiles).
  • Can be used for lists, grids and table-like data.
  • Items that are out of sight are removed, so only a fraction of the total content is drawn on the screen. This is good for speed and memory consumption.
  • Support for unequal content heights and dynamic resizing of content elements.
  • As more data is loaded, the scroll view increases in size, so that the scrollbar can be used to go back to a specific point on the page.
  • Items are handled per "page", which is a normal way of handling batches of search results from the server.
  • Pages can contain an arbitrary and unequal amount of items.
  • Pre-fetches data of the current page, the page before and after (or n pages ahead).
  • When there is room on the page to show more data: automatic detection of "loadable space" (so no loading area detection div is needed).
  • Optionally use previous/next paging buttons.
  • Supports dynamic content (for instance when filtering results).

Not included (by design):

  • Special support for older mobile browsers: no touch layer, requestAnimationFrame, absolute positioning or speed/deceleration calculations.

Installation

Use as npm module:

npm install --save mithril-infinite

or download/clone from Github.

For working with the examples, see the examples documentation.

Usage

Note: The parent of "scroll-view" must have a height. Also make sure that html has a height (typically set to 100%).

Handling data

Data can be provided:

  • With pageUrl for referencing URLs
  • With pageData for server requests

Using pageUrl for referencing URLs

An example using data files:

import infinite from "mithril-infinite"

m(infinite, {
  maxPages: 16,
  pageUrl: pageNum => `data/page-${pageNum}.json`,
  item
})

With these options we are:

  • limiting the number of pages to 16
  • passing a function to generate a JSON data URL
  • passing a function that should return a Mithril element

A simple item function:

const item = (data, opts, index) => 
  m(".item", [
    m("h2", data.title),
    m("div", data.body)
  ])

The item function passes 3 parameters:

  1. data contains the loaded data from pageUrl.
  2. opts contains: isScrolling: Bool, pageId: String, pageNum: Number, pageSize: Number
  3. index: the item index

Data file structure

Data is handled per "results" page. You are free to use any data format.

You could use a JSON data object for each page, containing a list of items. For example:

[
  {
    "src": "cat.jpg",
    "width": 500,
    "height": 375
  }
]

Or:

[
  ["red", "#ff0000"],
]

Using pageData for server requests

In most real world situations an API server will provide the data. So while passing file URLs with pageUrl is a handy shortcut, we preferably use data requests.

With m.request
import infinite from "mithril-infinite"

const pageData = pageNum => 
  m.request({
    method: "GET",
    dataType: "jsonp",
    url: dataUrl(pageNum)
  })

m(infinite, {
  pageData,
  item
})

Demo tip: in the example "Grid" we use jsonplaceholder.typicode.com to fetch our images:

const PAGE_ITEMS = 10

const dataUrl = pageNum =>
  `http://jsonplaceholder.typicode.com/photos?_start=${(pageNum - 1) * PAGE_ITEMS}&_end=${pageNum * PAGE_ITEMS}`

With async
import infinite from "mithril-infinite"

const asyncPageData = async function(pageNum) {
  try {
    const response = await fetch(dataUrl(pageNum))
    return response.json()
  } catch (ex) {
    //console.log('parsing failed', ex)
  }
}

m(infinite, {
  pageData: asyncPageData,
  item
})

Returning data directly
import infinite from "mithril-infinite"

const returnData = () =>
  [{ /* some data */ }]

m(infinite, {
  pageData: returnData,
  item
})

Returning data as a Promise
import infinite from "mithril-infinite"

const returnDelayedData = () =>
  new Promise(resolve =>
    setTimeout(() =>
      resolve(data)
    , 1000)
  )

m(infinite, {
  pageData: returnDelayedData,
  item
})

Handling dynamic data

In situations where the Infinite component needs to show different items - for instance when filtering or sorting search results - we must provide a unique key for each page. The key will enable Mithril to properly distinguish the pages.

Use option pageKey to provide a function that returns a unique identifying string. For example:

import infinite from "mithril-infinite"
import stream from "mithril/stream"

const query = stream("")

const Search = {
  view: () =>
    m("div", 
      m("input", {
        oninput: e => query(e.target.value),
        value: query()
      })
    )
}

const MyComponent = {
  view: () => {
    const queryStr = query()
    return m(infinite, {
      before: m(Search),
      pageKey: pageNum => `${pageNum}-${queryString}`,
      // other options
    })
  }
}

Advanced item function example

To enhance the current loading behavior, we:

  • Load images when they are visible in the viewport
  • Stop loading images when the page is scrolling. This makes a big difference in performance, but it will not always result in a good user experience - the page will seem "dead" when during the scrolling. So use with consideration.

The item function can now look like this:

const item = (data, opts) =>
  m("a.grid-item",
    m(".image-holder",
      m(".image", {
        oncreate: vnode => maybeShowImage(vnode, data, opts.isScrolling),
        onupdate: vnode => maybeShowImage(vnode, data, opts.isScrolling)
      })
    )
  )

// Don't load the image if the page is scrolling
const maybeShowImage = (vnode, data, isScrolling) => {
  if (isScrolling || vnode.state.inited) {
    return
  }
  // Only load the image when visible in the viewport
  if (infinite.isElementInViewport({ el: vnode.dom })) {
    showImage(vnode.dom, data.thumbnailUrl)
    vnode.state.inited = true
  }
el.style.backgroundImage = `url(${url})`

Getting the total page count

How the total page count is delivered will differ per server. jsonplaceholder.typicode.com passes the info in the request header.

Example "Fixed" shows how to get the total page count from the request, and use that to calculate the total content height.

We place the pageData function in the oninit function so that we have easy access to the state.pageCount variable:

const state = vnode.state
state.pageCount = 1

state.pageData = pageNum => 
  m.request({
    method: "GET",
    dataType: "jsonp",
    url: dataUrl(pageNum),
    extract: xhr => (
      // Read the total count from the header
      state.pageCount = Math.ceil(parseInt(xhr.getResponseHeader("X-Total-Count"), 10) / PAGE_ITEMS),
      JSON.parse(xhr.responseText)
    )
  })

Then pass state.pageData to infinite:

m(infinite, {
  pageData: state.pageData,
  maxPages: state.pageCount,
  ...
})

Using images

For a better loading experience (and data usage), images should be loaded only when they appear on the screen. To check if the image is in the viewport, you can use the function infinite.isElementInViewport({ el }). For example:

if (infinite.isElementInViewport({ el: vnode.dom })) {
  loadImage(vnode.dom, data.thumbnailUrl)
}

Images should not be shown with the <img/> tag: while this works fine on desktop browsers, this causes redrawing glitches on iOS Safari. The solution is to use background-image. For example:

el.style.backgroundImage = `url(${url})`

Using table data

Using <table> tags causes reflow problems. Use divs instead, with CSS styling for table features. For example:

.page {
  display: table;
  width: 100%;
}
.list-item {
  width: 100%;
  display: table-row;
}
.list-item > div {
  display: table-cell;
}

Pagination

See the "Paging" example.

Custom page wrapper

Use processPageData to either:

  • Process content data before passing to item
  • Wrap in a custom element wrapper
  • Use a custom item function

Simple example with a wrapper:

m(infinite, {
  processPageData: (content, options)  => {
    return m(".my-page", content.map((data, index) => item(data, options, index)));
  },
  ...
});

Configuration options

Appearance options

Parameter Mandatory Type Default Description
scrollView optional Selector String Pass an element's selector to assign another element as scrollView
class optional String Extra CSS class appended to mithril-infinite__scroll-view
contentTag optional String "div" HTML tag for the content element
pageTag optional String "div" HTML tag for the page element; note that pages have class mithril-infinite__page plus either mithril-infinite__page--odd or mithril-infinite__page--even
maxPages optional Number Number.MAX_VALUE Maximum number of pages to draw
preloadPages optional Number 1 Number of pages to preload when the app starts; if room is available, this number will increase automatically
axis optional String "y" The scroll axis, either "y" or "x"
autoSize optional Boolean true Set to false to not set the width or height in CSS
before optional Mithril template or component Content shown before the pages; has class mithril-infinite__before
after optional Mithril template or component Content shown after the pages; has class mithril-infinite__after; will be shown only when content exists and the last page is in view (when maxPages is defined)
contentSize optional Number (pixels) Use when you know the number of items to display and the height of the content, and when predictable scrollbar behaviour is desired (without jumps when content is loaded); pass a pixel value to set the size (height or width) of the scroll content, thereby overriding the dynamically calculated height; use together with pageSize
setDimensions optional Function ({scrolled: Number, size: Number}) Sets the initial size and scroll position of scrollView; this function is called once

Callback functions

Parameter Mandatory Type Default Description
pageUrl either pageData or pageUrl Function (page: Number) => String Function that accepts a page number and returns a URL String
pageData either pageData or pageUrl Function (page: Number) => Promise Function that fetches data; accepts a page number and returns a promise
item required: either item or processPageData Function (data: Array, options: Object, index: Number) => Mithril Template Function that creates a Mithril element from received data
pageSize optional Function (content: Array) => Number Pass a pixel value to set the size (height or width) of each page; the function accepts the page content and returns the size
pageChange optional Function (page: Number) Get notified when a new page is shown
processPageData required: either item or processPageData Function (data: Array, options: Object) => Array Function that maps over the page data and returns an item for each
getDimensions optional Function () => {scrolled: Number, size: Number} Returns an object with state dimensions of scrollView: scrolled (either scrollTop or scrollLeft) and size (either height or width); this function is called on each view update
pageKey optional Function (page: Number) => String key is based on page number Function to provide a unique key for each Page component; use this when showing dynamic page data, for instance based on sorting or filtering

Paging options

Parameter Mandatory Type Default Description
currentPage optional Number Sets the current page
from optional Number Not needed when only one page is shown (use currentPage); use page data from this number and higher
to optional Number Not needed when only one page is shown (use currentPage); Use page data to this number and lower

Options for infinite.isElementInViewport

All options are passed in an options object: infinite.isElementInViewport({ el, leeway })

Parameter Mandatory Type Default Description
el required HTML Element The element to check
axis optional String "y" The scroll axis, either "y" or "x"
leeway optional Number 300 The extended area; by default the image is already fetched when it is 100px outside of the viewport; both bottom and top leeway are calculated

Styling

Note: The parent of "scroll-view" must have a height. Also make sure that html has a height (typically set to 100%).

Styles are added using j2c. This library is also used in the examples.

CSS classes

Element Key Class
Scroll view scrollView mithril-infinite__scroll-view
Scroll content scrollContent mithril-infinite__scroll-content
Content container content mithril-infinite__content
Pages container pages mithril-infinite__pages
Page page mithril-infinite__page
Content before before mithril-infinite__before
Content after after mithril-infinite__after
State Key Class
Scroll view, x axis scrollViewX mithril-infinite__scroll-view--x
Scroll view, y axis scrollViewY mithril-infinite__scroll-view--y
Even numbered page pageEven mithril-infinite__page--even
Odd numbered page pageOdd mithril-infinite__page--odd
Page, now placeholder placeholder mithril-infinite__page--placeholder

Fixed scroll and overflow-anchor

Some browsers use overflow-anchor to prevent content from jumping as the page loads more data above the viewport. This may conflict how Infinite inserts content in "placeholder slots".

To prevent miscalculations of content size, the "scroll content" element has style overflow-anchor: none.

Size

Minified and gzipped: ~ 3.9 Kb

Dependencies

Licence

MIT

mithril-infinite's People

Contributors

arthurclemens avatar dependabot[bot] avatar mcsnappy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

mithril-infinite's Issues

Scroll Direction

I'm looking at using this tool for a chat application and I have a few questions.

  1. Can you set the scroll direction? Chat applications traditionally add new content to the bottom of the chat log and you scroll up to see more. I'd like to use this to simulate that user experience.

  2. Page size. How well does this handle dynamically sized content. When I load in some of the messages, it is impossible to tell how tall the message will be until it finishes rendering. Will this tool handle this well and adjust?

  3. Pinning. If I have a chat application and the user is at the bottom of the chat, when new messages come in I would like to rerender the chat with the new message and scrolled all the way to the bottom. However if the user is not at the bottom and they scroll up, it makes sense to not move the chat log and keep the current location. Do you have any suggestions for this problem?

Thanks for your time. And sorry if this isn't the correct place to ask.

class isn't optional

if class option is not present, then throws Uncaught SyntaxError: Failed to execute 'add' on 'DOMTokenList': The token provided must not be empty.

NPM install pulls down Mithril

When I try to install this via NPM, on windows it pulls down mithril. However, on OSX, it will pull down correctly (along with j2c).

A small image of the issue

image

Why is m.redraw being used inside the view?

See here

since m.redraw does nothing if it's already redrawing, which if you're in a view I'd think is nearly all the time, doesn't this simply not work?

Wouldn't it be wiser to put it inside a setTimeout so that the current stack finishes before redraw is called again?

No redraw after `pageData` call on mithril0.2 version

Hello, I've got an issue with mithril-infinite version 0.2.

When my function page data is called, I return a promise containing the new items. But those are not redraw. As a result, I have to call m.redraw with a timeout in order to display my items:

.tap(function redrawTimeout() {
  setTimeout(m.redraw.bind(null, null), 100);
});

It's not a pretty way to do it ^^

Looking at the code, I saw that when calling opts.pageData you handle the promise giving the result to the page but no redraw is called.

if (result.then) {
  // A Promise
  result.then(function (r) {
    content(r);
  });
} else {
  content = result;
}

When I add one, everything works fine:

if (result.then) {
  // A Promise
  result.then(function (r) {
    content(r);
    m.redraw();
  });
} else {
  content = result;
}

I'm going to make PR.

What do you think?

regards,

Robin

Code improvement suggestions

I was taking a gander at your mithril-infinite, and noticed a few things that could be done differently:

  1. Why these pointless Object.assign calls, when you could just return the literal instead? (That's one of many in that file alone.)

  2. This setTimeout is redundant in light of the above (edit: forgot to remove this part), and is completely unused elsewhere.

  3. This removeEventListener will never remove the scroll event handler you think it will because the handler is always created afresh for each instance.

    You should instead set it as a state property in the main oninit and just directly add/remove the event listener here in this node. This will simplify the code and eliminate a memory leak.

  4. I would do this in a frame-throttled resize event handler, to only force a style recalc when the state.scrollView element is resized, rather than on every redraw. In addition, it's a chance to schedule a redraw to update the UI in case it's resized without the user updating anything.

Refresh component on demand

Sometimes, the page data is expected to be cached but updated later once server response is received. If a page data is returned from cache but updated later, there should be some provision to refresh the component.

Scrolling does not load

I am trying to create a very simple example, but there are some things that are really not clear to me.
This is my setup:

view: function() {
    return m('div', {style: 'height: 600px;'}, m(List, {
      header: {
        title: 'the title'
      },
      tiles: m(infinite, {
        item,
        pageData,
      }),
    }));
  }

I also made sure that html has a height of 100%.

What happens with the above example is that first, the div get's overflown, so infinite is actually much bigger than the height of 600px. Then the first two pages are loaded, and when scrolling to the end nothing happens. I can see neither network requests nor any errors in the console. What am I doing wrong here?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.