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
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.
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
}
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:
scroll
eventsHow 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:
it loads Elm app and attaches it to some DOM element on the page it
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With