Slide 1

Slide 1 text

Making a simple Scroll Table with Elm How to display a ton-a-rows without slideshow framerates

Slide 2

Slide 2 text

Table where you can scroll through tons of items AKA “Infinite scroll” Useful when… pagination would be ugly client wants to display 1k+ items for whatever reason making something not laggy What is this “Scroll Table”?

Slide 3

Slide 3 text

What is Elm? Toolset for making web apps Language + Framework in one Simple, but useful type system “If it compiles, it works” Basically no runtime exceptions

Slide 4

Slide 4 text

Getting started - Brainstorming How to make something slow faster? Pretty much can’t -- only so many gigahertzies Solution Only rows clients can see need to be visible Cuts down our workload from O(N) to O(1) Sounds like magic

Slide 5

Slide 5 text

Putting our drinking hats on All of our rows The rows we need to show Visible area One row

Slide 6

Slide 6 text

Maybe the US is way behind on education… Height AreaVisible = Number RowsVisible * Height Row ➡ Number RowsVisible = Height AreaVisible / Height Row Index FirstVisibleRow = Position Scroll / Height Row Index LastVisibleRow = Index FirstVisibleRow + Rows Visible Indices Visible = [Index FirstVisibleRow .. Index LastVisibleRow ] Doing 8th grade math (basic Algebra)

Slide 7

Slide 7 text

Basic Math to Code calculateVisibleIndices model scrollTop = let { rowHeight, rowCount, height } = model firstRow = max 0 <| scrollTop // rowHeight - 10 visibleRows = (height + 1) // rowHeight lastRow = min rowCount <| firstRow + visibleRows + 20 in { model | visibleIndices = [firstRow..lastRow] } (buffering more rows so scrolling looks nicer)

Slide 8

Slide 8 text

Writing the code import Html.App exposing (beginnerProgram) main = beginnerProgram { model = initializedModel , view = view , update = update }

Slide 9

Slide 9 text

Model type alias VisibleIndices = List Int type alias Model = { height : Int , width : Int , cellWidth : Int , rowCount : Int , rowHeight : Int , visibleIndices : VisibleIndices } tableWidth = 900 initialModel : Model initialModel = { height = 500 , width = tableWidth , cellWidth = tableWidth // 3 , rowCount = 1000 , rowHeight = 30 , visibleIndices = [] } initializedModel = calculateVisibleIndices initialModel 0

Slide 10

Slide 10 text

Update type Msg = NoOp | UserScroll Int update : Msg -> Model -> Model update action model = case action of NoOp -> model UserScroll scrollTop -> calculateVisibleIndices model scrollTop

Slide 11

Slide 11 text

View Sea of
s with inline-block for the cells of the rows Importantly: view : Model -> Html Msg view model = [...] [ div [ [...] , onScroll UserScroll

Slide 12

Slide 12 text

Handling events onScroll : (Int -> msg) -> Attribute msg onScroll tagger = on "scroll" <| Json.map tagger scrollTop -- on “scroll” $ constructor <$> scrollTop div [ [...] , onScroll UserScroll scrollTop : Json.Decoder Int scrollTop = Json.at [ "target", "scrollTop" ] Json.int type Msg = NoOp | UserScroll Int -- UserScroll : Int -> Msg

Slide 13

Slide 13 text

Result (A live demo would be nice)

Slide 14

Slide 14 text

Resources Repo: https://github.com/justinwoo/elm-scroll-table/ Elm: elm-lang.org Old blag post: http://qiita.com/kimagure/items/57cdd08bdf56cc51d294 In Purescript (Halogen): https://github.com/justinwoo/purescript-scroll-table In Cycle: https://github.com/justinwoo/cycle-scroll-table In ClojureScript (reagent, core.async): https://github.com/justinwoo/reagent- core-async-scroll-table

Slide 15

Slide 15 text

Scroll Table in Other Languages Typescript, Purescript, Clojurescript

Slide 16

Slide 16 text

Typescript (Cycle.js) About as you’d expect function main({DOM}) { let actions = intent(DOM); let state$ = model(actions); let vtree$ = view(state$); return { DOM: vtree$ }; } run(main, { DOM: makeDOMDriver('#app') }); function makeVisibleIndices$( tableHeight$ : Stream, rowHeight$ : Stream, rowCount$ : Stream, scrollTop$ : Stream ) { let firstVisibleRow$ = xs.combine(scrollTop$, rowHeight$) .map(([scrollTop, rowHeight]) => Math.floor(scrollTop / rowHeight)) .compose(dropRepeats()); let visibleRows$ = xs.combine(tableHeight$, rowHeight$) .map(([tableHeight, rowHeight]) => Math.ceil (tableHeight / rowHeight) ); let visibleIndices$ = xs.combine(rowCount$, visibleRows$, firstVisibleRow$) .map(([rowCount, visibleRows, firstVisibleRow]) => { let visibleIndices : number[] = []; let lastRow = firstVisibleRow + visibleRows; for (let i = firstVisibleRow; i <= lastRow; i++) { visibleIndices.push(i); }

Slide 17

Slide 17 text

Typescript (cont.) function intent(DOM : DOMSource) { return DOM.select('#scroll-table-container') .events('scroll', {useCapture: true}) .map(e => (e.target).scrollTop) } interface Model { tableHeight: number, rowHeight: number, columns: string[], rowCount: number, visibleIndices: number[] }

Slide 18

Slide 18 text

Purescript! This version done with Halogen Based on Components kind of like React Uses Queries for what needs to change Eval for getting the new state More involved - being simplified in the next major version http://www.slideshare.net/jdegoes/halogen-past-present-and-future`

Slide 19

Slide 19 text

Purescript (cont.) data Query a = UserScroll Int a foreign import getScrollTop :: HTMLElement -> Int ui = component render eval where render :: State -> ComponentHTML Query render state = [...] [ P.class_ $ className "container" , CSS.style do Display.position Display.relative [...] , E.onScroll $ E.input \x -> UserScroll (getScrollTop x.target) [...]

Slide 20

Slide 20 text

Purescript (cont. 2) calculateVisibleIndices :: State -> Int -> State calculateVisibleIndices model scrollTop = case model of { rowHeight, rowCount, height } -> do let firstRow = scrollTop / rowHeight let visibleRows = (height + 1) / rowHeight let lastRow = firstRow + visibleRows model { visibleIndices = firstRow..lastRow } eval :: Natural Query (ComponentDSL State Query g) eval (UserScroll e next) = do modify $ \s -> calculateVisibleIndices s e pure next

Slide 21

Slide 21 text

Clojurescript Using Reagent atoms and Core.async (def app-state (atom {:height 500 :width 800 :row-count 100 :row-height 30 :visible-indices []})) (def scroll-channel (chan)) (go (>! scroll-channel 0)) (def visible-indices-channel (make-visible-indices-channel scroll-channel)) (go (while true (let [x (

Slide 22

Slide 22 text

Clojurescript (cont.) (def root-view [...] [:div#app-container [:div.scroll-table-container {:id "scroll-table-container" [...] {:component-did-mount #(let [node (dom/getElement "scroll-table-container")] (events/listen node "scroll" (fn [] (go (put! scroll-channel (.-scrollTop node))))))}))