Introducing plzwrk - A Haskell front-end framework

mikesol

Mike Solomon

Posted on May 14, 2020

Introducing plzwrk - A Haskell front-end framework

Table of Contents

I really like programming in Haskell. Recently, I learned about Asterius, a Haskell-to-WebAssembly compiler. So I thought it'd be fun to create a small frontend library to build websites that are compiled with Asterius.

Voilà plzwrk.

I've used it to build a couple toy websites and I must say the process has been quite delightful. In this article, I'd like to present a brief overview of plzwrk. If you're a frontend developer and interested in Haskell, I hope that you'll give it a try!

A hello world in plzwrk

Here it is!

{-# LANGUAGE QuasiQuotes       #-}

import           Web.Framework.Plzwrk
import           Web.Framework.Plzwrk.Asterius

main :: IO ()
main = do
  browser <- asteriusBrowser
  let element = [hsx|<p>Hello world!</p>|]
  plzwrk'_ element browser
Enter fullscreen mode Exit fullscreen mode

Let's unpack what's going on.

On the first line of the main function, we are creating a browser. plzwrk ships with two browser representations - one backed by Asterius (which we use above) - and a mock one that is useful for testing.

On the second line of main, we create our element to be inserted into the DOM. plzwrk has two ways to insert elements into the DOM:

  • hsx, which is similar to jsx.
  • Functions like div and img from Web.Framework.Plzwrk.Tag that correspond to HTML tags (ie div creates a <div></div> tag).

If you've used jsx or templating languages like Nunjucks before, hsx will probably be more comfortable.

The third and last line sends the element and the browser to plzwrk for rendering. Here's how it looks.

Composing elements

Here's how we can add an element into another element in plzwrk:

main :: IO ()
main = do
  browser <- asteriusBrowser
  let element = [hsx|<p>Hello world!</p>|]
  let bigger = [hsx|<div style="background-color:red">
    #el{replicate 10 element}#
  </div>
  |]
  plzwrk'_ bigger browser
Enter fullscreen mode Exit fullscreen mode

This takes our "Hello world!" and repeats it 10 times.

#el{}# tells hsx to expect a list of elements between the curly brackets. hsx currently has four types of Haskell values that it can accept in curly brackets:

  • el is a list of elements.
  • e is a single element.
  • t is a single piece of text, either in the body of an element or as an attribute.
  • c is a callback that one would supply to click or input.

Let's use them all in the example below:

main :: IO ()
main = do
  browser <- asteriusBrowser
  let who = "world"
  let mystyle = "background-color:red"
  let element = [hsx|<p>Hello #t{who}#</p>|]
  let bigger = [hsx|<div click=#c{(\e s -> (consoleLogS browser) "clicked")}# style=#t{mystyle}#>
    #el{replicate 10 element}#
    #e{element}#
  </div>
  |]
  plzwrk'_ bigger browser
Enter fullscreen mode Exit fullscreen mode

In the lambda function #c{(\e s -> return $ (consoleLogS browser) "clicked")}#, we see two arguments passed to the listener: e is an event and s is the state.

Speaking of state, let's see how plzwrk handles state!

Stateful elements

All elements that accept arguments from a state are created with hsx' (note the apostrophe) or with a function like div' or img' (again, note the apostrophe).

let elt = (\name -> [hsx'|<p>#t{name}#</p>|])
Enter fullscreen mode Exit fullscreen mode

When it's time to render using plzwrk, we compose elt with a getter from a state that will hydrate name:

newtype Person = Person { _name :: String }

main :: IO ()
main = do
  browser <- asteriusBrowser
  let elt = (\name -> [hsx'|<p>#t{name}#</p>|])
  plzwrk' (elt <$> _name) (Person "Stacey") browser
Enter fullscreen mode Exit fullscreen mode

You can see the above example live here.

All event listeners in plzwrk return a modified state, and this modified state is then used to update the DOM.


newtype Person = Person { _name :: String }

main :: IO ()
main = do
  browser <- asteriusBrowser
  let elt = (\name -> [hsx'|<div>
    <p>#t{name}#</p>
    <button click=#c{(\_ s -> return $ Person "Bob")}#>Change name</button>
  </div>|])
  plzwrk' (elt <$> _name) (Person "Stacey") browser
Enter fullscreen mode Exit fullscreen mode

When we click the "Change name" button, the name will change from "Stacey" to "Bob" in the UI.

The pattern above allows elements to declaritively state their dependencies and leave it to the implementation of the state to provide these dependencies. For example, elt accepts any string as a name, and it's only when we call elt <$> _name that we link it to the Person type.


data Person = Person { _name :: String, _age :: Int }

main :: IO ()
main = do
  browser <- asteriusBrowser
  let mystyle = "background-color:pink"
  let element = (\age -> [hsx'|<p>You just turned <span>#t{show age}#</span>. Congrats!</p>|]) <$> _age
  let bigger = (\name age -> [hsx'|<div style=#t{mystyle}#>
    <h1>#t{name}#</h1>
    #el{replicate age element}#
    #e{p__ ":-)"}#
  </div>
  |]) <$> _name <*> _age
  plzwrk' bigger (Person "Joe" 42) browser
Enter fullscreen mode Exit fullscreen mode

You can see the above example live here.

In the above example, the phrase "You just turned 42. Congrats!" will be printed 42 times.

Both element and bigger are composed with getters from a state. This is similar to the strategy used in Redux with one caveat - instead of passing setters to elements, we pass the entire state to event handlers. This keeps the actual elements pure (i.e. no accidentally making a network call from the DOM construction function) and allows for maximum flexibility in working with the state.

Getting started with plzwrk

plzwrk uses Asterius as its backend for web development. Compiling an application using plzwrk is no different than compiling an application using ahc-cabal and ahc-dist as described in the Asterius documentation with one caveat. You must use --constraint "plzwrk +plzwrk-enable-asterius" when running ahc-cabal.

A minimal flow is shown below, mostly copied from the Asterius documentation. It assumes that you have a cabal-buildable project in the pwd. Note the use of the --constraint "plzwrk +plzwrk-enable-asterius" flag in the ahc-cabal step:

username@hostname:~/my-dir$ docker run --rm -it -v $(pwd):/project -w /project meeshkan/plzwrk
asterius@hostname:/project$ ahc-link --input-hs Main.hs --browser --bundle
Enter fullscreen mode Exit fullscreen mode

Thanks

Thanks for checking out plzwrk! I'm looking forward to seeing what people build with it. If you've built something with plzwrk and would like to show it off in the README, if you would like to see any features or if you spot a bug, please file an issue on GitHub!

💖 💪 🙅 🚩
mikesol
Mike Solomon

Posted on May 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related