Lens

Lens combinators for Shpadoinkle applications. The primary use case is the composition of heterogeneous Html components.

Heterogeneous Composition

Optics in general are an ideal way to compose heterogeneous Html components.

The lens library is famous for making bringing first-class lenses to Haskell. However, to derive lenses for any data type, we’ve found it’s best to use generic-lens with -XOverloadedLabels.

On Record

We can use 'OverloadedLabels' to gain lenses to each field on a record, then compose heterogeneous sub-components onto the parent state with onRecord.

{-# LANGUAGE OverloadedLabels #-}

import Data.Generics.Labels () -- Generic Label instances for Lens

data Form = Form
  { name :: Text
  , age  :: Int
  } deriving Generic

form :: Form -> Html m Form
form f = div_
  [ label [ for "name" ] [ "Name" ]
  , onRecord #name $ input' (2)
    [ id "name"
    , value $ f ^. #name
    , onInput $ const . id (1)
    ]
  , label [ for "age" ] [ "Age" ]
  , onRecord #age $ input' (4)
    [ id "name"
    , value . pack .show $ f ^. #age
    , onInput $ const . fromMaybe 0 . readMay . unpack (3)
    ]
  ]
1 This will be an input element for the "name". We use id as our event handler to set the state to whatever the handler sees as the current value. Therefore, this prop is Prop m Text.
2 We have an impedance Html m Text vs Html m Form which we resolve with the use of onRecord and a generic derived lens as a label #name.
3 This input will capture the "age". We use readMay and fromMaybe 0 to convert the incoming Text to an Int. Therefore, this prop is Prop m Int.
4 Another impedance Html m Int vs Html m Form which we resolve with the use of onRecord and a generic derived lens as a label #age.

On Sum

Here we add it a component that increments an Int.

newtype Counter = Counter Int
  deriving (Eq, Ord, Num, Show, Generic)

counter :: Counter -> Html m Counter
counter c = div_
  [ label [ for' "counter" ] [ text . pack $ show c ]
  , button [ id' "counter", onClick (+ 1) ] [ "Increment" ]
  ]

We’ll also add a Model to switch between the Html Form above and this new component Html m Counter.

{-# LANGUAGE OverloadedLabels #-}

import Data.Generics.Labels ()

data Model
  = MCounter Counter
  | MForm Form
  deriving (Eq, Show, Generic)

view :: Model -> Html m Model
view = \case
  MCounter c -> div_
    [ onSum #_MCounter $ counter c (1)
    , button [ onClick . const . MForm $ Form "" 18 ] [ "Go to Form" ]
    ]
  MForm f    -> div_
    [ onSum #_MForm $ form f (2)
    , button [ onClick . const $ MCounter 0 ] [ "Go to Counter" ]
    ]
1 Here we have an impedance Html m Counter vs Html m Model which we resolve with the use of onSum and a generic derived prism as a label #_MCounter.
2 Another impedance, Html m Form vs Html m Model which we resolve with the use of onSum and a generic derived prism as a label #_MForm.

And because we can use these lens combinators to compose heterogeneous Html at any place we wish, adding in navigation buttons to switch the Model from one constructor to the other is trivial.