Making a Scroll Table with Purescript-Halogen

  • 0
  • 0

    By now I've made this in a variety of ways, using React, Cycle.js, and Elm, so naturally, I thought it'd be fitting to make this with Purescript. But instead of using the reactive-lite version like I did last time, I wanted to dive into the most mature solution for UI development in Purescript: purescript-halogen. This won't really adequately explain all the parts involved, but hopefully pique your interest in learning more about Purescript and purescript-halogen.


    Vad är "scroll table"?

    A scroll table (in my definition) is a table that will only try to display the rows required in the user's current view. I accomplish this by setting a static row height, getting the height of my view, and the current scroll position of my view.

    Based on this, my application state type is defined:

    type VisibleIndices = Array Int
    type State =
      { height :: Int
      , width :: Int
      , colWidth :: Int
      , rowCount :: Int
      , rowHeight :: Int
      , visibleIndices :: Array Int

    And the function for calculating my visible indices is fairly straightforward:

    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 }

    Then the initial state of my application can be calculated:

    initialState :: State
    initialState =
        { height: 600
        , width: 900
        , colWidth: 300
        , rowCount: 10000
        , rowHeight: 30
        , visibleIndices: []

    Using purescript-halogen

    Our main function will be a function of purescript-halogen's effects and is pretty similar to Elm's StartApp bootstrap:

    main :: Eff (HalogenEffects ()) Unit
    main = runAff throwException (const (pure unit)) do
      app <- runUI ui initialState
      appendToBody app.node

    Now for the fun part: we need to define ui to feed in here. First, we need to define our component query.

    Halogen components have a query type that they expect, that basically declares the kinds of "actions" that can be sent to the component to handle, which they can use to modify the state of the component. In our scroll table, the only query we care about is the user's scrolling, with the parameter of the scroll position of our view when the user scrolls:

    data Query a
      = UserScroll Int a

    Then, combining purescript-css and purescript-halogen-css, I was also able to type my CSS:

    ui :: forall g. (Functor g) => Component State Query g
    ui = component render eval
        render :: State -> ComponentHTML Query
        render state =
            [ H.h1
              [ do TextAlign.textAlign ]
              [ H.text "Scroll Table!!!!" ]
            , H.div_
                [ H.div
                    [ P.class_ $ className "container"
                    , do
                        Display.position Display.relative
                        Geometry.height $ Size.px (toNumber state.height)
                        Geometry.width $ Size.px (toNumber state.width)
                        Overflow.overflowX Overflow.hidden
                        Border.border Border.solid (Size.px 1.0)
                    , E.onScroll $ E.input \x -> UserScroll (getScrollTop
                    [ tableView state ]
        eval :: Natural Query (ComponentDSL State Query g)
        eval (UserScroll e next) = do
          modify $ \s -> calculateVisibleIndices s e
          pure next

    In building the ComponentHTML, functions with name_ don't take property lists, whereas name do. It's nice to have the distinction in the above. For the scroll event, I use (Halogen.HTML.Events.)onScroll, and use input in order to define a function that will return a Query accordingly. The evaluation step just needs to handle my UserScroll query, and just uses calculateVisibleIndices to do so. (If you do want to read an explanation of the type signatures and how they work, probably it is best to read the purescript-halogen README)

    I cheated for getting the scrollTop of my element though, as it doesn't seem the HTMLElement type has the property available. No problem though, cheating by using FFI is quite easy:

    foreign import getScrollTop :: HTMLElement -> Int
    exports.getScrollTop = function (target) {
      return target.scrollTop;

    The only thing missing now is our tableView function that we used above:

    tableView :: State -> ComponentHTML Query
    tableView { rowCount, rowHeight, colWidth, visibleIndices } =
        [ do Geometry.height $ Size.px (toNumber (rowCount * rowHeight)) ]
        [ H.tbody_ $ map makeRow visibleIndices ]
        makeRow index = do
          let i = toNumber index
          let key = show (i % (toNumber (length visibleIndices)))

            [ P.key key
            , do
              Display.position Display.absolute
     $ Size.px (i * (toNumber rowHeight))
              Geometry.width $ Size.pct (toNumber 100)
              [ do Geometry.width (Size.px (toNumber colWidth)) ]
              [ H.text $ show i ]
              [ do Geometry.width (Size.px (toNumber colWidth)) ]
              [ H.text $ show (i * 1.0) ]
              [ do Geometry.width (Size.px (toNumber colWidth)) ]
              [ H.text $ show (i * 100.0) ]

    And that's it!


    So hopefully I've shown that writing Purescript and using purescript-halogen isn't too terrifying. Also, while I haven't been selling it, it's fairly nice to know that all the inline CSS styles I'm using are valid. But even more than that, every time I compile, I know that everything will work, and the only bugs I need to worry about are logical ones that I've introduced (which can be easily tested with generative testing and such).

    If you made it this far, thanks for reading! Please also tweet me your reactions, criticisms, praises, remarks, comments, responses, suggestions, advice, complaints, thoughts, explanations, corrections, and/or declarations on the futility of writing in a language that compiles to Javascript instead of Javascript itself or something.