Skip to content

Latest commit

 

History

History
184 lines (148 loc) · 7.49 KB

2013-12-07-24-days-of-hackage-threepenny-gui.md

File metadata and controls

184 lines (148 loc) · 7.49 KB
title
24 Days of Hackage: threepenny-gui

While a lot of the work I tend to do in Haskell is building libraries, there have been plenty of times where I would have loved to provide a GUI. Usually, I shy away from this task, and settle for a command line interface - working with GUI libraries is usually a nightmare, both in code and deployment! In July earlier this year, Heinrich Apfelmus announced the release of his new GUI library threepenny-gui, which can solve both of these problems. threepenny-gui is a Haskell library to build GUIs, but with a twist: rather than deal with bindings to GTK or QT, threepenny-gui uses an interface that is already almost universally acceptable - web pages!

threepenny-gui is thus a collection of common UI elements, a way of composing them into web pages, and the ability to watch these interface elements for interactions. threepenny-gui also uses web sockets to provide a tight feedback loop between the client and the server. Today, we'll explore the threepenny-gui library by building a to-do list.

Before we even begin considering how to write the GUI, we should start by defining a few data types that will sufficiently model the application. We could use persistent for our data store, as we at saw yesterday - but instead I'll just work with an in-memory database using Haskell types.

type Database = Map UserName ToDoList
type UserName = String
type ToDoList = Set String

Our "database" will be a map from usernames to to-do lists, where a username is just a String and a to-do list is a Set of Strings - very straight forward. Next, we can start up a server for our application using threepenny-gui:

main :: IO ()
main = startGUI defaultConfig setup

setup :: Window -> UI ()
setup rootWindow = undefined

startGUI begins a HTTP server, here we use the default configuration which listens on port 10000. The setup action takes a Window - which corresponds to the browser page the client is viewing - and builds up the UI of the entire application. Every client connection will call setup on connection, which means by default there is no cross-communication or state. To add state and cross-communication, we need to explicitly introduce a shared variable. To achieve this sharing, we'll use a mutable variable within the STM framework:

main :: IO ()
main = do
  database <- atomically $ newTVar (Map.empty)
  startGUI defaultConfig (setup database)

setup :: TVar Database -> Window -> UI ()
setup database rootWindow = do

Now we're in a position to start building our UI. The first stage of the UI is to prompt the client for their username in order to determine which to-do list to display to them:

setup :: TVar Database -> Window -> UI ()
setup database rootWindow = void $ do
  userNameInput <- UI.input
    # set (attr "placeholder") "User name"

  loginButton <- UI.button #+ [ string "Login" ]

  getBody rootWindow #+
    map element [ userNameInput, loginButton ]

First of all we call UI.input, which creates a new <input> element. The # operator is reverse function application, which we use to then change the placeholder attribute of our newly created text field. To create our login button, we create a button element with UI.button, and then we add a string element as a child of the button element in order to set the text content.

Now that we have both of these UI elements, we can append them to the body of the rootWindow UI element. getBody takes a Window and returns the corresponding Element for the window, which we can append to using #+.

Next, we need to respond to the user clicking the "login" button, which we do by observing the click event:

  on UI.click loginButton $ \_ -> do

Now, what do we need to do when someone clicks "login"? We need to look up that username in our database and, if they exist, present a list of to-do items the user has created. We should also show an input field to add new to-do items. The first part is straight forward:

    userName <- get value userNameInput

    currentItems <- fmap Set.toList $ liftIO $ atomically $ do
      db <- readTVar database
      case Map.lookup userName db of
        Nothing -> do
           writeTVar database (Map.insert userName Set.empty db)
           return Set.empty

        Just items -> return items

We get the value of the userNameInput input field, and then enter an STM transaction to find all the to-do items. We attempt to lookup the user in the database, and if they have items we return them. Otherwise, we "register" a new user and give them an empty set of to-do items. Next, we render the user's to-do items:

    let showItem item = UI.li #+ [ string item ]
    toDoContainer <- UI.ul #+ map showItem currentItems

Again, straight forward! I just map a showItem routine over each to-do entry, and contain them all in a <ul> container. We'll need to add new entries to this list later, so we'll bind the <ul> element to a name.

The last bit of functionality is the ability to create new to-do items. For this, lets use another <input> element, and when the user presses "return" have the item be added to the list:

    newItem <- UI.input

    on UI.sendValue newItem $ \input -> do
      liftIO $ atomically $ modifyTVar database $
        Map.adjust (Set.insert input) userName

      set UI.value "" (element newItem)
      element toDoContainer #+ [ showItem input ]

A new input element is created, and we listen for the sendValue event, which is produced when a client presses return. Then we run a little STM transaction to add a new item to the to-do list. Once this transaction completes, we clear the input field and append the newly created to-do item to the to-do list.

Finally, we combine this all together into a UI:

    header <- UI.h1 #+ [ string $ userName ++ "'s To-Do List" ]
    set children
      [ header, toDoContainer, newItem ]
      (getBody rootWindow)

And we're done! If we head over to http://localhost:10000 we're presented with a username field, and if we fill this in we'll be shown a list of to-do items. Changing the values and then logging in as the same user again shows the items have persisted, just as we were intending.

Today is my first exposure to threepenny-gui, and overall I really like what I'm seeing! In a little over 50 lines of code we have a functioning application, and I feel I understand a lot of what I'm doing. There are a few parts that still feel a little odd - for example passing around UI Elements, where (to me) it would feel more natural to just pass an Element instead. Nonetheless, that's a minor concern that shouldn't stop you experimenting with this fantastic project.

I also started my first draft of this post raving on about FRP, but observant readers will notice we didn't really use any of the FRP functionality that threepenny-gui offers. Sadly, I didn't get time to touch this part of the library, and I think it has the potential to do more interesting things. For example, making the to-do list collaborative by reacting to asynchronous changes to the to-do list.

As always, the code for today's example is over on Github - feel free to have a play and see what you come up with!