Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Making a simple Scroll Table with Elm

Making a simple Scroll Table with Elm

Mini-talk I gave for the Futurice WWWeeklies

Justin Woo

June 18, 2016
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

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

    View Slide

  2. 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”?

    View Slide

  3. 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

    View Slide

  4. 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

    View Slide

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

    View Slide

  6. 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)

    View Slide

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

    View Slide

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

    View Slide

  9. 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

    View Slide

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

    View Slide

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

    View Slide

  12. Handling events
    onScroll : (Int -> msg) -> Attribute msg
    onScroll tagger =
    on "scroll" -- 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

    View Slide

  13. Result
    (A live demo would be nice)

    View Slide

  14. 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

    View Slide

  15. Scroll Table in Other
    Languages
    Typescript, Purescript,
    Clojurescript

    View Slide

  16. 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);
    }

    View Slide

  17. 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[]
    }

    View Slide

  18. 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`

    View Slide

  19. 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)
    [...]

    View Slide

  20. 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

    View Slide

  21. 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 ((swap! app-state
    assoc :visible-indices x))))

    View Slide

  22. 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))))))}))

    View Slide