- Sun 04 March 2018
- PureScript
- #purescript, #pux
Code Walkthrough
In the first part of this post we discussed the four major components Pux architecture (State
, Event
, view
, and foldp
) and started to walk through the code of my Hexlight mini app.
We left off discussing the view
function right at the point where we introduced a regex to match color codes.
view (continued)
The next section will deal with the question how to best integrate the regex matching in the view
function.
Here's the code of matchInput
again:
matchInput :: String -> Maybe (Array (Maybe String))
matchInput input = case regex "#[0-9a-fA-F]{6}" global of
Right rx -> match rx input
Left _ -> Nothing
Design Decisions
I wanted the view
function to focus as much as possible on shoving HTML elements and styles around.
Hence I decided to encapsulate the whole logic of dealing with matchInput
's result in a showColors
function:
view state = div do
h2 $ text "drop some text!"
div do
textarea $ text ""
showColors (matchInput state.input) showColor
where state.input
contains the text in the textarea and showColor
would show an individual color (something like (\color -> span $ text color)
).
This leaves showColors
with a pretty unhandy signature (and a few other issues):
showColors :: Maybe (Array (Maybe String)) -> (String -> HTML Event) -> HTML Event
showColors Nothing showColor = show "no match"
showColors (Just colorMatches) show =
case sequenceDefault colorMatches of
Nothing -> show "no match"
Just matches -> for_ matches show
My idea of providing the function showColor :: (String -> HTML Event)
as a parameter was that showColors
would not have to know about any HTML.
However, this did not work out so well.
Due to the Maybe
with an array of more Maybe
in it, I either needed to have a special case in the showColor
function for "no match" or handle the rendering of this special case directly here.
In any case I had the handling of Nothing
duplicated.
In the next iteration I decided to factor out the Maybe
before passing the match result to showColors
.
This way, only one Nothing
case has to be handled (and in the end, all we care for is if there's a match or not).
I also removed the showColor
parameter, as it couldn't keep the HTML out as much as I had hoped.
showColors :: Maybe (Array String) -> HTML Event
showColors Nothing = span $ text "no match"
showColors (Just colors) = for_ colors \color -> do
span $ text color
view :: State -> HTML Event
view state = div do
h2 $ text "drop some text!"
div do
textarea $ text ""
showColors $ traverse join $ sequence (matchInput state.input)
This was quite a fun exercise for working with Traversable, although I'm still not sure if this really is a good way to do it.
In the final form I moved the handling of the Nothing
case out of showColors
altogether.
view
and showColors
both have to know how to render HTML now, so it made sense to gather all the matching logic on one place.
Moreover, I believe showColors
is a better fit for it's name now.
showColors :: Array String -> HTML Event
showColors colors = div $ for_ colors \color -> do
span $ text color
view :: State -> HTML Event
view state = div do
h2 $ text "drop some text!"
div do
textarea $ text ""
case (traverse join $ sequence (matchInput state.input)) of
Nothing -> div $ span $ text "no colors found"
Just colors -> do
showColors colors
CSS and Colors
It's time to bring in some colors and styling. The recommended way to compose CSS with Pux is the purescript-css package.
Smolder.Markup provides the operator !
(with
) to add an attribute to a markup node, Pux provides style
to create such an attribute from CSS.
As I found it quite confusing how the packages play together and where the individual functions come from, I'll specify the import statements as well.
So in order to specify the width of a div we can do
import CSS (pct, width)
import Pux.DOM.HTML.Attributes (style)
import Text.Smolder.HTML (div, span)
import Text.Smolder.Markup ((!))
styledDiv = div ! style do width (80.0 # pct)
$ span -- ... div elements
By the way, #
is a syntactic sugar function called applyFlipped
, basically a reversed $
.
(80.0 # pct)
is the same as (pct 80.0)
, it just reads very nicely for units.
In this app, showColors
is where most CSS happens.
This is what we want to have:
Each color code is displayed in a block with the background color it represents. There's a small margin around each block, and the text is centered in the block. As a little bonus, the text is rendered black or white depending on the background.
So the first exciting thing we want to do here is to create a CSS color attribute from the color code string that fell out of the regex.
Conveniently, purescript-css's fromHexString
does exactly that.
Maybe.
But we won't pay much attention to the case that it fails, as our regex should only give us valid color codes—and because this is only a toy app ;)
showColors :: Array String -> HTML Event
showColors colors =
div ! style do width (80.0 # pct)
$ for_ colors \hexString -> do
case fromHexString hexString of
Just col -> do
span ! style do backgroundColor col
$ text hexString
Nothing -> text "error"
That's it for the background already.
The other interesting thing here is adjusting the text color to the background color.
Of course this is an issue that StackOverflow knows an answer to.
It's an interesting read that I recommend, but the simplified and approximated version boils down to using dark text color if the background's relative luminance is larger than 0.179
, and light text otherwise.
Again very conveniently, purescript-css has a function for calculating a color's relative luminance as well.
Together with the not-so-exciting margins and centering, our final showColors
looks like this:
showColors :: Array String -> HTML Event
showColors colors =
div ! style do width (80.0 # pct)
$ for_ colors \hexString -> do
case fromHexString hexString of
Just col -> do
span ! style do display inlineBlock
margin (4.0 # px) (4.0 # px) (4.0 # px) (4.0 # px)
textAlign center
width (7.0 # em)
backgroundColor col
color (if luminance col > 0.179 then black else white)
Nothing -> text "error"
onChange
The final thing left to do in view
before we can look into foldp
is adding the interactivity.
So far, we match the colors found in state.input
and render them very nicely, but there's no connection to the content of our textarea.
Luckily, this is very easy. In the previous post we defined our very simple event type:
import Pux.DOM.Events (DOMEvent)
data Event = InputChange DOMEvent
Let's create such an event when the content of the textarea changes (I'm leaving out all CSS for the sake of clarity here):
import Pux.DOM.Events (onChange)
import Text.Smolder.Markup ((#!))
view :: State -> HTML Event
view state = div do
h2 $ text "drop some text!"
div do
textarea #! onChange InputChange
$ text ""
case (traverse join $ sequence (matchInput state.input)) of
Nothing -> div $ span $ text "no colors found"
Just colors -> do
showColors colors
Similar to how !
allows us to attach an attribute to a markup element, #!
allows us to attach an event.
onChange
then takes our event constructor and creates the event we need.
Since our event is so simple this is really all we need to do here.
And this takes us directly to the final part, where the event we just created ends up in foldp
.
foldp
So our goal here is to take up the event we created above and use it to transition to a new State
.
Let's get right to it:
import Pux.DOM.Events (targetValue)
foldp :: Event -> State -> EffModel State Event (dom :: DOM)
foldp (InputChange ev) s =
{ state: s { input = targetValue ev }
, effects: []
}
At least to me, this seemed a little magical at first. But in the end of the day it's not that complicated.
foldp
receives an event and the current state as it's input; that's what it needs to determine the new state.
It produces a record called EffModel
(a type from Pux) that wraps the new state and an array of additional effects.
In our simple case, there are no additional effects and the new state contains just the updated input.
This leaves targetValue
the only part left to be explained.
Obviously, it is able to extract the content of our textarea from the event, so we can assign it to state.input
.
Probably it's just my lack of Javascript that this was not clear to me, but apparently every event has a target
(textarea) and that target has a value
(the content).
And targetValue
just returns the event.target.value
from our DOMEvent
, which was nicely packaged into it by onChange
. That's it.
Conclusion
With all four components, State
, Event
, view
, and foldp
defined and plugged together in our main
function, this concludes our walkthrough through this mini app.
I liked Pux very much.
It is nice how concerns can be separated quite cleanly with this architecture.
I think it was a good idea to try to keep the HTML and CSS rendering as far away from the business logic as possible.
If I where to finetune the app further, I'd try to encapsulate handling the result of matchInput
better, so view
wouldn't have to deal with it.
So what is left to be wished for?
In retrospect, the most difficult part for me was understanding Pux' wrapper types (such as CoreEffects
, EffModel
, and DOMEvent
).
None of these is really that complicated in the end, and after playing around with them for a while I understood that it's all in the documentation on Pursuit.
However, I think what would have helped me most would be more examples on Pursuit.
It's a problem that I often have when getting started with a new Haskell library as well:
Once you know how it works, the documentation as a type reference is very useful.
But when trying to understand how to use the library, types are not enough for a beginner.