Stack Builders logo
Arrow icon Insights

The Modular, Functional Client Side

Haskell can be used as a tool for building wonderfully flexible and maintainable client-side web applications.

Management of many is the same as management of few. It is a matter of organization.

—Sun Tzu

The valuable tool is not simply the one which allows us to move quickly from point 'A' to point 'B'; it is the one that enables a rapid change in course to point 'C' when 'B' is no longer deemed desirable. Too often are we wooed by frameworks offering the elegant five line example which yields promises of simplicity and development speed only to find that, in many cases, the clarity of these examples are illusions which scale poorly with the demands of real-world requirements. As such, it is helpful to keep in mind that true simplicity is a matter of modularity and organization - not a matter of terseness.

<iframe src="/static/news/bindings-tutorial.js_.jsexe/index.html" sandbox="allow-scripts" scrolling="no" frameborder=0 width=345 height=240 style="float: right;"> </iframe>

In this post we present a minimal yet illustrative data binding example where we do not depend on any client side web framework. Instead we use a small set of easy to understand tools and techniques which together provide an alternative for modern client side development. These tools and techniques include Functional Reactive Programming (FRP) for data binding (provided by the Sodium package) and two embedded domain languages, BlazeHtml and Clay, which allow easy semantic decomposition and recombination of HTML and CSS respectively. We also make use of the GHCJS compiler which graciously compiles our Haskell to the JavaScript, HTML, and CSS needed to power the demonstration neighboring this paragraph.

main :: IO ()
main = runWebGUI $ \ webView -> do

    doc  <- getDocument webView
    body <- findBody doc

    htmlElementSetInnerHTML body html

    firstNameInputField <- findInputElement doc "firstNameInput"
    lastNameInputField  <- findInputElement doc "lastNameInput"
    fullNameInputField  <- findInputElement doc "fullNameInput"

    (firstNameEvents, publisherFn) <- sync newEvent
    (lastNameEvents, publisherFn') <- sync newEvent

    elementOnkeyup firstNameInputField $ publishInputValues publisherFn
    elementOnkeyup lastNameInputField  $ publishInputValues publisherFn'

    let fullNameEvents = firstNameEvents <> lastNameEvents

    sync $ listen fullNameEvents $ const $ do
      val  <- htmlInputElementGetValue firstNameInputField
      val' <- htmlInputElementGetValue lastNameInputField
      htmlInputElementSetValue fullNameInputField $ val <> " " <> val'

    return ()

In the above code example we make use of GHCJS-DOM; a rather firmly typed JavaScript DOM manipulation library. As mentioned above, Sodium takes care of the reactivity of the little example by converting our DOM events into streams to be the subjects of manipulation. Programming using FRP allows us to handle our events and time-varying values in a functional way, that is, breaking problems down into small functions that are then composed into larger segments of functionality. In the above, we merge two streams and observe the result publishing data back to the user interface whenever we detect the occurrence of an event.

John Boyd went back to the Sabre vs MiG-15 ratios, because he was puzzled by the fact that on paper the MiG-15 was a better plane so why were the Sabres then so successful? Boyd learned that the bubble canopy of the Sabre gave the U.S. pilots better visibility and therefore better situational awareness. The full hydraulic flight controls allowed the Sabre pilots to transition from offensive to defensive maneuvers faster than his Soviet counterpart. Better observation and greater agility were the keys to the success of the Sabre pilots.

—J Lindberg, Fighter Tactics Academy

If this style of programming is new to you then take a moment to think about how we might modify the above code snippet to include a possible middle name input. Imagine a solution that you might use to drop all potential special characters from full names. If you find it easy to envision these solutions it is perhaps the product of our tools offering "situational awareness" rather than offering "rocket boosters" in hopes of quickly speeding us along to point 'B'.

html :: String
html = renderHtml content

content :: Html
content = do
  nameForm
  css

css :: Html
css = style $ toHtml C.css

nameForm :: Html
nameForm = section ! A.id "formContainer" $
  form $ do
    labeledInputField "firstNameInput" "First Name" False
    labeledInputField "lastNameInput"  "Last Name" False
    labeledInputField "fullNameInput"  "Full Name" True

labeledInputField :: AttributeValue -> Html -> Bool -> Html
labeledInputField id_ label_ readonly = p $ do
  input ! A.type_ "text"
        ! A.maxlength "10"
        ! A.id id_
        !? (readonly, A.readonly "")
  label label_

BlazeHTML was designed for optimal composability and, by virtue of being a functional programming library, the ease with which we can break down our HTML into semantic components is quite astonishing. Here we choose to decompose our little example into a simple nameForm comprised of labeledInputFields. Notice how our labeledInputField abstraction is not a cumbersome partial or sub-template its "just a function". Even from this brief example we can see that, with very little ceremony, we are taking an already high-level domain language and crafting flexible components specifically tailored to the needs of our application.

css :: String
css = unpack . render $ formCss

formCss :: Css
formCss = do
  formContainerCss
  formLabelCss
  inputCss
  advertisementCss

formContainerCss :: Css
formContainerCss = "#formContainer" ?
 do background niceGrey
    display    inlineBlock
    border     solid (px 1) "#CDCDCD"
    borderRadius (px 5) (px 5) (px 5) (px 5)
    margin     (em 0.75) (em 0.75) (em 0.75) (em 0.75)
    padding    (px 10) (px 10) (px 10) (px 10)

inputCss :: Css
inputCss = "input" ?
  do padding    (px 9) (px 9) (px 9) (px 9)
     border     solid (px 1) "#E5E5E5"
     outline    solid (px 0) (rgba 0 0 0 255)
     fontFamily ["Verdana", "Tahoma"] [sansSerif]
     fontSize   (px 12)
     background ("#FFFFFF" :: Color)
     boxShadow  (px 0) (px 0) (px 8) (rgba 0 0 0 10)
     background $
       linearGradient (straight sideTop)
     [ ("#ffffff", pct 0.1)
     , ("#eeeeee", pct 14.9)
     , ("#ffffff", pct 85.0)
     ]

formLabelCss :: Css
formLabelCss = "form label" ?
  do marginLeft (px 10)
     fontSize (px 13)
     color coolBreezeGrey

advertisementCss :: Css
advertisementCss = "#ad" ?
  do marginLeft (px 5)
     fontSize (px 10)
     fontStyle italic
     color coolBreezeGrey

coolBreezeGrey :: Color
coolBreezeGrey = "#999999"

niceGrey :: Color
niceGrey = "#F5F5F5"

Finally we take a look at Clay; the vehicle we use to realize our example's dynamically generated CSS. In terms of semantic decomposition and easy recombination, Clay is as advantageous as BlazeHTML. The ease with which a user can compartmentalize her CSS using Clay should not be overlooked. The incremental and straight-forward creation of CSS pattern libraries is a direct benefit of the maneuverability that Clay gives us which stems from the principles we have been discussing throughout this post.

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

—John Carmack

Set against the backdrop of a tech-industry dominated by rigid frameworks, my hope is that this little post has sparked some interest in the style of client-side development provided by traditional functional programming. I encourage those of you who are not already familiar with these techniques to check out tools such as RxJS, ClojureScript, Bacon.js, PureScript, HTML Components, Elm, and others which are paving the way to a functional, modular client-side development experience.

Published on: Jul. 29, 2014
Last updated: Dec. 20, 2024

Written by:

Software Developer
Eric C. Jones

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.