Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement infinite scroll in Elm?

Tags:

javascript

elm

I try to implement infinite for my app and decided to do this in the following way. Here code extracted from index.html it binds Elm app to particular node and also define some code that will be triggered on scroll event:

(function() {
        
        var loadMore = function () {
          return $(window).scrollTop() === $(document).height() - $(window).height() 
        };

        var node  = document.getElementById('main');
        var myApp = Elm.Main.embed(node);

        $(window).bind('scroll', function () {
          var isBottom = loadMore();
          myApp.ports.scroll.send(isBottom);
        });

      })();

I assume it does what I need, but I'm not 100% sure.

The part I don't understand is how to handle this in Elm code. My current approach (which doesn't work) is following. I provide it just to make my intent more clear of what I try to achieve.

-- SUBSCRIPTIONS
port scroll : (Bool -> msg) -> Sub msg

subscriptions : Model -> Sub Msg 
subscriptions model = 
  scroll Scroll
like image 527
SuperManEver Avatar asked Nov 19 '16 08:11

SuperManEver


2 Answers

A complete implementation

Here is a working implementation without ports: https://ellie-app.com/5R4Fw95QLQfa1.

When you scroll to the end of the list, it loads more list items.

Performance considerations

In this implementation our event decoder is fetching offsetHeight to inform us about our container's height. This causes constant reflows and might impact your program's performance. A better alternative is to know the height of the scroll element beforehand, or remove the event listener as soon as you find the height value.

module Main exposing (main)

import Browser
import Html
import Html exposing (Html, Attribute, ul, li, text, button, div)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput, onClick, on)
import Json.Decode
import List
import String

type alias Model = List String

initialModel =
    [ "Pamplemousse"
    , "Ananas"
    , "Jus d'orange"
    , "Boeuf"
    , "Soupe du jour"
    , "Camembert"
    , "Jacques Cousteau"
    , "Baguette"
    ]


-- UPDATE


type Msg
    = LoadMore
    | ScrollEvent ScrollInfo


type alias ScrollInfo =
    { scrollHeight : Float
    , scrollTop : Float
    , offsetHeight : Float
    }


update msg model =
    case msg of
        LoadMore ->
            List.concat [ model, initialModel ]

        ScrollEvent { scrollHeight, scrollTop, offsetHeight } ->
            if (scrollHeight - scrollTop) <= offsetHeight then
                List.concat [ model, initialModel ]
            else
                model



-- VIEW


view content =
    div [ onScroll ScrollEvent ]
        [ ul
            [ class "grocery-list"
            , style  "height" "300px" 
            , style "display" "block" 
            , style "overflow" "scroll" 
            , onScroll ScrollEvent
            ]
            (List.map listItem content)
        , button [ onClick LoadMore ] [ text "load more" ]
        ]




listItem itemText =
    li
        [ style  "height" "50px" 
        , style "display" "block" 
        ]
        [ text itemText ]


onScroll msg =
    on "scroll" (Json.Decode.map msg scrollInfoDecoder)


scrollInfoDecoder =
    Json.Decode.map3 ScrollInfo
        (Json.Decode.at [ "target", "scrollHeight" ] Json.Decode.float)
        (Json.Decode.at [ "target", "scrollTop" ] Json.Decode.float)
        (Json.Decode.at [ "target", "offsetHeight" ] Json.Decode.float)

main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }
like image 179
Marcelo Lazaroni Avatar answered Sep 28 '22 18:09

Marcelo Lazaroni


In order to implement infinite scroll using approach I chose you need few things. I'm going to give some high level overview of what is going on, what main components are and how this all fit together and then dive into some code.

Abstractly speaking I need following things:

  • I need to somehow say to browser that I interested in scroll events
  • I need to trigger some code when this event occur.

How do I say browser that I'm interested in scroll events ?

Because current implementation of scroll event in Elm either doesn't exist or hard to work with I decided to use jQuery to work with those events.

Here is you can see all code I use from index.html. It does few essential things:

  1. it loads Elm app and attaches it to some DOM element on the page it

  2. binds a callback to scroll event that will be triggered every time this event occurs

    (function($) {
    
      var loadMore = function () {
        return $(window).scrollTop() === $(document).height() - $(window).height() 
    };
    
      var node  = document.getElementById('main');
      var myApp = Elm.Main.embed(node);
    
      $(window).bind('scroll', function () {
        var isBottom = loadMore();
        myApp.ports.scroll.send(isBottom);
      });
    
    })(jQuery);
    

I'd like to draw your attention to this line :

myApp.ports.scroll.send(isBottom);

Here is how I sent some data into Elm world.

myApp is just name of variable that holds a reference to Elm's app, nothing fancy here.

ports is just a keyword you have to use in order to implement this kind of things.

scroll this is name of function that will be called on the Elm's side. It defined by you (I will show later how to do this)

send is mandatory part. This is how you send data to Elm app.

Now all I need to do is somehow receive this data on Elm's side.

Again, high level overview. Now data is marching to my Elm app and all I need to do is to subscribe to this event (We don't have callbacks in Elm, we have subscriptions :) )

I did this in following steps. I created module called Ports with following content:

port module Ports exposing (..)

port scroll : (Bool -> msg) -> Sub msg

port keyword before module keyword is mandatory if you want to be able to retrieve data from outside of Elm's world.

Next, I import this module in my App.elm, this is essentially some root level module (main coordination node). I just need to add this line:

import Ports exposing (..)

Next, I need to define subscriptions in my App.elm like this:

subscriptions : Model -> Sub Msg 
subscriptions model = 
  scroll Scroll

Essentially I subscribe to particular event and when this event occurs particular Msg will be dispatched.

I need few other things to make this whole thing works:

  • I need to include Scroll message into my Msg data type

  • Handle this case in update function

    type Msg = NoOp | Scroll Bool

As you can see I indicated in value constructor Scroll that I'm expecting boolean value

And of course update function. Depending on whether pos true or false I trigger some code to load more articles, for example.

update msg model = 
  case msg of 
    NoOp -> 
      model ! []

    Scroll pos -> 
      -- do something with it 
like image 36
SuperManEver Avatar answered Sep 28 '22 20:09

SuperManEver