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
  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”?
  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
  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
  5. Putting our drinking hats on All of our rows The

    rows we need to show Visible area One row
  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)
  7. 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)
  8. Writing the code import Html.App exposing (beginnerProgram) main = beginnerProgram

    { model = initializedModel , view = view , update = update }
  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
  10. Update type Msg = NoOp | UserScroll Int update :

    Msg -> Model -> Model update action model = case action of NoOp -> model UserScroll scrollTop -> calculateVisibleIndices model scrollTop
  11. View Sea of <div>s with inline-block for the cells of

    the rows Importantly: view : Model -> Html Msg view model = [...] [ div [ [...] , onScroll UserScroll
  12. 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
  13. 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
  14. 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<number>, rowHeight$ : Stream<number>, rowCount$ : Stream<number>, scrollTop$ : Stream<number> ) { let firstVisibleRow$ = xs.combine(scrollTop$, rowHeight$) .map(([scrollTop, rowHeight]) => Math.floor(scrollTop / rowHeight)) .compose(dropRepeats<number>()); 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); }
  15. Typescript (cont.) function intent(DOM : DOMSource) { return DOM.select('#scroll-table-container') .events('scroll',

    {useCapture: true}) .map(e => (<Element>e.target).scrollTop) } interface Model { tableHeight: number, rowHeight: number, columns: string[], rowCount: number, visibleIndices: number[] }
  16. 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`
  17. 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) [...]
  18. 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
  19. 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 (<! visible-indices-channel)] (swap! app-state assoc :visible-indices x))))
  20. 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))))))}))