Skip to content

Latest commit

 

History

History
166 lines (132 loc) · 6.73 KB

2016-03-15-making-a-scroll-table-with-purescript-halogen.md

File metadata and controls

166 lines (132 loc) · 6.73 KB

Making a Scroll Table with Purescript-Halogen

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.

Method

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 =
  calculateVisibleIndices
    { height: 600
    , width: 900
    , colWidth: 300
    , rowCount: 10000
    , rowHeight: 30
    , visibleIndices: []
    }
    0

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
  where
    render :: State -> ComponentHTML Query
    render state =
      H.div_
        [ H.h1
          [ CSS.style do TextAlign.textAlign TextAlign.center ]
          [ H.text "Scroll Table!!!!" ]
        , H.div_
            [ H.div
                [ P.class_ $ className "container"
                , CSS.style 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) Color.black
                , E.onScroll $ E.input \x -> UserScroll (getScrollTop x.target)
                ]
                [ 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 } =
  H.table
    [ CSS.style do Geometry.height $ Size.px (toNumber (rowCount * rowHeight)) ]
    [ H.tbody_ $ map makeRow visibleIndices ]
  where
    makeRow index = do
      let i = toNumber index
      let key = show (i % (toNumber (length visibleIndices)))

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

And that's it!

Conclusion

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.

Links