Mike Solomon
Posted on May 14, 2020
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
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 tojsx
. - Functions like
div
andimg
fromWeb.Framework.Plzwrk.Tag
that correspond to HTML tags (iediv
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
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 toclick
orinput
.
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
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>|])
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
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
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
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
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!
Posted on May 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.