reflex-jsx
is a parser and quasiquoter that lets you write
html-like syntax inside of a reflex MonadWidget t m
instance.
What's reflex, you ask? In short, it's a Functional Reactive Programming library in Haskell that compiles to JavaScript for use on websites. You'd use it anywhere you might otherwise use React or Angular.
Here's a quick motivating example. The following code shows a Github badge based on the username you enter, updated as you type.
In regular Reflex, here's what the code looks like (leaving out data types and the XHR)
Here's what that same widget looks like with reflex-jsx
:
Get a build working with Reflex. Due to GHCJS, this isn't as
trivial as normal libraries. I recommend going through Ryan
Trinkle's
reflex instructions
first, and getting a basic app up and running (using the
try-reflex
command).
From there, you can depend on the reflex-jsx
library inside of
build-depends
of your cabal file as usual, and use the work-on
command from the above platform. It looks something like this:
/path/to/reflex-platform/work-on ghcjs ./your-project
cd your-project/src
ghcjs Main.hs
This would work if your code lives in src
and your main file is
Main.hs
. Make sure you invoke work-on
from the parent directory.
At the top of the file, enable quasi-quoting, and import the lib:
{-# LANGUAGE QuasiQuotes #-}
import ReflexJsx
Use the code in a function, like so:
mainLayout :: MonadWidget t m => String -> m () -> m ()
mainLayout title content = do
[jsx| <div>
<div class="header">{text title}</div>
<div class="body">{content}</div>
</div>
|]
Something like:
[jsx| <div class="red" style="color:blue"></div> |]
is converted into
el "div" (Map.fromList [("class", "red"), ("style", "color:blue")]) $ text ""
So there's no restriction on the element type, and the attributes are
(String, String)
as it is in Reflex itself.
Splicing
or Antiquoting
is where you can embed a haskell expression inside
of the quasiquoted value. You can embed an expression anywhere you'd normally
put a DOM node, and you escape it with {}
. The constraint is that any
expression you pass in needs to evaluate to MonadWidget t m => m a
- which is
the normal type for any elements, static or dynamic.
For example, you could embed a simple text "value"
that is unchanging, or a
textInput
element that changes over time. The only thing to remember is that
you can't currently get values out of the nodes you insert, so for more
complicated data flows, you might not be able to use a jsx
block at all. In
that case, it is probably easier to use functions anyway.
Here's a more meaty example from a food tracking app:
consumedFoodView :: MonadWidget t m => ConsumedFood -> m ()
consumedFoodView (ConsumedFood id name nutrition amount) =
do [jsx| <div class="consumed-food">
<div class="line">
<div class="stats">
<span class="food-name">{text name}</span>
<div class="calories">
{nutritionData (calories nutrition) "cals"}
</div>
</div>
{macrosPie nutrition 50}
</div>
<div class="line">
<div class="macros">
{nutritionData (proteinGrams nutrition) "protein (g)"}
{nutritionData (fatGrams nutrition) "fat (g)"}
{nutritionData (carbGrams nutrition) "carbs (g)"}
</div>
</div>
</div> |]
Or, take a look at the syntax-highlighted version in Emacs if you tweak your
haskell-mode
slightly.
Notice we have a component that takes in a ConsumedFood
with some values, and
splices them into the jsx body. First we have {text name}
, which is using the
reflex function text :: MonadWidget t m => String -> m ()
to take a constant
string and display it inside the span
.
Next, you can see some more complicated splicing. nutritionData
is itself a
custom component created in this file, and is a MonadWidget t m
as well. You
can put any haskell expression inside the curly braces, as long as it evaluates
to a reflex widget. The other examples here are just more of the same type of
calls.
String
attributeThe example above lets you splice in your own nodes, but you can also splice in an attribute value. For instance, you can do something like this:
[jsx| <div style={"color: " ++ myColor ++ ";width:10px;height:10px"}/> |]
If you replace all of the attributes, you can pass in a set of dynamic ones. To
do this, you pass in an expression that evaluates to a type
Dynamic t (Map String String)
.
This means you are passing in a Map
that changes over time, and the attributes
will update and re-render automatically as necessary.
Here's an example of modifying a color based on a text box:
colorSelector :: MonadWidget t m => m ()
colorSelector = do
input <- textInput def
selectorAttrs <- mapDyn (\color -> Map.singleton "style" $ "color:" ++ color) (_textInput_value input)
[jsx| <div {...selectorAttrs}>test selector</div> |]
The value (_textInput_value input)
returns a Dynamic t String
of changing
input box values, and then the mapping line creates a dynamic map value with the
color equalling the current textbox value. This gets spliced in as the
attributes to the div
in the jsx body, and causes the text to dynamically
change color.
Gtk2HsSetup
This is due to the following issue: https://github.com/gtk2hs/gtk2hs/issues/179 .
The fix is to run stack install gtk2hs-buildtools
.
The name comes from React's JSX, but it's not trying to be feature-for-feature mapping of functionality. It's simply meant to play a similar role to JSX in that it allows the programmer to write HTML-like syntax where it feels appropriate (i.e. HTML-heavy components that don't have as much interactivity).
I almost called it hsx
until I discovered that there was already an
established package with that name on Hackage.
I imagine Reflex users will want to use this syntax less often than React users use JSX, as Reflex's interspersed return values make some types of components easier to represent with regular functions. I don't recommend this be used all over the place, because the template haskell slows down compile times, and it adds a layer of indirection.
Sorry about that - I actually am not sure how to make the errors nicer. I
essentially just splice in whatever you provide as Template Haskell expressions
using haskell-src-meta
and let the compiler figure it out if it's not
well-formed. However, sometimes the compiler gets really confused and fails in a
weird way. If anyone knows how to do reliable reflection on TH Exp datatypes to
determine what they are (i.e. String
vs. MonadWidget t m
), then I could fail
earlier with a nicer message.
Consider it extremely experimental. It's not feature complete, and has only been used by me a little bit on a side-project, so far from battle tested. Also, this is my first time working with Quasiquotes and Template Haskell so the implementation might broken. Do let me know if you run into issues though, and I will fix them as quickly as possible!
I'm happy to get suggestions on any aspect of the library! I may be doing things unidiomatically and appreciate getting style, implementation, and any other feedback you want to provide. Even better if you want to submit a PR to improve the library.