├── css
├── simple.css
└── tab.css
├── Setup.hs
├── images
├── event.png
├── behavior.png
└── dynamic.png
├── src
├── dom01.hs
├── count01.hs
├── textinput01.hs
├── dom02.hs
├── range01.hs
├── main01.hs
├── range02.hs
├── event01.hs
├── textinput06.hs
├── dom03.hs
├── textinput05.hs
├── textinput04.hs
├── textinput03.hs
├── event02.hs
├── event03.hs
├── timer01.hs
├── checkbox01.hs
├── event04.hs
├── checkbox02.hs
├── dom04.hs
├── main02.hs
├── range03.hs
├── dropdown01.hs
├── colorviewer.hs
├── xhr01.hs
├── button01.hs
├── textinput02.hs
├── radio01.hs
├── radio02.hs
├── trace01.hs
├── xhr02.hs
└── xhr03.hs
├── .gitignore
├── reflex-dom-inbits.cabal
├── stack.yaml
├── LICENSE
├── README.md
├── support
└── reflex-chart.hs
└── tutorial.md
/css/simple.css:
--------------------------------------------------------------------------------
1 | h1 {
2 | color: Green;
3 | }
4 |
--------------------------------------------------------------------------------
/Setup.hs:
--------------------------------------------------------------------------------
1 | import Distribution.Simple
2 | main = defaultMain
3 |
--------------------------------------------------------------------------------
/images/event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hansroland/reflex-dom-inbits/HEAD/images/event.png
--------------------------------------------------------------------------------
/images/behavior.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hansroland/reflex-dom-inbits/HEAD/images/behavior.png
--------------------------------------------------------------------------------
/images/dynamic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hansroland/reflex-dom-inbits/HEAD/images/dynamic.png
--------------------------------------------------------------------------------
/src/dom01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | main :: IO ()
5 | main = mainWidget $ el "h1" $ text "Welcome to Reflex"
--------------------------------------------------------------------------------
/src/count01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 |
3 | import Reflex.Dom
4 |
5 | main :: IO ()
6 | main = mainWidget $ display =<< count =<< button "ClickMe"
--------------------------------------------------------------------------------
/src/textinput01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | main :: IO()
5 | main = mainWidget bodyElement
6 |
7 | bodyElement :: MonadWidget t m => m ()
8 | bodyElement = el "div" $ do
9 | el "h2" $ text "Simple Text Input"
10 | ti <- textInput def
11 | dynText $ value ti
--------------------------------------------------------------------------------
/src/dom02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | main :: IO()
5 | main = mainWidget $ do
6 | el "h1" $ text "Welcome to Reflex-Dom"
7 | el "div" $ do
8 | el "p" $ text "Reflex-Dom is:"
9 | el "ul" $ do
10 | el "li" $ text "Fun"
11 | el "li" $ text "Not difficult"
12 | el "li" $ text "Efficient"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Haskell: From http://help.github.com/git-ignore
2 | dist
3 | cabal-dev
4 | *.o
5 | *.hi
6 | *.chi
7 | *.chs.h
8 | .virtualenv
9 | .hsenv
10 | .cabal-sandbox/
11 | .ipynb_checkpoints
12 | cabal.sandbox.config
13 | cabal.config
14 | .stack-work/
15 |
16 | # GHCJS
17 | *.jsexe
18 | *.js_hi
19 | *.js_o
20 | *.js_dyn_hi
21 | *.js_dyn_o
22 |
23 |
--------------------------------------------------------------------------------
/src/range01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | el "h2" $ text "Range Input"
11 | rg <- rangeInput def
12 | el "p" blank
13 | display $ _rangeInput_value rg
14 | return ()
--------------------------------------------------------------------------------
/src/main01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE TemplateHaskell #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | import Reflex.Dom
4 | import Data.FileEmbed
5 |
6 | main :: IO ()
7 | main = mainWidgetWithCss css bodyElement
8 | where css = $(embedFile "css/simple.css")
9 |
10 | bodyElement :: MonadWidget t m => m ()
11 | bodyElement = el "div" $ do
12 | el "h1" $ text "This title should be green"
13 | return ()
--------------------------------------------------------------------------------
/src/range02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | el "h2" $ text "Range Input"
11 | rg <- rangeInput $ def & attributes .~ constDyn ("min" =: "-100")
12 | el "p" blank
13 | display $ _rangeInput_value rg
14 | return ()
--------------------------------------------------------------------------------
/src/event01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RecursiveDo #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 |
4 | import Reflex.Dom
5 |
6 | main :: IO ()
7 | main = mainWidget bodyElement
8 |
9 | bodyElement :: MonadWidget t m => m ()
10 | bodyElement = do
11 | rec el "h2" $ text "Counter as a fold"
12 | numbs <- foldDyn (+) (0 :: Int) (1 <$ evIncr)
13 | el "div" $ display numbs
14 | evIncr <- button "Increment"
15 | return ()
--------------------------------------------------------------------------------
/src/textinput06.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE RecursiveDo #-}
3 | import Reflex.Dom
4 | import qualified Data.Text as T
5 |
6 | main :: IO ()
7 | main = mainWidget body
8 |
9 | body :: MonadWidget t m => m ()
10 | body = do
11 | rec el "h1" $ text "Clear TextInput Widget"
12 | ti <- textInput $ def & setValue .~ ("" <$ evReset)
13 | evReset <- button "Reset"
14 | return ()
--------------------------------------------------------------------------------
/src/dom03.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 | import qualified Data.Map as Map
5 | import Data.Monoid ((<>))
6 |
7 | main :: IO ()
8 | main = mainWidget $ do
9 | el "h1" $ text "A link to Google in a new tab"
10 | elAttr "a" attrs $ text "Google!"
11 |
12 | attrs :: Map.Map T.Text T.Text
13 | attrs = ("target" =: "_blank") <> ("href" =: "http://google.com")
--------------------------------------------------------------------------------
/src/textinput05.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 |
5 | main :: IO ()
6 | main = mainWidget body
7 |
8 | body :: MonadWidget t m => m ()
9 | body = do
10 | el "h1" $ text "Write into TextInput Widget"
11 | t1 <- textInput def
12 | evCopy <- button ">>>"
13 | let evText = tagPromptlyDyn (value t1) evCopy
14 | t2 <- textInput $ def & setValue .~ evText
15 | return ()
--------------------------------------------------------------------------------
/src/textinput04.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | main :: IO ()
5 | main = mainWidget bodyElement
6 |
7 | bodyElement :: MonadWidget t m => m ()
8 | bodyElement = do
9 | el "h2" $ text "Text Input - Read Value on 'Enter'"
10 | ti <- textInput def
11 | el "br" blank
12 | text "Contents of TextInput after 'Enter': "
13 | let evEnter = keypress Enter ti
14 | let evText = tagPromptlyDyn (value ti) evEnter
15 | dynText =<< holdDyn "" evText
--------------------------------------------------------------------------------
/src/textinput03.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | main :: IO ()
5 | main = mainWidget bodyElement
6 |
7 | bodyElement :: MonadWidget t m => m ()
8 | bodyElement = do
9 | el "h2" $ text "Text Input - Read Value on Button Click"
10 | ti <- textInput def
11 | evClick <- button "Click Me"
12 | el "br" blank
13 | text "Contents of TextInput on last click: "
14 | let evText = tagPromptlyDyn (value ti) evClick
15 | dynText =<< holdDyn "" evText
--------------------------------------------------------------------------------
/src/event02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RecursiveDo #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | import Reflex.Dom
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | rec el "h2" $ text "Combining Events with leftmost"
11 | dynCount <- foldDyn (+) (0 :: Int) $ leftmost [1 <$ evIncr, -1 <$ evDecr]
12 | el "div" $ display dynCount
13 | evIncr <- button "Increment"
14 | evDecr <- button "Decrement"
15 | return ()
--------------------------------------------------------------------------------
/css/tab.css:
--------------------------------------------------------------------------------
1 | ul.tab {
2 | background-color: lightgrey;
3 | }
4 |
5 | /* Float the list items side by side */
6 | ul.tab li {float: left;
7 | width: 90px;
8 | border: 1px solid #ccc;
9 | padding: 10;
10 | }
11 |
12 | /* Style the list */
13 | ul.tab {
14 | list-style-type: none;
15 | margin: 0;
16 | padding: 0;
17 | overflow: hidden;
18 | border: 1px solid #ccc;
19 | }
20 |
21 | /* Create an active/current tablink class */
22 | ul li.tabact {
23 | background-color: white;
24 | }
--------------------------------------------------------------------------------
/src/event03.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RecursiveDo #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | import Reflex.Dom
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | rec el "h2" $ text "Combining Events with mergeWith and foldDyn"
11 | dynCount <- foldDyn (+) (0 :: Int) (mergeWith (+) [1 <$ evIncr, -1 <$ evDecr])
12 | el "div" $ display dynCount
13 | evIncr <- button "Increment"
14 | evDecr <- button "Decrement"
15 | return ()
--------------------------------------------------------------------------------
/src/timer01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import Data.Time
4 | import Control.Monad.Trans (liftIO)
5 | import qualified Data.Text as T
6 |
7 | main :: IO ()
8 | main = mainWidget bodyElement
9 |
10 | bodyElement :: MonadWidget t m => m()
11 | bodyElement = do
12 | el "h2" $ text "A Simple Clock"
13 | now <- liftIO getCurrentTime
14 | evTick <- tickLossy 1 now
15 | let evTime = (T.pack . show . _tickInfo_lastUTC) <$> evTick
16 | dynText =<< holdDyn "No ticks yet" evTime
--------------------------------------------------------------------------------
/src/checkbox01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = el "div" $ do
10 | el "h2" $ text "Checkbox (Out of the box)"
11 | cb <- checkbox True def
12 | text "Click me"
13 | el "p" blank
14 | let dynState = checkedState <$> value cb
15 | dynText dynState
16 |
17 | checkedState :: Bool -> T.Text
18 | checkedState True = "Checkbox is checked"
19 | checkedState _ = "Checkbox is not checked"
--------------------------------------------------------------------------------
/src/event04.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RecursiveDo #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | import Reflex.Dom
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | el "h2" $ text "Using foldDyn with function application"
11 | rec dynNum <- foldDyn ($) (0 :: Int) $ leftmost [(+ 1) <$ evIncr, (+ (-1)) <$ evDecr, const 0 <$ evReset]
12 | el "div" $ display dynNum
13 | evIncr <- button "Increment"
14 | evDecr <- button "Decrement"
15 | evReset <- button "Reset"
16 | return ()
--------------------------------------------------------------------------------
/src/checkbox02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = el "div" $ do
10 | el "h2" $ text "Checkbox - User friendly"
11 | cb <- el "label" $ do
12 | cb1 <- checkbox True def
13 | text "Click me"
14 | return cb1
15 | el "p" blank
16 | let dynState = checkedState <$> value cb
17 | dynText dynState
18 |
19 | checkedState :: Bool -> T.Text
20 | checkedState True = "Checkbox is checked"
21 | checkedState _ = "Checkbox is not checked"
--------------------------------------------------------------------------------
/src/dom04.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE RecursiveDo #-}
3 | import Reflex.Dom
4 | import qualified Data.Text as T
5 | import qualified Data.Map as Map
6 | import Data.Monoid ((<>))
7 |
8 | main :: IO ()
9 | main = mainWidget $ do
10 | rec
11 | dynBool <- toggle False evClick
12 | let dynAttrs = attrs <$> dynBool
13 | elDynAttr "h1" dynAttrs $ text "Changing color"
14 | evClick <- button "Change Color"
15 | return ()
16 |
17 | attrs :: Bool -> Map.Map T.Text T.Text
18 | attrs b = "style" =: ("color: " <> color b)
19 | where
20 | color True = "red"
21 | color _ = "green"
22 |
--------------------------------------------------------------------------------
/src/main02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import Data.Map as Map
4 |
5 | main :: IO ()
6 | main = mainWidgetWithHead headElement bodyElement
7 |
8 | headElement :: MonadWidget t m => m ()
9 | headElement = do
10 | el "title" $ text "Main Title"
11 | styleSheet "css/simple.css"
12 | where
13 | styleSheet link = elAttr "link" (Map.fromList [
14 | ("rel", "stylesheet")
15 | , ("type", "text/css")
16 | , ("href", link)
17 | ]) $ return ()
18 |
19 | bodyElement :: MonadWidget t m => m ()
20 | bodyElement = el "div" $ do
21 | el "h1" $ text "This title should be green"
22 | return ()
--------------------------------------------------------------------------------
/src/range03.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 |
4 | import Data.Map
5 | import qualified Data.Text as T
6 | import Data.Monoid ((<>))
7 |
8 | main :: IO ()
9 | main = mainWidget bodyElement
10 |
11 | bodyElement :: MonadWidget t m => m ()
12 | bodyElement = do
13 | el "h2" $ text "Range Input"
14 | rg <- rangeInput $ def & attributes .~ constDyn
15 | ("min" =: "-100" <> "max" =: "100" <> "value" =: "0" <> "step" =: "10" <> "list" =: "powers" )
16 | elAttr "datalist" ("id" =: "powers") $ do
17 | elAttr "option" ("value" =: "0") blank
18 | elAttr "option" ("value" =: "-30") blank
19 | elAttr "option" ("value" =: "50") blank
20 | el "p" blank
21 | display $ _rangeInput_value rg
22 | return ()
--------------------------------------------------------------------------------
/src/dropdown01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 | import qualified Data.Map as Map
5 | import Data.Monoid((<>))
6 | import Data.Maybe (fromJust)
7 |
8 | main :: IO ()
9 | main = mainWidget bodyElement
10 |
11 | bodyElement :: MonadWidget t m => m ()
12 | bodyElement = el "div" $ do
13 | el "h2" $ text "Dropdown"
14 | text "Select country "
15 | dd <- dropdown 2 (constDyn countries) def
16 | el "p" blank
17 | let selItem = result <$> value dd
18 | dynText selItem
19 |
20 | countries :: Map.Map Int T.Text
21 | countries = Map.fromList [(1, "France"), (2, "Switzerland"), (3, "Germany"), (4, "Italy"), (5, "USA")]
22 |
23 | result :: Int -> T.Text
24 | result key = "You selected: " <> fromJust (Map.lookup key countries)
--------------------------------------------------------------------------------
/src/colorviewer.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import Data.Map
4 | import qualified Data.Text as T
5 |
6 | main :: IO ()
7 | main = mainWidget bodyElement
8 |
9 | bodyElement :: MonadWidget t m => m ()
10 | bodyElement = do
11 | el "h2" $ text "RGB Viewer"
12 | el "div" $ text "Enter RGB component values as numbers between 0 and 255"
13 | dfsRed <- labledBox "Red: "
14 | dfsGreen <- labledBox "Green: "
15 | dfsBlue <- labledBox "Blue: "
16 | textArea $
17 | def & attributes .~ (styleMap <$> value dfsRed <*> value dfsGreen <*> value dfsBlue)
18 | return ()
19 |
20 | labledBox :: MonadWidget t m => T.Text -> m (TextInput t)
21 | labledBox lbl = el "div" $ do
22 | text lbl
23 | textInput $ def & textInputConfig_inputType .~ "number"
24 | & textInputConfig_initialValue .~ "0"
25 |
26 | styleMap :: T.Text -> T.Text -> T.Text -> Map T.Text T.Text
27 | styleMap r g b = "style" =: mconcat ["background-color: rgb(", r, ", ", g, ", ", b, ")"]
--------------------------------------------------------------------------------
/src/xhr01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import qualified Data.Text as T
4 | import qualified Data.Map as Map
5 | import Data.Maybe (fromMaybe)
6 | import Data.Monoid ((<>))
7 |
8 | main :: IO ()
9 | main = mainWidget body
10 |
11 | body :: MonadWidget t m => m ()
12 | body = el "div" $ do
13 | el "h2" $ text "Swiss Meteo Data (raw version)"
14 | text "Choose station: "
15 | dd <- dropdown "BER" (constDyn stations) def
16 | -- Build and send the request
17 | evStart <- getPostBuild
18 | let evCode = tagPromptlyDyn (value dd) $ leftmost [ () <$ _dropdown_change dd, evStart]
19 | evRsp <- performRequestAsync $ buildReq <$> evCode
20 | -- Display the whole response
21 | el "h5" $ text "Response Text:"
22 | let evResult = (fromMaybe "" . _xhrResponse_responseText) <$> evRsp
23 | dynText =<< holdDyn "" evResult
24 | return ()
25 |
26 | buildReq :: T.Text -> XhrRequest ()
27 | buildReq code = XhrRequest "GET" ("https://opendata.netcetera.com/smn/smn/" <> code) def
28 |
29 | stations :: Map.Map T.Text T.Text
30 | stations = Map.fromList [("BIN", "Binn"), ("BER", "Bern"), ("KLO", "Zurich airport"), ("ZER", "Zermatt"), ("JUN", "Jungfraujoch")]
--------------------------------------------------------------------------------
/reflex-dom-inbits.cabal:
--------------------------------------------------------------------------------
1 | -- Initial reflex-dom-inbits.cabal generated by cabal init. For further
2 | -- documentation, see http://haskell.org/cabal/users-guide/
3 |
4 | name: reflex-dom-inbits
5 | version: 0.1.0.0
6 | synopsis: Reflex.Dom examples in short bits
7 | -- description:
8 | homepage: https://www.github.com/hansroland/reflex-dom-inbits
9 | license: BSD3
10 | license-file: LICENSE
11 | author: roland
12 | maintainer: rsx@bluewin.ch
13 | -- copyright:
14 | -- category:
15 | build-type: Simple
16 | extra-source-files: ChangeLog.md, README.md
17 | cabal-version: >=1.10
18 |
19 | library
20 | -- exposed-modules:
21 | -- other-modules:
22 | -- other-extensions:
23 | build-depends: base >=4.8 && <4.11
24 | , text
25 | , time
26 | , containers
27 | , file-embed
28 | , mtl
29 | , reflex
30 | , reflex-dom
31 | , reflex-dom-contrib
32 | , opench-meteo >= 0.2.0.0
33 | hs-source-dirs: src
34 | default-language: Haskell2010
35 |
--------------------------------------------------------------------------------
/src/button01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE ScopedTypeVariables #-}
3 | import Reflex.Dom
4 | import qualified Data.Text as T
5 | import Data.Monoid
6 |
7 | main :: IO ()
8 | main = mainWidget bodyElement
9 |
10 | bodyElement :: MonadWidget t m => m ()
11 | bodyElement = el "div" $ do
12 | el "h2" $ text "Button enabled / disabled"
13 | cb <- el "label" $ do
14 | cb1 <- checkbox True def
15 | text "Enable or Disable the button"
16 | return cb1
17 | el "p" blank
18 | counter :: Dynamic t Int <- count =<< disaButton (_checkbox_value cb) "Click me"
19 | el "p" blank
20 | display counter
21 |
22 | -- | A button that can be enabled and disabled
23 | disaButton :: MonadWidget t m
24 | => Dynamic t Bool -- ^ enable or disable button
25 | -> T.Text -- ^ Label
26 | -> m (Event t ())
27 | disaButton enabled label = do
28 | let attrs = ffor enabled $ \e -> monoidGuard (not e) $ "disabled" =: "disabled"
29 | (btn, _) <- elDynAttr' "button" attrs $ text label
30 | pure $ domEvent Click btn
31 |
32 | -- | A little helper function for monoid data types:
33 | -- If the boolean is True, return the first parameter, else return the null element of the monoid
34 | monoidGuard :: Monoid a => Bool -> a -> a
35 | monoidGuard p a = if p then a else mempty
36 |
37 |
--------------------------------------------------------------------------------
/src/textinput02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | import Reflex.Dom
3 | import Data.Monoid ((<>))
4 |
5 | main :: IO ()
6 | main = mainWidget bodyElement
7 |
8 | bodyElement :: MonadWidget t m => m ()
9 | bodyElement = do
10 | el "h2" $ text "Text Input - Configuration"
11 |
12 | el "h4" $ text "Max Length 14"
13 | t1 <- textInput $ def & attributes .~ constDyn ("maxlength" =: "14")
14 | dynText $ _textInput_value t1
15 |
16 | el "h4" $ text "Initial Value"
17 | t2 <- textInput $ def & textInputConfig_initialValue .~ "input"
18 | dynText $ _textInput_value t2
19 |
20 | el "h4" $ text "Input Hint"
21 | t3 <- textInput $
22 | def & attributes .~ constDyn("placeholder" =: "type something")
23 | dynText $ _textInput_value t3
24 |
25 | el "h4" $ text "Password"
26 | t4 <- textInput $ def & textInputConfig_inputType .~ "password"
27 | dynText $ _textInput_value t4
28 |
29 | el "h4" $ text "Multiple Attributes: Hint + Max Length"
30 | t5 <- textInput $ def & attributes .~ constDyn ("placeholder" =: "Max 6 chars" <> "maxlength" =: "6")
31 | dynText $ _textInput_value t5
32 |
33 | el "h4" $ text "Numeric Field with initial value"
34 | t6 <- textInput $ def & textInputConfig_inputType .~ "number"
35 | & textInputConfig_initialValue .~ "0"
36 | dynText $ _textInput_value t6
37 |
38 | return ()
--------------------------------------------------------------------------------
/src/radio01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE RecursiveDo #-}
3 | {-# LANGUAGE ScopedTypeVariables #-}
4 | import Reflex.Dom
5 | import Reflex.Dom.Contrib.Widgets.Common
6 | import Reflex.Dom.Contrib.Widgets.ButtonGroup
7 | import qualified Data.Text as T
8 |
9 | main :: IO ()
10 | main = mainWidget bodyElement
11 |
12 | bodyElement :: MonadWidget t m => m ()
13 | bodyElement = do
14 | el "h2" $ text "Radio Buttons from the Contrib Library"
15 | rec
16 | rbs :: HtmlWidget t (Maybe Selection) <-
17 | radioGroup
18 | (constDyn "size")
19 | (constDyn [(Small, "small"), (Medium, "Medium"), (Large, "LARGE")])
20 | WidgetConfig { _widgetConfig_initialValue = Nothing
21 | , _widgetConfig_setValue = never
22 | , _widgetConfig_attributes = constDyn mempty}
23 | text "Result: "
24 | display (translate <$> _hwidget_value rbs)
25 | return ()
26 |
27 | -- | A data type for the different choices
28 | data Selection = Small | Medium | Large
29 | deriving Eq
30 |
31 | -- | Helper function to translate a Selection to an Text value containing a number
32 | translate :: Maybe Selection -> T.Text
33 | translate Nothing = "0"
34 | translate (Just Small) = "10"
35 | translate (Just Medium) = "50"
36 | translate (Just Large) = "800"
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | # stack yaml starter file for reflex projects with ghc
2 |
3 | # For lts versions and sha values see:
4 | # https://github.com/commercialhaskell/stack/blob/master/doc/ghcjs.md
5 |
6 | resolver: lts-7.3
7 | compiler: ghcjs-0.2.1.9007003_ghc-8.0.1
8 |
9 | setup-info:
10 | ghcjs:
11 | source:
12 | ghcjs-0.2.1.9007003_ghc-8.0.1:
13 | url: http://ghcjs.tolysz.org/ghc-8.0-2016-10-11-lts-7.3-9007003.tar.gz
14 | sha1: 3196fd5eaed670416083cf3678396d02c50096de
15 |
16 | packages:
17 | - '.'
18 | - location:
19 | git: https://github.com/ghcjs/ghcjs-base.git
20 | # commit: dd7034ef8582ea8a175a71a988393a9d1ee86d6f
21 | commit: 9d7f01bd3be3a4f044a8716f0a1538dc00b63e6d
22 | extra-dep: true
23 | - location:
24 | git: https://github.com/reflex-frp/reflex
25 | commit: 91299fce0bb2caddfba35af6608df57dd31e3690
26 | extra-dep: true
27 | - location:
28 | git: https://github.com/reflex-frp/reflex-dom
29 | commit: 66b6d35773fcb337ab38ebce02c4b23baeae721e
30 | extra-dep: true
31 | - location:
32 | git: https://github.com/reflex-frp/reflex-dom-contrib
33 | commit: df4138406a5489acd72cf6c9e88988f13da02b31 # b4ac9378318ee71a109faef5860cccd9231bc7bd
34 | extra-dep: true
35 |
36 | extra-deps:
37 | - tz-0.1.2.0
38 | - dlist-0.7.1.2
39 | - ghcjs-dom-0.2.4.0
40 | - ref-tf-0.4.0.1
41 | - prim-uniq-0.1.0.1
42 | - zenc-0.1.1
43 | - opench-meteo-0.2.0.0
--------------------------------------------------------------------------------
/src/radio02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE RecursiveDo #-}
3 | import Reflex.Dom
4 | import qualified Data.Text as T
5 | import qualified Data.Map as Map
6 | import Data.Monoid((<>))
7 |
8 | main :: IO ()
9 | main = mainWidget bodyElement
10 |
11 | bodyElement :: MonadWidget t m => m ()
12 | bodyElement = el "div" $ do
13 | rec
14 | el "h2" $ text "Own Radio buttons"
15 | let group = "g"
16 | let dynAttrs = styleMap <$> dynColor
17 | evRad1 <- radioBtn "orange" group Orange dynAttrs
18 | evRad2 <- radioBtn "green" group Green dynAttrs
19 | evRad3 <- radioBtn "red" group Red dynAttrs
20 | let evRadio = (T.pack . show) <$> leftmost [evRad1, evRad2, evRad3]
21 | dynColor <- holdDyn "lightgrey" evRadio
22 | return ()
23 |
24 | data Color = White | Red | Orange | Green
25 | deriving (Eq, Ord, Show)
26 |
27 | -- | Helper function to create a radio button
28 | radioBtn :: (Eq a, Show a, MonadWidget t m) => T.Text -> T.Text -> a -> Dynamic t ( Map.Map T.Text T.Text)-> m (Event t a)
29 | radioBtn label group rid dynAttrs = do
30 | el "br" blank
31 | ev <- elDynAttr "label" dynAttrs $ do
32 | (rb1, _) <- elAttr' "input" ("name" =: group <> "type" =: "radio" <> "value" =: T.pack (show rid)) blank
33 | text label
34 | return $ domEvent Click rb1
35 | return $ rid <$ ev
36 |
37 | styleMap :: T.Text -> Map.Map T.Text T.Text
38 | styleMap c = "style" =: ("background-color: " <> c)
--------------------------------------------------------------------------------
/src/trace01.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE RecursiveDo #-}
3 | import Reflex.Dom
4 | import qualified Data.Text as T
5 | import qualified Data.Map as Map
6 | import Data.Monoid((<>))
7 |
8 | main :: IO ()
9 | main = mainWidget bodyElement
10 |
11 | bodyElement :: MonadWidget t m => m ()
12 | bodyElement = el "div" $ do
13 | rec
14 | el "h2" $ text "Some Tracing"
15 | let group = "g"
16 | let dynAttrs = styleMap <$> dynColor
17 | evRad1 <- radioBtn "orange" group Orange dynAttrs
18 | evRad2 <- radioBtn "green" group Green dynAttrs
19 | evRad3 <- radioBtn "red" group Red dynAttrs
20 | let evRadio = (T.pack . show) <$> leftmost [evRad1, evRad2, evRad3]
21 | let evRadioT = traceEvent ("Clicked rb in group " <> T.unpack group) evRadio
22 | dynColor <- holdDyn "lightgrey" evRadioT
23 | return ()
24 |
25 | data Color = White | Red | Orange | Green
26 | deriving (Eq, Ord, Show)
27 |
28 | -- | Helper function to create a radio button
29 | radioBtn :: (Eq a, Show a, MonadWidget t m) => T.Text -> T.Text -> a -> Dynamic t ( Map.Map T.Text T.Text)-> m (Event t a)
30 | radioBtn label group rid dynAttrs = do
31 | el "br" blank
32 | ev <- elDynAttr "label" dynAttrs $ do
33 | (rb1, _) <- elAttr' "input" ("name" =: group <> "type" =: "radio" <> "value" =: T.pack (show rid)) blank
34 | text label
35 | return $ domEvent Click rb1
36 | return $ rid <$ ev
37 |
38 | styleMap :: T.Text -> Map.Map T.Text T.Text
39 | styleMap c = "style" =: ("background-color: " <> c)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017-2018, Hans Roland Senn
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials provided
14 | with the distribution.
15 |
16 | * Neither the name of Hans Roland Senn nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reflex-dom-inbits - A beginner friendly step by step tutorial for reflex-dom
2 |
3 | [![License BSD3][badge-license]][license]
4 |
5 | [badge-license]: https://img.shields.io/badge/license-BSD3-green.svg?dummy
6 | [license]: https://github.com/hansroland/reflex-dom-inbits/blob/master/LICENSE
7 |
8 |
9 | This is a beginner-friendly tutorial for *reflex-dom*. It shows how to write Haskell programs with a graphical user interface.
10 | It contains an introductionary text and examples in short bits.
11 |
12 | Reflex-Dom is a library to create Haskell applications with a Graphical User Interface (GUI).
13 | The GUI is based on the popular Document Object Model (DOM) that is used in the Internet Browsers.
14 | Therefore Reflex-Dom programs can be run in the Web Browser or as a Webkit-Gtk application.
15 |
16 | *Reflex-dom* is based on *Reflex*, a Haskell implementation of Functional Reactive Programming.
17 |
18 | It's not necessary to be a Haskell guru to follow this tutorial.
19 | A basic understanding of Haskell and the concepts of *Functor*, *Applicative* and *Monad* is enough.
20 | With Reflex-Dom you can write GUI applications in Haskell without understanding the concepts of
21 | *State Monad* or *Monad Transformers*. You need also a basic background on *HTML* and
22 | on *Cascaded Style Sheets* (*CSS*). Of course, the more experience you have, the easier it is.
23 |
24 | Start by cloning this repository with ``` git clone https://github.com/hansroland/reflex-dom-inbits ```.
25 | Continue by installing Reflex.Dom. The preferred installation method is to use
26 | the reflex-platform from [https://github.com/reflex-frp/reflex-platform](https://github.com/reflex-frp/reflex-platform).
27 | Alternatively you can use stack, however, this will take a long time. If you use stack, I recommend
28 | to use version 1.24.0.2 of cabal. I was unable to run _stack setup_ with cabal 2.0.0.0.
29 |
30 | Then read the file [tutorial.md](tutorial.md).
--------------------------------------------------------------------------------
/support/reflex-chart.hs:
--------------------------------------------------------------------------------
1 | -- ----------------------------------------------------------------------
2 | -- Create the diagrams to explain Events, Behaviors and Dynamics
3 | -- ----------------------------------------------------------------------
4 | --
5 | -- run from main directory with runghc support/reflex-chart.hs
6 | --
7 | -- -----------------------------------------------------------------------
8 | import Graphics.Rendering.Chart hiding (x0, y0)
9 | import Graphics.Rendering.Chart.Backend.Cairo
10 | import Data.Colour
11 | import Data.Colour.Names
12 | import Data.Default.Class
13 | import Control.Lens
14 |
15 | chartEvent = toRenderable layout
16 | where
17 | eventPlot = plot_points_style .~ filledCircles 10 (opaque orange)
18 | $ plot_points_values .~ events
19 | -- $ plot_points_title .~ "events"
20 | $ def
21 |
22 | layout = layoutXAxis
23 | $ layoutYAxis
24 | $ layout_plots .~ [ toPlot eventPlot]
25 | $ def
26 |
27 | chartBehavior = toRenderable layout
28 | where
29 | behaviorPlot = plot_lines_values .~ behaviors
30 | $ plot_lines_style .~ solidLine 2.0 (opaque red)
31 | $ def
32 | layout = layoutXAxis
33 | $ layoutYAxis
34 | $ layout_plots .~ [ toPlot behaviorPlot]
35 | $ def
36 |
37 | chartDynamic = toRenderable layout
38 | where
39 | behaviorPlot = plot_lines_values .~ behaviors
40 | $ plot_lines_style .~ solidLine 2.0 (opaque red)
41 | $ def
42 | eventPlot = plot_points_style .~ filledCircles 10 (opaque orange)
43 | $ plot_points_values .~ events
44 | -- $ plot_points_title .~ "events"
45 | $ def
46 |
47 | layout = layoutXAxis
48 | $ layoutYAxis
49 | $ layout_plots .~ [ toPlot eventPlot, toPlot behaviorPlot]
50 | $ def
51 |
52 |
53 |
54 | layoutXAxis :: Layout Double Double -> Layout Double Double
55 | layoutXAxis = layout_x_axis . laxis_generate .~ const AxisData {
56 | _axis_visibility = def,
57 | _axis_viewport = vmap (minx,maxx),
58 | _axis_tropweiv = invmap (minx,maxx),
59 | _axis_ticks = [],
60 | _axis_grid = xValues,
61 | _axis_labels = [[(x1,"Event"), (x2, "Event"), (x3, "Event"), (x4,"Event")], [(x5, "time ->")]]
62 | }
63 |
64 | layoutYAxis :: Layout x0 Double -> Layout x0 Double
65 | layoutYAxis = layout_y_axis . laxis_generate .~ const AxisData {
66 | _axis_visibility = def,
67 | _axis_viewport = vmap (miny,maxy),
68 | _axis_tropweiv = invmap (miny,maxy),
69 | _axis_ticks = [],
70 | _axis_grid = [],
71 | _axis_labels = [[(y4, "Value a")]]
72 | }
73 |
74 |
75 |
76 |
77 | main :: IO ()
78 | main = do
79 | renderableToFile fileoptions "images/event.png" chartEvent
80 | renderableToFile fileoptions "images/behavior.png" chartBehavior
81 | renderableToFile fileoptions "images/dynamic.png" chartDynamic
82 | return ()
83 |
84 | miny,maxy :: Double
85 | (miny,maxy) = (-0.9, 3.6)
86 |
87 | minx,maxx :: Double
88 | (minx,maxx) = (-0.9, 7.0)
89 |
90 | fileoptions = FileOptions {
91 | _fo_size = (440, 220),
92 | _fo_format = PNG
93 | }
94 |
95 | -- --------------------------------------------------------
96 | -- Data
97 | -- --------------------------------------------------------
98 | x1 = 0
99 | x2 = 2
100 | x3 = 4
101 | x4 = 5
102 | x5 = 7
103 |
104 | y1 = 0
105 | y2 = 2
106 | y3 = 1
107 | y4 = 3
108 |
109 | xValues = [x1, x2, x3, x4]
110 |
111 |
112 | events :: [(Double, Double)]
113 | events = [(x1,y1), (x2,y2), (x3,y3), (x4, y4)]
114 |
115 | ends :: [(Double, Double)]
116 | ends = [(x2,y1), (x3, y2), (x4, y3), (x5, y4)]
117 |
118 | behaviors = [[(x1,y1), (x2,y1)], [(x2, y2), (x3, y2)], [(x3,y3), (x4,y3)], [(x4,y4), (x5,y4)]]
119 |
--------------------------------------------------------------------------------
/src/xhr02.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE ScopedTypeVariables #-}
3 | {-# LANGUAGE TemplateHaskell #-}
4 | import Reflex.Dom
5 | import qualified Data.Text as T
6 | import qualified Data.Map as Map
7 | import Data.Maybe (fromMaybe)
8 | import Data.Monoid ((<>))
9 | import Data.FileEmbed
10 | import Data.Meteo.Swiss
11 |
12 | main :: IO ()
13 | main = mainWidgetWithCss css body
14 | where css = $(embedFile "css/tab.css")
15 |
16 | -- | Enumeration to track type of page to display
17 | data Page = PageData | PageError
18 | deriving Eq
19 |
20 | -- | Create the HTML body
21 | body :: MonadWidget t m => m ()
22 | body = el "div" $ do
23 | el "h2" $ text "Swiss Weather Data (Tab display)"
24 | text "Choose station: "
25 | dd <- dropdown "BER" (constDyn stations) def
26 | el "p" blank
27 | -- Build and send the request
28 | evStart <- getPostBuild
29 | let evCode = tagPromptlyDyn (value dd) $ leftmost [ () <$ _dropdown_change dd, evStart]
30 | evRsp <- performRequestAsync $ buildReq <$> evCode
31 | -- Check on HTML response code and remember state.
32 | let (evOk, evErr) = checkXhrRsp evRsp
33 | dynPage <- foldDyn ($) PageData $ leftmost [const PageData <$ evOk, const PageError <$ evErr]
34 | -- Create the 2 pages
35 | pageData evOk dynPage
36 | pageErr evErr dynPage
37 | return ()
38 |
39 | -- | Display the meteo data in a tabbed display
40 | pageData :: MonadWidget t m => Event t XhrResponse -> Dynamic t Page -> m ()
41 | pageData evOk dynPage = do
42 | evSmnRec :: (Event t SmnRecord) <- return $ fmapMaybe decodeXhrResponse evOk
43 | let evSmnStat = fmapMaybe smnStation evSmnRec
44 | let dynAttr = visible <$> dynPage <*> pure PageData
45 | elDynAttr "div" dynAttr $
46 | tabDisplay "tab" "tabact" $ tabMap evSmnRec evSmnStat
47 |
48 | -- | Display the error page
49 | pageErr :: MonadWidget t m => Event t XhrResponse -> Dynamic t Page -> m ()
50 | pageErr evErr dynPage = do
51 | let dynAttr = visible <$> dynPage <*> pure PageError
52 | elDynAttr "div" dynAttr $ do
53 | el "h3" $ text "Error"
54 | dynText =<< holdDyn "" (_xhrResponse_statusText <$> evErr)
55 |
56 | -- | Split up good and bad response events
57 | checkXhrRsp :: FunctorMaybe f => f XhrResponse -> (f XhrResponse, f XhrResponse)
58 | checkXhrRsp evRsp = (evOk, evErr)
59 | where
60 | evOk = ffilter (\rsp -> _xhrResponse_status rsp == 200) evRsp
61 | evErr = ffilter (\rsp -> _xhrResponse_status rsp /= 200) evRsp
62 |
63 | -- | Helper function to create a dynamic attribute map for the visibility of an element
64 | visible :: Eq p => p -> p -> Map.Map T.Text T.Text
65 | visible p1 p2 = "style" =: ("display: " <> choose (p1 == p2) "inline" "none")
66 | where
67 | choose True t _ = t
68 | choose False _ f = f
69 |
70 | buildReq :: T.Text -> XhrRequest ()
71 | buildReq code = XhrRequest "GET" (urlDataStat code) def
72 |
73 | stations :: Map.Map T.Text T.Text
74 | stations = Map.fromList [("BIN", "Binn"), ("BER", "Bern"), ("KLO", "Zurich airport"), ("ZER", "Zermatt"), ("JUN", "Jungfraujoch")]
75 |
76 | -- | Create a tabbed display
77 | tabMap :: MonadWidget t m => Event t SmnRecord -> Event t SmnStation -> Map.Map Int (T.Text, m ())
78 | tabMap evMeteo evStat = Map.fromList[ (1, ("Station", tabStat evStat)),
79 | (2, ("MeteoData", tabMeteo evMeteo))]
80 |
81 | -- | Create the DOM elements for the Station tab
82 | tabStat :: MonadWidget t m => Event t SmnStation -> m ()
83 | tabStat evStat = do
84 | dispStatField "Code" staCode evStat
85 | dispStatField "Name" staName evStat
86 | dispStatField "Y-Coord" (tShow . staCh1903Y) evStat
87 | dispStatField "X-Coord" (tShow . staCh1903X) evStat
88 | dispStatField "Elevation" (tShow . staElevation) evStat
89 | return ()
90 |
91 | -- | Create the DOM elements for the Meteo data tab
92 | tabMeteo :: MonadWidget t m => Event t SmnRecord -> m ()
93 | tabMeteo evMeteo = do
94 | dispMeteoField "Date/Time" (tShow . smnDateTime) evMeteo
95 | dispMeteoField "Temperature" smnTemperature evMeteo
96 | dispMeteoField "Sunshine" smnSunshine evMeteo
97 | dispMeteoField "Precipitation" smnPrecipitation evMeteo
98 | dispMeteoField "Wind Direction" smnWindDirection evMeteo
99 | dispMeteoField "Wind Speed" smnWindSpeed evMeteo
100 | return ()
101 |
102 | -- | Display a single field from the SmnStation record
103 | dispStatField :: MonadWidget t m => T.Text -> (SmnStation -> T.Text) -> Event t SmnStation -> m ()
104 | dispStatField label rend evStat = do
105 | el "br" blank
106 | text $ label <> ": "
107 | dynText =<< holdDyn "" (fmap rend evStat)
108 | return ()
109 |
110 | -- | Display a single field from the SmnRecord record
111 | dispMeteoField :: MonadWidget t m => T.Text -> (SmnRecord -> T.Text) -> Event t SmnRecord -> m ()
112 | dispMeteoField label rend evRec = do
113 | el "br"blank
114 | text $ label <> ": "
115 | dynText =<< holdDyn "" (fmap rend evRec)
116 | return ()
117 |
118 | -- | Small helper function to convert showable values wrapped in Maybe to T.Text.
119 | -- You should use the test-show library from Hackage!!
120 | tShow :: Show a => Maybe a -> T.Text
121 | tShow Nothing = ""
122 | tShow (Just x) = (T.pack . show) x
--------------------------------------------------------------------------------
/src/xhr03.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 | {-# LANGUAGE ScopedTypeVariables #-}
3 | {-# LANGUAGE RecursiveDo #-}
4 | {-# LANGUAGE TemplateHaskell #-}
5 | import Reflex.Dom
6 | import qualified Data.Text as T
7 | import qualified Data.Map as Map
8 | import Data.Monoid
9 | import Data.FileEmbed
10 | import Data.Maybe (isJust, fromJust)
11 | import Data.Meteo.Swiss
12 |
13 | main :: IO ()
14 | main = mainWidgetWithCss css body
15 | where css = $(embedFile "css/tab.css")
16 |
17 | -- | Enumerate our pages
18 | data Page = Page01
19 | | Page02
20 | | Page03
21 | deriving Eq
22 |
23 | -- | Create the HTML body element
24 | body :: MonadWidget t m => m ()
25 | body = do
26 | el "h2" $ text "Swiss Weather Data - All Stations"
27 | rec dynPage <- foldDyn ($) Page01 $ leftmost [
28 | const Page01 <$ evOkAll,
29 | const Page01 <$ leftmost [evEndPg2, evEndPg3],
30 | const Page02 <$ evOk,
31 | const Page03 <$ leftmost [evErr, evErrAll] ]
32 | evStart <- getPostBuild
33 | -- Build and send the request for all stations
34 | evRspAll <- performRequestAsync $ fmap buildReqAll evStart
35 | let (evOkAll, evErrAll) = checkXhrRsp evRspAll
36 | -- Show list of all stations
37 | evStat <- page01 evOkAll dynPage
38 | -- Send request for a single station
39 | evRsp <- performRequestAsync $ buildReqStat <$> evStat
40 | let (evOk, evErr) = checkXhrRsp evRsp
41 | evSmnRec :: (Event t SmnRecord) <- return $ fmapMaybe decodeXhrResponse evOk
42 | evEndPg2 <- page02 evSmnRec dynPage
43 | evEndPg3 <- page03 (leftmost [evErr, evErrAll]) dynPage
44 | return ()
45 |
46 | -- | Display the page with a table with all stations
47 | -- Return an event with the 3-letter code of the station
48 | page01 :: MonadWidget t m => Event t XhrResponse -> Dynamic t Page -> m (Event t T.Text)
49 | page01 evRsp dynPage = do
50 | let dynAttr = visible <$> dynPage <*> pure Page01
51 | elDynAttr "div" dynAttr $ do
52 | -- Convert JSON to list of SmnRecords
53 | evListRaw :: Event t [SmnRecord] <- return $ fmapMaybe decodeXhrResponse evRsp
54 | -- We want only SmnRecords with a station name
55 | let evList = withNames <$> evListRaw
56 | -- list stations
57 | el "table" $ do
58 | dynList :: Dynamic t [SmnRecord] <- holdDyn [] evList
59 | evRowsDyn <- simpleList dynList displayStationRow
60 | return $ switchPromptlyDyn $ leftmost <$> evRowsDyn
61 |
62 | -- | Display the page with the data of a single station
63 | -- Return an event, when the user closes the page
64 | page02 :: MonadWidget t m => Event t SmnRecord -> Dynamic t Page -> m (Event t ())
65 | page02 evSmnRec dynPage = do
66 | let dynAttr = visible <$> dynPage <*> pure Page02
67 | elDynAttr "div" dynAttr $ do
68 | evBack <- button "Back"
69 | let evSmnStat = fmapMaybe smnStation evSmnRec
70 | el "div" $
71 | tabDisplay "tab" "tabact" $ tabMap evSmnRec evSmnStat
72 | return evBack
73 |
74 | -- | Display the error page
75 | -- Return an event, when the user closes the page
76 | page03 :: MonadWidget t m => Event t XhrResponse -> Dynamic t Page -> m (Event t ())
77 | page03 evRsp dynPage = do
78 | let dynAttr = visible <$> dynPage <*> pure Page03
79 | elDynAttr "div" dynAttr $ do
80 | evBack <- button "Back"
81 | el "h3" $ text "Error"
82 | dynText =<< holdDyn "" (_xhrResponse_statusText <$> evRsp)
83 | return evBack
84 |
85 | -- | Split up good and bad response events
86 | checkXhrRsp :: FunctorMaybe f => f XhrResponse -> (f XhrResponse, f XhrResponse)
87 | checkXhrRsp evRsp = (evOk, evRsp)
88 | where
89 | evOk = ffilter (\rsp -> _xhrResponse_status rsp == 200) evRsp
90 | evErr = ffilter (\rsp -> _xhrResponse_status rsp /= 200) evRsp
91 |
92 | -- | A button that sends an (Event t T.Text) with the station code as payload
93 | cmdButton :: MonadWidget t m
94 | => T.Text -- ^ Label
95 | -> Dynamic t SmnRecord
96 | -> m (Event t T.Text)
97 | cmdButton label staRec = do
98 | (btn, _) <- el' "button" $ text label
99 | let dynNam = smnCode <$> staRec
100 | return $ tagPromptlyDyn dynNam $ domEvent Click btn
101 |
102 | -- | Create the HTML element for a single HTML table row
103 | displayStationRow :: MonadWidget t m => Dynamic t SmnRecord -> m (Event t T.Text)
104 | displayStationRow dynRec = el "tr" $ do
105 | evRow <- el "td" $ cmdButton "View" dynRec
106 | el "td" $ dynText $ staName . fromJust . smnStation <$> dynRec
107 | return evRow
108 |
109 | -- | Filter only SmnRecords that really have a name
110 | withNames :: [SmnRecord] -> [SmnRecord]
111 | withNames = filter (isJust . smnStation)
112 |
113 | -- | Build the Xhr request to get data for all stations
114 | buildReqAll :: a -> XhrRequest ()
115 | buildReqAll _ = XhrRequest "GET" urlDataAll def
116 |
117 | -- | Build the Xhr request to get data for a single station
118 | buildReqStat :: T.Text -> XhrRequest ()
119 | buildReqStat code = XhrRequest "GET" (urlDataStat code) def
120 |
121 | -- | Get the station name from a SmnRecord
122 | getStation :: SmnRecord -> T.Text
123 | getStation = getStationName . smnStation
124 | where
125 | getStationName :: Maybe SmnStation -> T.Text
126 | getStationName (Just staRec) = staName staRec
127 | getStationName Nothing = "STATION IS NOTHING"
128 |
129 | -- | Helper function to create a dynamic attribute map for the visibility of an element
130 | visible :: Page -> Page -> Map.Map T.Text T.Text
131 | visible p1 p2 = "style" =: ("display: " <> choose (p1 == p2) "inline" "none")
132 | where
133 | choose True t _ = t
134 | choose False _ f = f
135 |
136 | -- ------------------------------------------------------------------------------------------------
137 | -- Code to display the data of a single station on page02
138 | -- ------------------------------------------------------------------------------------------------
139 |
140 | -- | Create a tabbed display
141 | tabMap :: MonadWidget t m => Event t SmnRecord -> Event t SmnStation -> Map.Map Int (T.Text, m ())
142 | tabMap evMeteo evStat = Map.fromList[ (1, ("Station", tabStat evStat)),
143 | (2, ("MeteoData", tabMeteo evMeteo))]
144 |
145 | -- | Create the DOM elements for the Station tab
146 | tabStat :: MonadWidget t m => Event t SmnStation -> m ()
147 | tabStat evStat = do
148 | dispStatField "Name" staName evStat
149 | dispStatField "Code" staCode evStat
150 | dispStatField "Y-Coord" (tShow . staCh1903Y) evStat
151 | dispStatField "X-Coord" (tShow . staCh1903X) evStat
152 | dispStatField "Elevation" (tShow . staElevation) evStat
153 | return ()
154 |
155 | -- | Create the DOM elements for the Meteo data tab
156 | tabMeteo :: MonadWidget t m => Event t SmnRecord -> m ()
157 | tabMeteo evMeteo = do
158 | dispMeteoField "Date/Time" (tShow . smnDateTime) evMeteo
159 | dispMeteoField "Temperature" smnTemperature evMeteo
160 | dispMeteoField "Sunshine" smnSunshine evMeteo
161 | dispMeteoField "Precipitation" smnPrecipitation evMeteo
162 | dispMeteoField "Wind Direction" smnWindDirection evMeteo
163 | dispMeteoField "Wind Speed" smnWindSpeed evMeteo
164 | return ()
165 |
166 | -- Display a single field from the SmnStation record
167 | dispStatField :: MonadWidget t m => T.Text -> (SmnStation -> T.Text) -> Event t SmnStation -> m ()
168 | dispStatField label rend evStat = do
169 | el "br" blank
170 | text $ label <> ": "
171 | dynText =<< holdDyn "" (fmap rend evStat)
172 | return ()
173 |
174 | -- Display a single field from the SmnRecord record
175 | dispMeteoField :: MonadWidget t m => T.Text -> (SmnRecord -> T.Text) -> Event t SmnRecord -> m ()
176 | dispMeteoField label rend evRec = do
177 | el "br"blank
178 | text $ label <> ": "
179 | dynText =<< holdDyn "" (fmap rend evRec)
180 | return ()
181 |
182 | -- | Small helper function to convert showable values wrapped in Maybe to T.Text.
183 | -- You should use the test-show library from Hackage!!
184 | tShow :: Show a => Maybe a -> T.Text
185 | tShow Nothing = ""
186 | tShow (Just x) = (T.pack . show) x
--------------------------------------------------------------------------------
/tutorial.md:
--------------------------------------------------------------------------------
1 | # A Beginner-friendly Step by Step Tutorial for Reflex-Dom
2 |
3 | This is a beginner-friendly tutorial. It shows how to write Haskell programs with a graphical user interface using reflex-dom.
4 |
5 | Today most computer programs have a graphical user interface (GUI).
6 | However Haskell programs with a GUI are still rare. Haskell programs normally use a command line interface (CLI).
7 | For a long time, there were no good options to write GUI programs in Haskell.
8 | It is difficult to match the event driven nature of a GUI program onto the functional paradigm.
9 | The traditional object oriented way to program a GUI application uses callbacks.
10 | These callbacks need a lot of global state and in Haskell managing state is not easy.
11 |
12 | To solve these problems the Haskell community developed a lot of new new ideas.
13 | One of them is called *Functional Reactive Programming*, *FRP* for short.
14 | Conal Elliott and Paul Hudak first developed the basic ideas and published them in 1997 in the paper [Functional Reactive Animation](http://conal.net/papers/icfp97/).
15 | On Hackage you can find a lot
16 | of different FRP libraries, eg *elera*, *frpnow*, *grapefruit-frp*, *netwire*, *reactive-banana*, *reflex* and many more.
17 |
18 | In this tutorial we use *reflex* and *reflex-dom*. Reflex is a FRP implementation written by Ryan Trinkle from [Obsidian](https://obsidian.systems/).
19 | Reflex is a strong foundation to handle events and values that change over time.
20 | Reflex-Dom is built on Reflex and on GHCJS.Dom. It allows you to write GUI programs that run in a
21 | Web Browser or as a 'native' application in WebkitGtk. Reflex-Dom was written by Ryan Trinkle too.
22 |
23 | Reflex-dom protects you from all the low level details of an FRP implementation. Writing GUI programs in reflex-dom is much fun.
24 | You can really write GUI programs in a functional way and you can separate the GUI logic from the business logic.
25 | It's not necessary to be a Haskell guru to write programs with reflex-dom.
26 | A good understanding of basic Haskell with the concepts of *Functor*, *Applicative* and *Monad* is enough.
27 | Of course, the more experience you have, the easier it is.
28 |
29 | # Basics of Functional Reactive Programming (FRP) and Reflex
30 |
31 | ## The Basic Ideas of Functional Reactive Programming
32 |
33 | Normally input functions are impure. Assume a function *getChar* that reads a single character from the keyboard.
34 | For different calls the function *getChar* normally returns a different character, depending on the key that was pressed on the keyboad .
35 | Therefore such a function is not a pure Haskell function. As everybody knows Haskell uses monadic IO actions to avoid impure functions.
36 | Functional Reactive Programming (FRP) takes an other approach. All potentially impure functions have a time parameter
37 | and the FRP system makes sure, that every call to such a function is done with a new and unique time value.
38 |
39 | In pseudo code:
40 |
41 | ~~~ { .haskell }
42 | getChar :: Time -> Char
43 |
44 | getChar 1 -- this returns eg a 'F'
45 | getChar 2 -- this returns eg a 'R'
46 | ~~~
47 |
48 | Everytime you call *getChar* with a parameter of 1, it will return the character 'F'.
49 | However the FRP framework will not allow you to do multiple calls to *getChar* with the parameter 1.
50 |
51 | With this trick, *getChar* is now a pure function.
52 |
53 | In the type declaration of a reflex function the time parameter is always shown explicitely.
54 | Normally a type parameter with the name *t* is used.
55 | However it's **never** necessary to supply this parameter as a programmer when you call the function.
56 |
57 | Reflex has 3 main time-dependent data types:
58 |
59 | * Event
60 | * Behavior
61 | * Dynamic
62 |
63 | A typical type declaration for a function could be:
64 |
65 | ```dispEvent :: MonadWidget t m => T.Text -> Event t ClickInfo -> m ()```
66 |
67 |
68 | Here the *t* is the time parameter. This parameter is always introduced with a precondition. In our case
69 | ``` MonadWidget t m => ```. The *m* is normally some monad. *ClickInfo* would be a user defined Haskell data type.
70 |
71 | To call the above function, in some monadic context, you would write:
72 |
73 | ```dispEvent "This is my event" evClick```
74 |
75 |
76 | Events and Behaviors are common data types in different FRP implementations. Dynamics, however, are probably unique
77 | in Reflex.
78 |
79 | ## Event
80 |
81 | Events occur at some points in time and they carry a value.
82 | The most prominent example for events are mouse clicks and pressing keys on a keyboard.
83 | The value of a keyboard event is normally the code of the pressed key.
84 |
85 | During any given time frame, an *Event* is either occurring or not occurring; if it is occurring, it will contain a value of
86 | the given type. This is shown in the following diagram:
87 |
88 | 
89 |
90 |
91 | In Reflex the data type *Event* has the following simplified type:
92 |
93 | ```data Event t a```
94 |
95 | '*a*' is the type of the event. I also call the value '*a*' the payload of the event. It can be more or less every Haskell data type.
96 | Sometimes we will even use functions as event payloads.
97 |
98 | Events are the main work horses in Reflex. As we will see, it is very common to transform an event of type *a*
99 | into an event of type *b*.
100 |
101 | The data type *Event* is an instance of the Haskell *Functor* type class.
102 | This allows easy event transformation with the well known *fmap* function:
103 |
104 | ```fmap :: (a -> b) -> Event a -> Event b```
105 |
106 | Later we will see other functions to transform events.
107 |
108 |
109 | ## Behavior
110 |
111 | A *Behavior* is a container for a value, that changes over time. Other than events, Behaviors always have a value.
112 | It is not possible to be notified when a Behavior changes.
113 |
114 | In Reflex the data type *Behavior* has the following simplified type:
115 |
116 | ```data Behavior t a```
117 |
118 |
119 | Behaviors can change their values only at time points where events occur. This is shown in the following diagram.
120 |
121 | 
122 |
123 | To write a Reflex-dom application we rarely use Behaviors. We use *Dynamics* if we need values that change over time.
124 |
125 | ## Dynamic
126 |
127 | A *Dynamic* is a container for a value that can change over time and allows notifications on changes.
128 | Dynamics are special to Reflex. They are a combination of the types *Behavior* and *Event*.
129 |
130 | ```data Dynamic t a```
131 |
132 |
133 | 
134 |
135 | The data type *Dynamic* is an instance of the Haskell *Applicative* type class.
136 | It is very common to use applicative syntax when working with Dynamics.
137 |
138 | # Before we really start coding...
139 |
140 | ## Used Library Versions
141 |
142 | In this tutorial, we will use reflex-dom-0.4 and reflex-0.5 both of which are available on Hackage.
143 | Unfortunately most of the examples will not compile with earlier versions of reflex-dom!
144 |
145 | If you used the *reflex-platform* to install reflex and reflex-dom, you will have the newer versions.
146 |
147 |
148 | ## Popular Language Extensions
149 |
150 | To write Reflex programs, very often we use some of the following GHC Haskell language extensions:
151 |
152 | ```{-# LANGUAGE OverloadedStrings #-}```
153 |
154 | We need it, because *Reflex* uses *Text* instead of *String*. The extension *OverloadedStrings* allows automatic conversion of string constants like "I'm a String"
155 | to the correct string type. We don't need to pack and unpack the string constants ourselfs.
156 |
157 | ```{-# LANGUAGE RecursiveDo #-}```
158 |
159 | Sometimes we need to access a value or an event from a DOM element before it is defined. *RecursiveDo* makes this easy, by extending the `do`-notation syntax sugar.
160 |
161 | ```{-# LANGUAGE ScopedTypeVariables #-}```
162 |
163 | Sometimes the compiler is unable to infer the type of a bound variable in a do-block.
164 | Or sometimes we want to document the type of such a variable. This makes it easier to understand the code.
165 | With the extension *ScopedTypeVariables* GHC accepts such type annotations.
166 |
167 | In the following example we specify the type of the value *name*.
168 |
169 | ~~~ { .haskell }
170 | {-# LANGUAGE ScopedTypeVariables #-}
171 | main :: IO ()
172 | main = do
173 | putStrLn "What's your name"
174 | name :: String <- getLine
175 | putStrLn $ "Hello " ++ name
176 | ~~~
177 |
178 | ## Popular Imports
179 |
180 | From all the 1001 libraries stored on Hackage, we will use only very few:
181 |
182 | ```import Reflex.Dom```
183 |
184 | Ok this tutorial is about Reflex.Dom so we should import and use it.
185 | It's not necessary to import Reflex. Reflex.Dom re-exports all the needed functions of Reflex.
186 |
187 | ```import qualified Data.Text as T```
188 |
189 | As mentioned above, reflex-dom uses the data type *Text* instead of *String*.
190 | So we have to import it!
191 |
192 | ```import qualified Data.Map as Map```
193 |
194 | Haskell maps are very popular in *Reflex-dom*. They are used in a lot of functions.
195 |
196 | ```import Data.Monoid```
197 |
198 | We normally use the function *mempty* to create an empty map,
199 | the function (=:) to create a singelton map
200 | and the function *mappend* rsp *(<>)* to combine two maps.
201 |
202 | Rarely we will use other libraries, eg
203 |
204 | * import Data.FileEmbed - library *file-embed* on Hackage and installed by reflex-platform.
205 | * import Data.Text.Encoding - will be installed with reflex-dom
206 | * import Data.Maybe - part of base
207 | * import Data.Time - part of base
208 | * import Reflex.Dom.Contrib.Widgets - from [https://github.com/reflex-frp/reflex-dom-contrib](https://github.com/reflex-frp/reflex-dom-contrib)
209 | * import Control.Monad.Trans - part of base
210 | * import Data.Meteo.Swiss - from [https://github.com/hansroland/opench/tree/master/meteo](https://github.com/hansroland/opench/tree/master/meteo)
211 |
212 |
213 | ## Some comments to the code examples
214 |
215 | A lot of the following examples could be written with less lines, just by using the monadic functions (>>=),
216 | (>>), (=<<), (<<) etc.
217 |
218 | Sometimes I use more lines in my code in order to have code that is easier to understand by beginners.
219 |
220 | # A First Simple Reflex-Dom Example
221 |
222 | Let's begin with a first simple reflex-dom example. The code is in the file *src/count01.hs*:
223 |
224 | ~~~ { .haskell }
225 | {-# LANGUAGE OverloadedStrings #-}
226 | import Reflex.Dom
227 |
228 | main :: IO ()
229 | main = mainWidget $ display =<< count =<< button "ClickMe"
230 | ~~~
231 |
232 | This example uses four reflex or reflex-dom functions:
233 |
234 | * mainWidget
235 | * display
236 | * count
237 | * button
238 |
239 | Let's look at the types of these functions and what they do:
240 |
241 | ## Function: *mainWidget*
242 |
243 | ```mainWidget :: (forall x. Widget x ()) -> IO ()```
244 |
245 | It sets up the reflex-dom environment. It takes an argument of type ```Widget``` and returns ```IO ()```
246 |
247 | The type *Widget* is a little bit scary. However we never really need to work with the details of it.
248 |
249 | ~~~ { .haskell }
250 | type Widget x =
251 | PostBuildT
252 | Spider
253 | (ImmediateDomBuilderT
254 | Spider (WithWebView x (PerformEventT Spider (SpiderHost Global))))
255 | ~~~
256 |
257 | *PostBuildT* is a monad transformer. It set's up a monadic environement for reflex-dom.
258 | As side effects, some of the reflex-dom functions will create and change the DOM elements.
259 | To follow this tutorial you don't need to understand the concepts behind monad transformers.
260 |
261 | The function *mainWidget* has two sister functions *mainWidgetWithCss* and *mainWidgetWithHead*.
262 | We will see them later.
263 |
264 | ## Function: *display*
265 |
266 | ```display :: (Show a, ... ) => Dynamic t a -> m ()```
267 |
268 | The function takes an argument of type ```Dynamic t a``` and returns unit in the current monad.
269 | It uses the *Show* instance of the datatype *a* to build a string representation of its first parameter.
270 | Then it creates a text element in the DOM, where it displays the string.
271 | As mentioned above, the creation of the DOM element is a monadic side effect.
272 |
273 | *display* has a precondition of *Show a*. It has other preconditions too.
274 | If you use *mainWidget* or one of its sister functions, the other preconditions are normally fullfilled automatically.
275 | Thefore I don't show them here and in the most examples to follow..
276 |
277 | ## Function: *count*
278 |
279 | ```count :: (Num b, ...) => Event t a -> m (Dynamic t b)```
280 |
281 | The *count* function takes an event as argument and creates a Dynamic.
282 | In this Dynamic the function counts up the number of times the event occured or fired.
283 |
284 | ## Function: *button*
285 |
286 | ```button :: (...) => Text -> m (Event t ())```
287 |
288 | The *button* function takes a text as argument. It creates a DOM button element labelled with the text, and returns
289 | an event with *()* as payload.
290 |
291 | Now it's easy to understand the whole line *mainWidget $ display =<< count =<< button "ClickMe"*:
292 |
293 | * Clicking on the button creates or triggers an event.
294 | * The function *count* creates a *Dynamic* value with the total number of these events.
295 | * The function *display* creates a DOM element with a string representation of this number and displays it as DOM element.
296 |
297 | ## Running the Program in the Browser
298 |
299 | If you installed *reflex-platform* do the following to run the program *src/count01.hs* in the browser:
300 |
301 | * Navigate into your *reflex-platform* directory.
302 | * Start the nix-shell by typing ``` ./try-reflex ``` (The first time this may take some time...)
303 | * In the nix-shell, navigate into your *reflex-dom-inbits* directory. You can use normal linux *cd* commands.
304 | * Compile the program with ``` ghcjs src/count01.hs```
305 | * Open the resulting *src/count01.jsexe/index.html* file with your browser. eg ``` chromium src/count01.jsexe/index.html```
306 |
307 | If you installed reflex with *stack*, use the command ```stack exec ghcjs src/count01.hs``` and then
308 | open the resulting *src/count01.jsexe/index.html* file with your browser as described above.
309 |
310 | Unfortunately interactive ghcjs does not yet work, if the ghcjs compiler was compiled with GHC 8.0.
311 |
312 | ## Running the Program in WebkitGtk
313 |
314 | If you have installed *reflex-platform* do the following to run the program *src/count01.hs* as a native programme:
315 |
316 | * Navigate into your *reflex-platform* directory.
317 | * Start the nix-shell by typing ``` ./try-reflex ``` (The first time this may take some time...)
318 | * In the nix-shell, navigate into your *reflex-dom-inbits* directory. You can use normal linux *cd* commands.
319 | * Run the program with ``` runghc src/count01.hs```
320 |
321 |
322 | # Creating other DOM Elements
323 |
324 | Till now we used the 2 helper functions *button* and *display* to create DOM elements.
325 | Reflex has 2 other very frequently used helper functions to create DOM elements:
326 |
327 | * text
328 | * dynText
329 |
330 | ## Function: *text*
331 |
332 | ```text :: (...) => Text -> m ()```
333 |
334 | It is very simple: It just displays the text in the DOM. During program execution the text displayed in the DOM never changes.
335 | The text is not of type *Dynamic t Text* but only of *Text*, hence it is static!!
336 |
337 | ## Function: *dynText*
338 |
339 | ```dynText :: (...) => Dynamic t Text -> m ()```
340 |
341 | The function *dynText* does more or less the same as the function *text*.
342 | However, the function argument is of type *Dynamic t Text*. The text may change during the execution of the program!!
343 | We will see some examples later.
344 |
345 | ## The Function family *el*, *elAttr*, *elClass*, *elDynAttr*, *elDynClass*
346 |
347 | Reflex-dom has 2 function families to create all the different kind of DOM elements:
348 |
349 | * el, elAttr, elClass, elDynAttr, elDynClass
350 | * el', elAttr', elClass', elDynAttr', elDynClass'
351 |
352 | First we will look at the family without the primes ' in the name.
353 | We will cover the second function family with the primes in the names in a later section.
354 |
355 | ## Function: *el*
356 |
357 | This function has the type signature:
358 |
359 | ```el :: (...) => Text -> m a -> m a```
360 |
361 | It takes a text string with the HTML-type of the DOM element as a first parameter.
362 | The second parameter is either the text of the element or a valid child element.
363 |
364 | The file *src/dom01.hs* contains a typical first example and shows basic usage of the function *el*:
365 |
366 | ~~~ { .haskell }
367 | {-# LANGUAGE OverloadedStrings #-}
368 | import Reflex.Dom
369 |
370 | main :: IO ()
371 | main = mainWidget $ el "h1" $ text "Welcome to Reflex-Dom"
372 | ~~~
373 |
374 | This will create a header-1 DOM element with the text "Welcome to Reflex-Dom".
375 |
376 | In HTML:
377 |
378 | ~~~ { .html }
379 |
380 |
381 | ...
382 |
383 |
384 |
Welcome to Reflex-Dom
385 |
386 |
387 | ~~~
388 |
389 | We are now able to create whole web pages. The file *src/dom02.hs* contains a small example:
390 |
391 | ~~~ { .haskell }
392 | {-# LANGUAGE OverloadedStrings #-}
393 | import Reflex.Dom
394 |
395 | main :: IO()
396 | main = mainWidget $ do
397 | el "h1" $ text "Welcome to Reflex-Dom"
398 | el "div" $ do
399 | el "p" $ text "Reflex-Dom is:"
400 | el "ul" $ do
401 | el "li" $ text "Fun"
402 | el "li" $ text "Not difficult"
403 | el "li" $ text "Efficient"
404 | ~~~
405 |
406 | **Try it!!**
407 |
408 | Unfortunately, this web page is very static, It doesn't react to any input actions of the user.
409 | Later, we will learn how make more dynamic web pages.
410 |
411 | ## Function *blank*
412 |
413 | If you use HTML elements without any values or without a child, you could simply write:
414 |
415 | ```el "br" $ return ()```
416 |
417 | Because this ```return ()``` is used frequently and we need a ```$```, there is a little helper with the following definition:
418 |
419 | ~~~ { .haskell }
420 | blank :: forall m. Monad m => m ()
421 | blank = return ()
422 | ~~~
423 |
424 | Hence we can write: ```el "br" blank```
425 |
426 |
427 | ## Function: *elAttr*
428 |
429 | With the function *el*, we can't create a DOM element with attributes, eg a link:
430 |
431 | ```Google!```
432 |
433 | To add attributes, reflex-dom has a function *elAttr*. It has the type:
434 |
435 | ```elAttr :: (...) => Text -> Map Text Text -> m a -> m a```
436 |
437 | The function *elAttr* is similar to the function *el*, but it takes an additional
438 | parameter of type *Map Text Text*. This parameter contains the attributes of the DOM element.
439 | A Map is a *key-value* relation,
440 | In the above link example, *target* and *href* are the keys and *"_blank"* and *"http://google.com"*
441 | are the values.
442 |
443 | The file *src/dom03.hs* contains an example for *elAttr*:
444 |
445 | ~~~ { .haskell }
446 | {-# LANGUAGE OverloadedStrings #-}
447 | import Reflex.Dom
448 | import qualified Data.Text as T
449 | import qualified Data.Map as Map
450 | import Data.Monoid ((<>))
451 |
452 | main :: IO ()
453 | main = mainWidget $ do
454 | el "h1" $ text "A link to Google in a new tab"
455 | elAttr "a" attrs $ text "Google!"
456 |
457 | attrs :: Map.Map T.Text T.Text
458 | attrs = ("target" =: "_blank") <> ("href" =: "http://google.com")
459 | ~~~
460 |
461 | The module Reflex.Dom defines a little helper function (=:) to create a singelton Map.
462 |
463 | ```(=:) :: k -> a -> Map k a```
464 |
465 | Two singleton maps are then merged / appended with the (<>) operator from Data.Monoid.
466 |
467 |
468 | ## Function: *elClass*
469 |
470 | The function *elClass* allows you to specify the name of the class to be used by the Cascaded Style Sheets (CSS).
471 |
472 | It has the following type:
473 |
474 | ```elClass :: (...) => Text -> Text -> m a -> m a```
475 |
476 | The first parameter is again the HTML-type of the DOM element. The second is the name of the CSS class.
477 |
478 | A small example:
479 |
480 | ```elClass "h1" "mainTitle" $ text "This is the main title"```
481 |
482 | In HTML:
483 |
484 | ```
This is the main title
```
485 |
486 | ## Function: *elDynAttr*
487 |
488 | All the above functions allow us to define DOM elements with static attributes.
489 | But you cannot change the attributes while the program is running!
490 |
491 | With the function *elDynAttr*, as the name says, you can specify dynamic attributes,
492 | that change during program execution. It has the following type:
493 |
494 | ```elDynAttr :: (...) => Text -> Dynamic t (Map Text Text) -> m a -> m a```
495 |
496 | You guessed it, the first parameter is again the type, and the second is a map with **any** attribute,
497 | you can use for your DOM element. However, this time this map is wrapped in a Dynamic.
498 |
499 | To use the function *elDynAttr* we must somehow create a Dynamic.
500 | Reflex has several functions to create Dynamic values. As a first example, we will use the function *toggle*:
501 |
502 | ```toggle :: (...) => Bool -> Event t a -> m (Dynamic t Bool)```
503 |
504 | The function toggle create a new Dynamic using the first parameter as the initial value
505 | and flips this boolean value every time the event in the second parameter occurs.
506 |
507 | The file *src/dom04.hs* contains an example for the function *elDynAttr*:
508 |
509 | ~~~ { .haskell .numberLines}
510 | {-# LANGUAGE OverloadedStrings #-}
511 | {-# LANGUAGE RecursiveDo #-}
512 | import Reflex.Dom
513 | import qualified Data.Text as T
514 | import qualified Data.Map as Map
515 | import Data.Monoid ((<>))
516 |
517 | main :: IO ()
518 | main = mainWidget $ do
519 | rec
520 | dynBool <- toggle False evClick
521 | let dynAttrs = attrs <$> dynBool
522 | elDynAttr "h1" dynAttrs $ text "Changing color"
523 | evClick <- button "Change Color"
524 | return ()
525 |
526 | attrs :: Bool -> Map.Map T.Text T.Text
527 | attrs b = "style" =: ("color: " <> color b)
528 | where
529 | color True = "red"
530 | color _ = "green"
531 | ~~~
532 |
533 | Comments:
534 |
535 | * We need recursive do: We refer to the event *evClick* before it's defined.
536 | * *dynBool* contains our value of type *Dynamic t bool*. It is created by the *toggle* function.
537 | * *dynAttrs* contains the *Dynamic t (Map Text Text)*. It is created with an applicative call to the function *attrs*.
538 | * The function *attrs* contains the 'business logic' of this example:
539 | It decides on the boolean parameter about the color of the DOM element.
540 | * Please note, that the function *attrs* is a normal pure function as we know and love them since Haskell kindergarden!
541 | So if you have a program that needs some input values, you can easily write a reflex-dom frontend, without changing your logic!
542 | * Transforming a Dynamic value or combining several Dynamic values with the help of applicative syntax and a pure function is a common pattern in Reflex.
543 |
544 | ## Function *elDynClass*
545 |
546 | The function *elDynClass* is similar to *elClass* but here the type of the parameter to specify the name of the CSS class is Dynamic.
547 | This allows you to change the CSS-class of the element dynamically during runtime.
548 |
549 | The function has the type:
550 |
551 | ```elDynClass :: (...) => Text -> Dynamic t Text -> m a -> m a```
552 |
553 | # Main Functions
554 |
555 | With the function *elAttr*, *elClass*, *elDynAttr*, and *elDynClass* we can reference selectors of CSS style sheets.
556 | But with the function *mainWidget* we have no possibility to specify one or several css files.
557 | As mentioned above, *mainWidget* has two sister function, and they solve this little issue..
558 |
559 | This is not a tutorial about Cascaded Style Sheets. Therefore I don't spice up my examples with sexy css.
560 | In the next 2 examples, I reference a file "css/simple.css". It contains the most simple css ever possible:
561 |
562 | ```h1 { color: Green; }```
563 |
564 | It just uses a green color for all header-1 DOM elements.
565 |
566 |
567 | ## Function *mainWidgetWithCss*
568 |
569 | This function has the following type:
570 |
571 | ```
572 | mainWidgetWithCss :: ByteString ->
573 | Widget Spider (Gui Spider (WithWebView SpiderHost) (HostFrame Spider)) () ->
574 | IO ()
575 | ```
576 |
577 | Again it looks scary. The first parameter is a ByteString containing your css specifications.
578 | Normally you don't want to have the css specs embeded in your Haskell program. You want them in a separate file.
579 |
580 | On Hackage, there is a library called *file-embed*.
581 | It contains a function *embedFile* that allows you, during compilation, to embed the contents of a file into your source code.
582 | This function uses a GHC feature called *Template Haskell*. Hence we need the GHC language extension for Template Haskell.
583 | And we need an additional import (*Data.FileEmbed*).
584 |
585 | The second paramter of *mainWidgetWithCss* is just the same thing as the parameter of the function *mainWidget*,
586 | we used for all our examples till now. It's the HTML body element.
587 |
588 | The file *src/main01.hs* contains a full example:
589 |
590 | ~~~ { .haskell }
591 | {-# LANGUAGE TemplateHaskell #-}
592 | {-# LANGUAGE OverloadedStrings #-}
593 | import Reflex.Dom
594 | import Data.FileEmbed
595 |
596 | main :: IO ()
597 | main = mainWidgetWithCss css bodyElement
598 | where css = $(embedFile "css/simple.css")
599 |
600 | bodyElement :: MonadWidget t m => m ()
601 | bodyElement = el "div" $ do
602 | el "h1" $ text "This title should be green"
603 | return ()
604 | ~~~
605 |
606 | Comments:
607 |
608 | * Template Haskell runs at compile time. If you change something in your css file, you have to recompile
609 | and re-deploy your application.
610 | * The path to the css file (*css/simple.css* in the above example) is used by the compiler and therefore relative to your working
611 | directory during compile time. I assume that your working directory is *reflex-dom-inbits*.
612 | * If the css file does not exist, or the path is wrong, you will get an error during compile time.
613 | * You can ignore the warning *The constraint ‘MonadWidget t m’ matches an instance declaration...*. Later if necessary you can replace MonadWiget with fine-grained constraints.
614 |
615 | ## Function *mainWidgetWithHead*
616 |
617 | The function *mainWidgetWithHead* has the following type:
618 |
619 | ```
620 | mainWidgetWithHead ::
621 | Widget Spider (Gui Spider (WithWebView SpiderHost) (HostFrame Spider)) () ->
622 | Widget Spider (Gui Spider (WithWebView SpiderHost) (HostFrame Spider)) () -> IO ()
623 | ```
624 | The function *mainWidgetWithHead* takes two parameters as we know them from the functions *mainWidget* and *mainWidgetWithCss*.
625 | The first parameter is the HTML head element, and the second parameter the HTML body element.
626 |
627 | File *src/main02.hs* contains the example:
628 |
629 | ~~~ { .haskell }
630 | {-# LANGUAGE OverloadedStrings #-}
631 | import Reflex.Dom
632 | import Data.Map as Map
633 |
634 | main :: IO ()
635 | main = mainWidgetWithHead headElement bodyElement
636 |
637 | headElement :: MonadWidget t m => m ()
638 | headElement = do
639 | el "title" $ text "Main Title"
640 | styleSheet "css/simple.css"
641 | where
642 | styleSheet link = elAttr "link" (Map.fromList [
643 | ("rel", "stylesheet")
644 | , ("type", "text/css")
645 | , ("href", link)
646 | ]) $ return ()
647 |
648 | bodyElement :: MonadWidget t m => m ()
649 | bodyElement = el "div" $ do
650 | el "h1" $ text "This title should be green"
651 | return ()
652 | ~~~
653 |
654 | Comments:
655 |
656 | * We use the function *Map.fromList* to create the map parameter for the function *elAttr*.
657 | * The path to the css file (*css/simple.css* in the above example) is used during run time.
658 | It is relative to the directory you run your program from.
659 | * Depending on how you run your reflex-dom program, you have to copy your *.css files to the correct directory.
660 | * If the css file does not exist, or the path is wrong, the browser or WebkitGtk will simply ignore your css specs.
661 | * If you compile your program with *ghcjs* and run it with *chromium src/.jsexe/index.html*, your working
662 | directory is *src/.jsexe*. You have to manually copy the directory from *reflex-dom-inbits/css* to *src/.jsexe*.
663 | * If you change your css files, the changes become active after a reload of the *index.html* page in the browser.
664 | * It is possible, to specify other options in your header element.
665 | * Unfortunately you have to annotate the type ```:: MonadWidget t m => m ()``` for the functions *headElement* and *bodyElement*. GHC is not able to infer these types and gives you a not so nice error message.
666 | * The *title* element in the header, will be used in the page tab of your browser.
667 |
668 | ## Summary
669 |
670 | * Use *mainWidget* for small examples.
671 | * Use *mainWidgetWithCss* if you don't want anybody to change your CSS specifications.
672 | * Use *mainWidgetWithHead* for professional projects.
673 |
674 |
675 | # Basic Event Handling
676 |
677 | In this section we will have a first look on how to use events.
678 |
679 | ## Function *foldDyn*
680 |
681 | Remember the first example *src/dom01.hs* with the counter. There we used the predefined function *count*.
682 | We will now do the same example, but we handle the events ourselfs with the *foldDyn* function.
683 |
684 | The function *foldDyn* has the type:
685 |
686 | ```foldDyn :: (...) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)```
687 |
688 | It works similar to the well known *foldr* function from the list data type.
689 | It first creates a Dynamic with the value specified in the second parameter.
690 | Every time an event (third parameter) occurs,
691 | it uses the fold function in the first parameter and folds up the values of the event.
692 |
693 | File *src/event01.hs* contains an example for *foldDyn*
694 |
695 | ~~~ { .haskell .numberLines}
696 | {-# LANGUAGE RecursiveDo #-}
697 | {-# LANGUAGE OverloadedStrings #-}
698 | import Reflex.Dom
699 |
700 | main :: IO ()
701 | main = mainWidget bodyElement
702 |
703 | bodyElement :: MonadWidget t m => m ()
704 | bodyElement = do
705 | rec el "h2" $ text "Counter as a fold"
706 | numbs <- foldDyn (+) (0 :: Int) (1 <$ evIncr)
707 | el "div" $ display numbs
708 | evIncr <- button "Increment"
709 | return ()
710 | ~~~
711 |
712 | Look at the line: ```numbs <- foldDyn (+) (0 :: Int) (1 <$ evIncr)```:
713 |
714 | * We use the normal addition as a fold function.
715 | * We have to specify the type of the initial value. The compiler does not know whether we want to count up numbers of type *Int* or *Integer* or even *Float*.
716 | * evIncr has the type ```Event t ()```. We cannot use () as an argument for the (+) fold function. Therfore we use
717 | applicative syntax to replace the event payload *()* by the number *1*. 1 we can use together with our fold function (+)!
718 | * This is the first example that uses *event tranformation*
719 | * We need *recursive do": We refer to *evIncr* before it is defined.
720 |
721 | Please note, that in reflex-dom the implemention of *count* differs from our example above.
722 |
723 | ## Function *leftmost*
724 |
725 | Now we want a second button to decrement our counter.
726 | To combine the events of the 2 buttons we use the function *leftmost*:
727 |
728 | ```leftmost :: Reflex t => [Event t a] -> Event t a```
729 |
730 | If an event in the array of the first parameter occurs, the function *leftmost* returns this event.
731 | If two or more events in the array occur simultaneously,
732 | the function *leftmost* returns the leftmost of all the simultaneously occuring events.
733 |
734 | File *src/event02.hs* contains the example:
735 |
736 | ~~~ { .haskell .numberLines}
737 | {-# LANGUAGE RecursiveDo #-}
738 | {-# LANGUAGE OverloadedStrings #-}
739 | import Reflex.Dom
740 |
741 | main :: IO ()
742 | main = mainWidget bodyElement
743 |
744 | bodyElement :: MonadWidget t m => m ()
745 | bodyElement = do
746 | rec el "h2" $ text "Combining Events with leftmost"
747 | counts <- foldDyn (+) (0 :: Int) $ leftmost [1 <$ evIncr, -1 <$ evDecr]
748 | el "div" $ display counts
749 | evIncr <- button "Increment"
750 | evDecr <- button "Decrement"
751 | return ()
752 | ~~~
753 |
754 | ## Function *mergeWith*
755 |
756 | Assume, it would be possible to click in the above example both buttons simultaneously.
757 | If we click both buttons together, the function *leftmost* returns only *evIncr* and we lose *evDecr*.
758 | In situations, where we are not allowed to lose events, we can use the function *mergeWith*.
759 |
760 | The function *mergeWith* has the following type:
761 |
762 | ```mergeWith :: Reflex t => (a -> a -> a) -> [Event t a] -> Event t a```
763 |
764 | It uses a function of type *(a -> a -> a)* to combine the payloads of all simultaneously occuring events.
765 |
766 | File *src/event03.hs* contains the full example:
767 |
768 | ~~~ { .haskell .numberLines}
769 | {-# LANGUAGE RecursiveDo #-}
770 | {-# LANGUAGE OverloadedStrings #-}
771 | import Reflex.Dom
772 |
773 | main :: IO ()
774 | main = mainWidget bodyElement
775 |
776 | bodyElement :: MonadWidget t m => m ()
777 | bodyElement = do
778 | rec el "h2" $ text "Combining Events with mergeWith and foldDyn"
779 | dynCount <- foldDyn (+) (0 :: Int) (mergeWith (+) [1 <$ evIncr, -1 <$ evDecr])
780 | el "div" $ display dynCount
781 | evIncr <- button "Increment"
782 | evDecr <- button "Decrement"
783 | return ()
784 | ~~~
785 |
786 | ## Function Application as Fold Function
787 |
788 | Now in addition to the increment and decrement buttons, we want a third button to reset the counter to zero.
789 | The challenge is this reset: To continue to use normal addition as a fold function, we would have to read out the current value of the counter
790 | and replace the reset event with the negative value of the counter. This, however, is very messy!!
791 |
792 | A better approach is to use events, that carry functions as payloads.
793 | We transform
794 |
795 | * the payload of the event of the increment button to the function ```(+ 1)```,
796 | * the payload of the event of the decrement button to the function ```(+ (-1))```,
797 | * the payload of the event of the reset button to the function ```const 0```.
798 |
799 | As a fold function we then use normal function application *($)* to apply the transformed function to the current value of our counter.
800 |
801 | File *src/event04.hs* has the full example:
802 |
803 | ~~~ { .haskell .numberLines}
804 | {-# LANGUAGE RecursiveDo #-}
805 | {-# LANGUAGE OverloadedStrings #-}
806 | import Reflex.Dom
807 |
808 | main :: IO ()
809 | main = mainWidget bodyElement
810 |
811 | bodyElement :: MonadWidget t m => m ()
812 | bodyElement = do
813 | el "h2" $ text "Using foldDyn with function application"
814 | rec dynNum <- foldDyn ($) (0 :: Int) $ leftmost [(+ 1) <$ evIncr, (+ (-1)) <$ evDecr, const 0 <$ evReset]
815 | el "div" $ display dynNum
816 | evIncr <- button "Increment"
817 | evDecr <- button "Decrement"
818 | evReset <- button "Reset"
819 | return ()
820 | ~~~
821 |
822 | Using function application as a fold function over a current value is very powerful!! We'll see more examples.
823 |
824 | # Predefined Input Widgets
825 |
826 | In this section, we look at the standard reflex-dom input elements. They are predefined and easy to use.
827 | We already have seen buttons, hence we will not cover them here again.
828 |
829 | For most of the input widgets, reflex-dom defines two data structures
830 |
831 | * a configuration record
832 | * an element record.
833 |
834 | ## Text Input Fields
835 |
836 | *TextInput* fields are of the most popular input widgets in GUI applications.
837 | They allow the user to enter texual data, eg their name, address, phone and credit card numbers and so on.
838 |
839 | The configuration record has the following definition:
840 |
841 | ~~~ { .haskell }
842 | data TextInputConfig t
843 | = TextInputConfig { _textInputConfig_inputType :: Text
844 | , _textInputConfig_initialValue :: Text
845 | , _textInputConfig_setValue :: Event t Text
846 | , _textInputConfig_attributes :: Dynamic t (Map Text Text) }
847 | ~~~
848 |
849 | and the element record is defined as:
850 |
851 | ~~~ { .haskell }
852 | data TextInput t
853 | = TextInput { _textInput_value :: Dynamic t Text
854 | , _textInput_input :: Event t Text
855 | , _textInput_keypress :: Event t Int
856 | , _textInput_keydown :: Event t Int
857 | , _textInput_keyup :: Event t Int
858 | , _textInput_hasFocus :: Dynamic t Bool
859 | , _textInput_builderElement :: InputElement EventResult GhcjsDomSpace t }
860 | ~~~
861 |
862 | The type *GhcjsDomSpace* originates from a lower level GHCJS library, that is used to build Reflex-dom. You will not use it in your program.
863 |
864 | The function to create a text input element is:
865 |
866 | ```textInput :: (...) => TextInputConfig t -> m (TextInput t)```
867 |
868 | ### The Type Class Default
869 |
870 | The Haskell library *data-default-class* defines a type class *Default* to initialize a data type with default values:
871 |
872 | ~~~ { .haskell }
873 | -- | A class for types with a default value.
874 | class Default a where
875 | -- | The default value for this type.
876 | def :: a
877 | ~~~
878 |
879 | It has one single function *def*.
880 |
881 | The data type *TextInputConfig* is an instance of this type class and the *def* function is defined like this:
882 |
883 | ~~~ { .haskell }
884 | instance Reflex t => Default (TextInputConfig t) where
885 | def = TextInputConfig { _textInputConfig_inputType = "text"
886 | , _textInputConfig_initialValue = ""
887 | , _textInputConfig_setValue = never
888 | , _textInputConfig_attributes = constDyn mempty }
889 | ~~~
890 |
891 | We will see more configuration records. They are all instances of the type class *Default*.
892 |
893 | ### The Function *never*
894 |
895 | ``` never :: Event t a```
896 |
897 | It's an event, that never occurs.
898 |
899 |
900 | ### The Function *constDyn*
901 |
902 | Note the type of the _textInputConfig_attributes: It's ```Dynamic t (Map Text Text)```.
903 | To create a Dynamic map we can use the function *constDyn*:
904 | It takes an value of type ```a``` and returns a value of type ```Dynamic t a```.
905 | Of course, a Dynamic created with *constDyn* will not change while our program is running.
906 |
907 |
908 | ~~~ { .haskell }
909 | constDyn :: Reflex t => a -> Dynamic t a
910 | ~~~
911 |
912 | ### Syntactic Sugar with (&) and (.~)
913 |
914 | *TextInputConfig* is a normal Haskell record structure with accessor functions.
915 | You can use the following code to create a TextInput widget configured with an initial value:
916 |
917 | ```textInput def { _textInputConfig_initialValue = "0"}```
918 |
919 | However Reflex-dom uses lenses to give us syntactic sugar to populate these configuration records.
920 |
921 | With the two combinators *(&)* and *(.~)*. we can write:
922 |
923 | ```textInput $ def & textInputConfig_initialValue .~ "input"```
924 |
925 | Note that the underscore (_) in front of *_textInputConfig* has gone.
926 |
927 | If you are not familar with lenses, you can use the standard Haskell record syntax.
928 |
929 | ### Examples
930 |
931 | The first example in *src/textinput01.hs* is very simple:
932 |
933 | A TextInput widget where you can enter some text. The text you entered is immediately shown in a second widget.
934 | It uses the *dynText* function we described earlier and the function *value* to extract the current value out of the TextInput widget.
935 |
936 | ~~~ { .haskell }
937 | {-# LANGUAGE OverloadedStrings #-}
938 | import Reflex.Dom
939 |
940 | main :: IO()
941 | main = mainWidget bodyElement
942 |
943 | bodyElement :: MonadWidget t m => m ()
944 | bodyElement = el "div" $ do
945 | el "h2" $ text "Simple Text Input"
946 | ti <- textInput def
947 | dynText $ value ti
948 | ~~~
949 |
950 | ### The Type Class *HasValue*
951 |
952 | In the above example the value *ti* is of type *TextInput*.
953 | The function *dynText* needs an parameter of type *Dynamic t Text*.
954 | So the function *value* should have the type: ```value :: TextInput -> Dynamic t Text```.
955 |
956 | However, this is not quite true!
957 | In the section about checkboxes you will see a function again called *value*. It will have the type:
958 |
959 | ```value :: Checkbox -> Dynamic t Bool``` .
960 |
961 | Reflex-dom uses advanced Haskell type level hackery to define this *value* function polymorphically. For you it's simple:
962 | The function *value* normally does what you naturally expect!
963 | Most of the predefined input widgets are instances of the type class *HasValue*. Hence most of these widgets
964 | support the *value* function. All of these *value* functions return *Dynamic* values.
965 |
966 | The next example in *src/textinput02.hs* shows some examples how to configure a TextInput widget.
967 |
968 | ~~~ { .haskell }
969 | {-# LANGUAGE OverloadedStrings #-}
970 | import Reflex.Dom
971 | import Data.Monoid ((<>))
972 |
973 | main :: IO ()
974 | main = mainWidget bodyElement
975 |
976 | bodyElement :: MonadWidget t m => m ()
977 | bodyElement = do
978 | el "h2" $ text "Text Input - Configuration"
979 |
980 | el "h4" $ text "Max Length 14"
981 | t1 <- textInput $ def & attributes .~ constDyn ("maxlength" =: "14")
982 | dynText $ _textInput_value t1
983 |
984 | el "h4" $ text "Initial Value"
985 | t2 <- textInput $ def & textInputConfig_initialValue .~ "input"
986 | dynText $ _textInput_value t2
987 |
988 | el "h4" $ text "Input Hint"
989 | t3 <- textInput $
990 | def & attributes .~ constDyn("placeholder" =: "type something")
991 | dynText $ _textInput_value t3
992 |
993 | el "h4" $ text "Password"
994 | t4 <- textInput $ def & textInputConfig_inputType .~ "password"
995 | dynText $ _textInput_value t4
996 |
997 | el "h4" $ text "Multiple Attributes: Hint + Max Length"
998 | t5 <- textInput $ def & attributes .~ constDyn ("placeholder" =: "Max 6 chars" <> "maxlength" =: "6")
999 | dynText $ _textInput_value t5
1000 |
1001 | el "h4" $ text "Numeric Field with initial value"
1002 | t6 <- textInput $ def & textInputConfig_inputType .~ "number"
1003 | & textInputConfig_initialValue .~ "0"
1004 | dynText $ _textInput_value t6
1005 |
1006 | return ()
1007 | ~~~
1008 |
1009 | ## Reading out the Value of a TextInput Widget on an Event
1010 |
1011 | In all the above examples we used the contents of the TextInput field immediately when it changed.
1012 | Sometimes you want to use this contents when the user clicks a button.
1013 |
1014 | ### Function *tagPromptlyDyn*
1015 |
1016 | ```tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a```
1017 |
1018 | When the event in the second function parameter occurs, the function returns a new event with the payload of the Dynamic value in the first function parameter.
1019 | The function *tagPromptlyDyn* *lifts* the Dynamic value onto the Event.
1020 |
1021 | ### Function *holdDyn*
1022 |
1023 | ```holdDyn :: (...) => a -> Event t a -> m (Dynamic t a)```
1024 |
1025 | It converts an Event with a payload of type *a* into a Dynamic with the same value.
1026 | We have to specify a default value, to be used before the first event occurs.
1027 |
1028 | Look at the last two lines in the file *src/textinput03.hs*:
1029 |
1030 | * *evText* is the event that carries the contents of the TextInput as payload.
1031 | * With the function *holdDyn* we create a Dynamic. Its value changes on each click on the button.
1032 |
1033 |
1034 | ~~~ { .haskell }
1035 | {-# LANGUAGE OverloadedStrings #-}
1036 | import Reflex.Dom
1037 |
1038 | main :: IO ()
1039 | main = mainWidget bodyElement
1040 |
1041 | bodyElement :: MonadWidget t m => m ()
1042 | bodyElement = do
1043 | el "h2" $ text "Text Input - Read Value on Button Click"
1044 | ti <- textInput def
1045 | evClick <- button "Click Me"
1046 | el "br" blank
1047 | text "Contents of TextInput on last click: "
1048 | let evText = tagPromptlyDyn (value ti) evClick
1049 | dynText =<< holdDyn "" evText
1050 | ~~~
1051 |
1052 | ## Reading out the Value of a TextInput Widget on Pressing the *Return* Key
1053 |
1054 | Sometimes you want to use the text in the TextInput widget when the user presses the *Return/Enter* key
1055 | inside the widget.
1056 |
1057 | ### Function *keypress*
1058 |
1059 | ```keypress :: (...) => Key -> e -> Event t ()```
1060 |
1061 | Instead of the button, we use this function to create the event that triggers reading the TextInput value:
1062 |
1063 | The code in *src/textinput04.hs* is similar to the example above:
1064 |
1065 | ~~~ { .haskell }
1066 | {-# LANGUAGE OverloadedStrings #-}
1067 | import Reflex.Dom
1068 |
1069 | main :: IO ()
1070 | main = mainWidget bodyElement
1071 |
1072 | bodyElement :: MonadWidget t m => m ()
1073 | bodyElement = do
1074 | el "h2" $ text "Text Input - Read Value on 'Enter'"
1075 | ti <- textInput def
1076 | el "br" blank
1077 | text "Contents of TextInput after 'Enter': "
1078 | let evEnter = keypress Enter ti
1079 | let evText = tagPromptlyDyn (value ti) evEnter
1080 | dynText =<< holdDyn "" evText
1081 | ~~~
1082 |
1083 | ## Setting the Contents of a TextInput Widget
1084 |
1085 | Sometimes you want to set the text in the TextInput widget with the value of an Dynamic t T.Text.
1086 | Remember, reflex-dom is Haskell and we cannot set the value of a widget *somewhere* in the code.
1087 | We have to specify this, when we define the widget in our code.
1088 |
1089 | Using the syntactic sugar of the lens library is the easiest way to set the value of a TextInput widget:
1090 |
1091 | ```ti <- textInput $ def & setValue .~ evText```
1092 |
1093 | Here the value *evText* has the type ```Event t T.Text```. The text payload of this event will be written
1094 | into the TextInput widget. That's it!
1095 |
1096 | If you are not familiar with lenses, just take this as a syntax construct.
1097 |
1098 | File *src/textinput05.hs* has the example:
1099 |
1100 | ~~~ { .haskell }
1101 | {-# LANGUAGE OverloadedStrings #-}
1102 | import Reflex.Dom
1103 | import qualified Data.Text as T
1104 |
1105 | main :: IO ()
1106 | main = mainWidget body
1107 |
1108 | body :: MonadWidget t m => m ()
1109 | body = do
1110 | el "h1" $ text "Write into TextInput Widget"
1111 | t1 <- textInput def
1112 | evCopy <- button ">>>"
1113 | let evText = tagPromptlyDyn (value t1) evCopy
1114 | t2 <- textInput $ def & setValue .~ evText
1115 | return ()
1116 | ~~~
1117 |
1118 | We define two TextInput widgets and a button in between. Clicking the button
1119 | generates the event *evCopy* with a unit *()* as payload.
1120 | Then we use the function *tagPromptlyDyn* to create a new event *evText*, with
1121 | the value of the first textbox as payload. In the definition of the second TextInput widget we
1122 | use this event to set the text of the second textbox.
1123 |
1124 | Sometimes you want to have a *Reset* button to clear TextInput widgets. This is now easy:
1125 | We use event transformation to create an event with an empty text. The code is in *src/textinput06.hs*:
1126 |
1127 | ~~~ { .haskell }
1128 | {-# LANGUAGE OverloadedStrings #-}
1129 | {-# LANGUAGE RecursiveDo #-}
1130 | import Reflex.Dom
1131 | import qualified Data.Text as T
1132 |
1133 | main :: IO ()
1134 | main = mainWidget body
1135 |
1136 | body :: MonadWidget t m => m ()
1137 | body = do
1138 | rec el "h1" $ text "Clear TextInput Widget"
1139 | ti <- textInput $ def & setValue .~ ("" <$ evReset)
1140 | evReset <- button "Reset"
1141 | return ()
1142 | ~~~
1143 |
1144 | We use *recursive do* because we define the Reset button after the TextInput widget.
1145 |
1146 |
1147 | ## TextAreas
1148 |
1149 | TextInput fields have only one input text line. If you want several input lines, you must use TextAreas.
1150 |
1151 | TextAreas are built in the same way as TextInput fields: There is a configuration record and a data record.
1152 | They are similar to the record for TextInput fields. The names are different: *_textInput* is replaced by *_textArea*.
1153 |
1154 | ## Using Several TextInput Fields
1155 |
1156 | With the TextInput and TextArea widgets we are now able to write our first usefull GUI program in Haskell:
1157 | It is a RGB color viewer. We enter the 3 color components, and the program shows us the resulting RGB color.
1158 |
1159 | The file *src/colorviewer.hs* contains the example:
1160 |
1161 | ~~~ { .haskell }
1162 | {-# LANGUAGE OverloadedStrings #-}
1163 | import Reflex.Dom
1164 | import Data.Map
1165 | import qualified Data.Text as T
1166 |
1167 | main :: IO ()
1168 | main = mainWidget bodyElement
1169 |
1170 | bodyElement :: MonadWidget t m => m ()
1171 | bodyElement = do
1172 | el "h2" $ text "RGB Viewer"
1173 | el "div" $ text "Enter RGB component values as numbers between 0 and 255"
1174 | dfsRed <- labledBox "Red: "
1175 | dfsGreen <- labledBox "Green: "
1176 | dfsBlue <- labledBox "Blue: "
1177 | textArea $
1178 | def & attributes .~ (styleMap <$> value dfsRed <*> value dfsGreen <*> value dfsBlue)
1179 | return ()
1180 |
1181 | labledBox :: MonadWidget t m => T.Text -> m (TextInput t)
1182 | labledBox lbl = el "div" $ do
1183 | text lbl
1184 | textInput $ def & textInputConfig_inputType .~ "number"
1185 | & textInputConfig_initialValue .~ "0"
1186 |
1187 | styleMap :: T.Text -> T.Text -> T.Text -> Map T.Text T.Text
1188 | styleMap r g b = "style" =: mconcat ["background-color: rgb(", r, ", ", g, ", ", b, ")"]
1189 | ~~~
1190 |
1191 | As soon as you change the value in one of the TextInput fields, the background color of the TextArea widget changes!
1192 |
1193 | Comments:
1194 |
1195 | * The function *labledBox* combines a TextInput field with a label.
1196 | * The interesting thing happens in the line ```styleMap <$> value dfsRed <*> value dfsGreen <*> value dfsBlue```.
1197 | We again use applicative syntax to call the function *styleMap* with the current values of our 3 input fields.
1198 | * The function styleMap contains our 'business logic'. It creates the correct string to color the resulting TextArea widget.
1199 | * The function *styleMap* is again a normal, simple, pure Haskell function!
1200 | * The example shows, how to process the input of several TextInput fields
1201 |
1202 | ## Checkboxes
1203 |
1204 | Checkboxes are rather simple, therefore the configuration and the element records are simple too.
1205 |
1206 | The configuration record:
1207 |
1208 | ~~~ { .haskell }
1209 | data CheckboxConfig t
1210 | = CheckboxConfig { _checkboxConfig_setValue :: Event t Bool
1211 | , _checkboxConfig_attributes :: Dynamic t (Map Text Text) }
1212 | ~~~
1213 |
1214 | The *Default* instance:
1215 |
1216 | ~~~ { .haskell }
1217 | instance Reflex t => Default (CheckboxConfig t) where
1218 | def = CheckboxConfig { _checkboxConfig_setValue = never
1219 | , _checkboxConfig_attributes = constDyn mempty }
1220 | ~~~
1221 |
1222 | The element function:
1223 |
1224 | ```checkbox :: (...) => Bool -> CheckboxConfig t -> m (Checkbox t)```
1225 |
1226 | The first pramameter is the initial state of the checkbox: True for checked, False for unchecked.
1227 |
1228 | ### Example *src/checkbox01.hs*
1229 |
1230 | ~~~ { .haskell }
1231 | {-# LANGUAGE OverloadedStrings #-}
1232 | import Reflex.Dom
1233 | import qualified Data.Text as T
1234 |
1235 | main :: IO ()
1236 | main = mainWidget bodyElement
1237 |
1238 | bodyElement :: MonadWidget t m => m ()
1239 | bodyElement = el "div" $ do
1240 | el "h2" $ text "Checkbox (Out of the box)"
1241 | cb <- checkbox True def
1242 | text "Click me"
1243 | el "p" blank
1244 | let dynState = checkedState <$> value cb
1245 | dynText dynState
1246 |
1247 | checkedState :: Bool -> T.Text
1248 | checkedState True = "Checkbox is checked"
1249 | checkedState _ = "Checkbox is not checked"
1250 | ~~~
1251 |
1252 | As mentioned above, here the function *value* is used with the type signature: ```value :: Checkbox -> Dynamic t Bool``` .
1253 |
1254 | This is the most simple way to create and use a checkbox. However, you have to click exactly into
1255 | the small square to change the state of the checkbox. When you click at the label *Click me* it does not
1256 | change it's state. This is not user friendly!
1257 |
1258 | ### Example *src/checkbox02.hs*
1259 |
1260 | ~~~ { .haskell }
1261 | {-# LANGUAGE OverloadedStrings #-}
1262 | import Reflex.Dom
1263 | import qualified Data.Text as T
1264 |
1265 | main = mainWidget $ el "div" $ do
1266 | el "h2" $ text "Checkbox - User friendly"
1267 | cb <- el "label" $ do
1268 | cb1 <- checkbox True def
1269 | text "Click me"
1270 | return cb1
1271 | el "p" blank
1272 | let dynState = checkedState <$> value cb
1273 | dynText dynState
1274 |
1275 | checkedState :: Bool -> T.Text
1276 | checkedState True = "Checkbox is checked"
1277 | checkedState _ = "Checkbox is not checked"
1278 | ~~~
1279 |
1280 | This example shows how to fix the issue with checkbox01.hs.
1281 | We create a combined widget: The checkbox element is a child of a *label* element.
1282 | The result of the combined widget is the checkbox.
1283 |
1284 | It works now as expected: To change the state of the checkbox, you can either click into the small square
1285 | or at the text *Click me*
1286 |
1287 | ## Radio Buttons
1288 |
1289 | Unfortunately basic reflex-dom does not contain a simple predefined function to create radio buttons.
1290 | We will learn how to write our own radio button function later in the chapter *Defining your own events*.
1291 |
1292 | However there is a library reflex-dom-contrib on Github: [https://github.com/reflex-frp/reflex-dom-contrib](https://github.com/reflex-frp/reflex-dom-contrib).
1293 |
1294 | This library also defines a configuration record and a widget record.
1295 |
1296 | The configuration record is defined as:
1297 |
1298 | ~~~ { .haskell }
1299 | data WidgetConfig t a
1300 | = WidgetConfig { _widgetConfig_setValue :: Event t a
1301 | , _widgetConfig_initialValue :: a
1302 | , _widgetConfig_attributes :: Dynamic t (Map Text Text)
1303 | }
1304 | ~~~
1305 |
1306 | The widget record is defined as:
1307 |
1308 | ~~~ { .haskell }
1309 | data HtmlWidget t a = HtmlWidget
1310 | { _hwidget_value :: Dynamic t a
1311 | -- ^ The authoritative value for this widget.
1312 | , _hwidget_change :: Event t a
1313 | -- ^ Event that fires when the widget changes internally (not via a
1314 | -- setValue event).
1315 | , _hwidget_keypress :: Event t Int
1316 | , _hwidget_keydown :: Event t Int
1317 | , _hwidget_keyup :: Event t Int
1318 | , _hwidget_hasFocus :: Dynamic t Bool
1319 | }
1320 | ~~~
1321 |
1322 | From this library we use the function *radioGroup*:
1323 |
1324 | ~~~ { .haskell }
1325 | radioGroup :: (Eq a, ...) => Dynamic t T.Text -> Dynamic t [(a, T.Text)] -> GWidget t m (Maybe a)
1326 | ~~~
1327 |
1328 | Remember: In HTML a radio button in a list looks like:
1329 |
1330 | ~~~ { .html }
1331 |
LARGE
1332 | ~~~
1333 |
1334 | The first parameter of the function *radioGroup* is a *Dynamic T.Text* that is used to create the *name* attribute (*size* in the above HTML).
1335 |
1336 |
1337 | The second parameter of the function *radioGroup* takes a list of tuples. The left component of one tuple contains a value.
1338 | This value will be the payload of the event, that is created when the radio button is clicked.
1339 | The type of this value must be an instance of the *Eq* type class.
1340 | The right component of the tuple will be used as label of the radio button.
1341 |
1342 | The function *radioGroup* returns a value of type GWidget. The documentation for the function *radioGroup* says:
1343 | *Radio group in a 'GWidget' interface (function from 'WidgetConfig' to 'HtmlWidget' )*.
1344 | Hence we have to add a *WidgetConfig* value, that will be consumed by the resulting function of *radioGroup*
1345 |
1346 | The final result is of type *HtmlWidget*.
1347 |
1348 | File *src/radio01.hs* shows the details
1349 |
1350 | ~~~ { .haskell }
1351 | {-# LANGUAGE OverloadedStrings #-}
1352 | {-# LANGUAGE RecursiveDo #-}
1353 | {-# LANGUAGE ScopedTypeVariables #-}
1354 | import Reflex.Dom
1355 | import Reflex.Dom.Contrib.Widgets.Common
1356 | import Reflex.Dom.Contrib.Widgets.ButtonGroup
1357 | import qualified Data.Text as T
1358 |
1359 | main :: IO ()
1360 | main = mainWidget bodyElement
1361 |
1362 | bodyElement :: MonadWidget t m => m ()
1363 | bodyElement = do
1364 | el "h2" $ text "Radio Buttons from the Contrib Library"
1365 | rec
1366 | rbs :: HtmlWidget t (Maybe Selection) <-
1367 | radioGroup
1368 | (constDyn "size")
1369 | (constDyn [(Small, "small"), (Medium, "Medium"), (Large, "LARGE")])
1370 | WidgetConfig { _widgetConfig_initialValue = Nothing
1371 | , _widgetConfig_setValue = never
1372 | , _widgetConfig_attributes = constDyn mempty}
1373 | text "Result: "
1374 | display (translate <$> _hwidget_value rbs)
1375 | return ()
1376 |
1377 | -- | A data type for the different choices
1378 | data Selection = Small | Medium | Large
1379 | deriving Eq
1380 |
1381 | -- | Helper function to translate a Selection to an Text value containing a number
1382 | translate :: Maybe Selection -> T.Text
1383 | translate Nothing = "0"
1384 | translate (Just Small) = "10"
1385 | translate (Just Medium) = "50"
1386 | translate (Just Large) = "800"
1387 | ~~~
1388 |
1389 | Comments
1390 |
1391 | * If you use the debugger or inspector of your web browser, you will see, that the function *radioGroup* does not pack the radio buttons into a HTML list. It packs them into a HTML table.
1392 | * The type annotation in the line ```rbs :: HtmlWidget t (Maybe Selection) <- radioGroup ... ``` is not necessary. I added it, so you can immediately see the type.
1393 | * Again we use applicative syntax and a pure Haskell function to transform the *rbs* value to a dynamic text.
1394 | * Again translating the Selection to our result string (the *business logic*) is done with a simple pure function!
1395 | * Radio buttons from the contrib library are userfriendly: To check, you can either click on the small circle or on the label.
1396 |
1397 | ## DropDowns
1398 |
1399 | A DropDown allows you to select items from a predefined list. Normally you only see the selected item. When you click on the little down array, the dropdown widget presents a list of possible values and you can choose one.
1400 |
1401 | DropDowns are defined in the basic reflex-dom library. We don't need the *contrib* library to use them.
1402 |
1403 | DropDowns also have a configuration record that supports the *Default* instance and an element record:
1404 |
1405 | The configuration record:
1406 |
1407 | ~~~ { .haskell }
1408 | data DropdownConfig t k
1409 | = DropdownConfig { _dropdownConfig_setValue :: Event t k
1410 | , _dropdownConfig_attributes :: Dynamic t (Map Text Text)
1411 | }
1412 | ~~~
1413 |
1414 | The default instance:
1415 |
1416 | ~~~ { .haskell }
1417 | instance Reflex t => Default (DropdownConfig t k) where
1418 | def = DropdownConfig { _dropdownConfig_setValue = never
1419 | , _dropdownConfig_attributes = constDyn mempty
1420 | }
1421 | ~~~
1422 |
1423 | The element record:
1424 |
1425 | ~~~ { .haskell }
1426 | data Dropdown t k
1427 | = Dropdown { _dropdown_value :: Dynamic t k
1428 | , _dropdown_change :: Event t k
1429 | }
1430 | ~~~
1431 |
1432 | The *dropdown* function has the following type:
1433 |
1434 | ~~~ { .haskell }
1435 | dropdown :: (Ord k, ...) => k
1436 | -> Dynamic t (Map.Map k Text)
1437 | -> DropdownConfig t k
1438 | -> m (Dropdown t k)
1439 | ~~~
1440 |
1441 | Let's start with the second parameter: It's a map. The keys of type k of this map identify the values.
1442 | The *Text* values will be shown in the dropdown to the user. The values will be presented in the order of the keys.
1443 | In the example below, the keys are of type *Int*.
1444 |
1445 | The first parameter is the key of the item, that is initially selected. In the example below it's the *2*.
1446 | Hence the dropdown shows *Switzerland*.
1447 |
1448 | The file *src/dropdown01.hs* has the example:
1449 |
1450 | ~~~ { .haskell }
1451 | {-# LANGUAGE OverloadedStrings #-}
1452 | import Reflex.Dom
1453 | import qualified Data.Text as T
1454 | import qualified Data.Map as Map
1455 | import Data.Monoid((<>))
1456 | import Data.Maybe (fromJust)
1457 |
1458 | main :: IO ()
1459 | main = mainWidget bodyElement
1460 |
1461 | bodyElement :: MonadWidget t m => m ()
1462 | bodyElement = el "div" $ do
1463 | el "h2" $ text "Dropdown"
1464 | text "Select country "
1465 | dd <- dropdown 2 (constDyn countries) def
1466 | el "p" $ return ()
1467 | let selItem = result <$> value dd
1468 | dynText selItem
1469 |
1470 | countries :: Map.Map Int T.Text
1471 | countries = Map.fromList [(1, "France"), (2, "Switzerland"), (3, "Germany"), (4, "Italy"), (5, "USA")]
1472 |
1473 | result :: Int -> T.Text
1474 | result key = "You selected: " <> fromJust (Map.lookup key countries)
1475 | ~~~
1476 |
1477 | Let's look at the line ```let selItem = result <$> value dd``` .
1478 | In our example the expression *value dd* returns an element of the type *Int*.
1479 | If the user chooses "Germany" this expression evaluates to *3*. This is the map-key of the selected item.
1480 | To print out the selected item, we use the function *result* to look up this key in our map.
1481 |
1482 | If you use the function *dropdown* with a first parameter that is missing as key in the map of the second parameter,
1483 | reflex will add a *(key,value)* pair with this missing key and an empty text string. Hence the use of *Map.lookup*
1484 | is not dangerous!
1485 |
1486 | ## Ranges
1487 |
1488 | Ranges allow the user to select a value from a range of values.
1489 |
1490 | Ranges again have a configuration and an element record.
1491 | The configuration record is an instance of the *Default* type class.
1492 |
1493 | The configuration record:
1494 |
1495 | ~~~ { .haskell }
1496 | data RangeInputConfig t
1497 | = RangeInputConfig { _rangeInputConfig_initialValue :: Float
1498 | , _rangeInputConfig_setValue :: Event t Float
1499 | , _rangeInputConfig_attributes :: Dynamic t (Map Text Text)
1500 | }
1501 | ~~~
1502 |
1503 | The *Default* instance:
1504 |
1505 | ~~~ { .haskell }
1506 | instance Reflex t => Default (RangeInputConfig t) where
1507 | def = RangeInputConfig { _rangeInputConfig_initialValue = 0
1508 | , _rangeInputConfig_setValue = never
1509 | , _rangeInputConfig_attributes = constDyn mempty
1510 | }
1511 | ~~~
1512 |
1513 | The element record:
1514 |
1515 | ~~~ { .haskell }
1516 | data RangeInput t
1517 | = RangeInput { _rangeInput_value :: Dynamic t Float
1518 | , _rangeInput_input :: Event t Float
1519 | , _rangeInput_mouseup :: Event t (Int, Int)
1520 | , _rangeInput_hasFocus :: Dynamic t Bool
1521 | , _rangeInput_element :: HTMLInputElement
1522 | }
1523 | ~~~
1524 |
1525 | The example in *src/range01* uses default values for everything:
1526 |
1527 | ~~~ { .haskell }
1528 | {-# LANGUAGE OverloadedStrings #-}
1529 | import Reflex.Dom
1530 | import qualified Data.Text as T
1531 |
1532 | main :: IO ()
1533 | main = mainWidget bodyElement
1534 |
1535 | bodyElement :: MonadWidget t m => m ()
1536 | bodyElement = do
1537 | el "h2" $ text "Range Input"
1538 | rg <- rangeInput def
1539 | el "p" blank
1540 | display $ _rangeInput_value rg
1541 | return ()
1542 | ~~~
1543 |
1544 | As you can see, the default range goes from 0 to 100.
1545 |
1546 | The next example in *src/range02.hs* allows the user to select numbers in the range of -100 to +100.
1547 | We have to set the minimum attribute to -100.
1548 |
1549 | ~~~ { .haskell }
1550 | {-# LANGUAGE OverloadedStrings #-}
1551 | import Reflex.Dom
1552 | import qualified Data.Text as T
1553 |
1554 | main :: IO ()
1555 | main = mainWidget bodyElement
1556 |
1557 | bodyElement :: MonadWidget t m => m ()
1558 | bodyElement = do
1559 | el "h2" $ text "Range Input"
1560 | rg <- rangeInput $ def & attributes .~ constDyn ("min" =: "-100")
1561 | el "p" blank
1562 | display $ _rangeInput_value rg
1563 | return ()
1564 | ~~~
1565 |
1566 | Depending on your version of *RangeInput*, it does not support the *HasValue* type class. The *HasValue* class was added end of March 2017.
1567 | If your version is older, you cannot use the *value* function. However in all versions you can use the *_rangeInput_value* function!
1568 |
1569 | The next example from *src/range03.hs* allows only numbers from -100 to +100 in steps from 10.
1570 | We add ticks above the values of -30, 0 and 50:
1571 |
1572 | ~~~ { .haskell }
1573 | {-# LANGUAGE OverloadedStrings #-}
1574 | import Reflex.Dom
1575 |
1576 | import Data.Map
1577 | import qualified Data.Text as T
1578 | import Data.Monoid ((<>))
1579 |
1580 | main :: IO ()
1581 | main = mainWidget bodyElement
1582 |
1583 | bodyElement :: MonadWidget t m => m ()
1584 | bodyElement = do
1585 | el "h2" $ text "Range Input"
1586 | rg <- rangeInput $ def & attributes .~ constDyn
1587 | ("min" =: "-100" <> "max" =: "100" <> "value" =: "0" <> "step" =: "10" <> "list" =: "powers" )
1588 | elAttr "datalist" ("id" =: "powers") $ do
1589 | elAttr "option" ("value" =: "0") blank
1590 | elAttr "option" ("value" =: "-30") blank
1591 | elAttr "option" ("value" =: "50") blank
1592 | el "p" blank
1593 | display $ _rangeInput_value rg
1594 | return ()
1595 | ~~~
1596 |
1597 | It generates the following HTML:
1598 |
1599 | ~~~ { .html }
1600 |
1601 |
1606 | ~~~
1607 |
1608 | # Defining your own Elements with Events
1609 |
1610 | ## The Function family *el'*, *elAttr'*, *elClass'*, *elDynAttr'*, *elDynClass'*
1611 |
1612 | In this section of the tutorial, we will look at the second function family to create DOM elements.
1613 | The names of these functions all end with a prime ('). They take the same parameters and work similar as the functions without the prime
1614 | in the name, but they give you more power!
1615 |
1616 | As an example we look at the difference between *el* and *el'*. Similar facts hold for the other functions.
1617 |
1618 | The unprimed version *el* creates a DOM element, but does not give access to the created element:
1619 |
1620 | ~~~ { .haskell }
1621 | el :: DomBuilder t m => Text -> m a -> m a
1622 | ~~~
1623 |
1624 | With the primed function *el'* we get access to the created DOM element:
1625 |
1626 | ~~~ { .haskell }
1627 | el' :: DomBuilder t m => Text -> m a -> m (Element EventResult (DomBuilderSpace m) t, a)
1628 | ~~~
1629 |
1630 | Again the type of *el'* is a little bit scary!
1631 |
1632 | Without going into details, we see that the primed function *el'* takes the same arguments as the unprimed function *el*.
1633 |
1634 | We can simplify the type of *el'* to
1635 |
1636 | el' :: (...) => Text -> m a -> m (*Element*, a)
1637 |
1638 |
1639 | It returns a tuple *(Element, a)*:
1640 | The first value of this tuple is the DOM element, the second value is (ignoring monadic wrapping) the second parameter of the *el'* function.
1641 |
1642 | With access to the DOM element, we are now able to add events to our widgets. The next section shows the details.
1643 |
1644 | ## The function *domEvent*
1645 |
1646 | When we use the unprimed version of *el* to define a button
1647 |
1648 | ```el "button" $ text "Click me"```
1649 |
1650 | we get a button, but when clicked, it will not create any events.
1651 |
1652 | With the return value of the primed version(*el'*), and with the help of the function *domEvent* we are now able to add events to DOM elements:
1653 |
1654 | ~~~ { .haskell }
1655 | button :: DomBuilder t m => Text -> m (Event t ())
1656 | button t = do
1657 | (e, _) <- el' "button" $ text t
1658 | return $ domEvent Click e
1659 | ~~~
1660 |
1661 | Please note, that reflex-dom uses a slightly different definition.
1662 |
1663 | The function *domEvent* takes an event name as a first parameter and an element as a second parameter, and returns an event of a variable type.
1664 |
1665 | Reflex-dom uses some advanced type hackery like TypeFamilies to create events of variable types depending of the event name.
1666 |
1667 | * ```domEvent Click e``` returns an event of type *()*
1668 | * ```domEvent Mousedown e``` returns an event of type *(Int,Int)* with the mouse coordinates.
1669 |
1670 | This is defined in the module [Reflex.Dom.Builder.Class.Events](https://github.com/reflex-frp/reflex-dom/blob/develop/reflex-dom-core/src/Reflex/Dom/Builder/Class/Events.hs):
1671 |
1672 | * The data type *EventName* lists the possible event names.
1673 | * The type family *EventResultType* defines the type of the resulting event.
1674 |
1675 | ## Example: *Disable / enable a Button*
1676 |
1677 | In a lot of web shops you must check a checkbox to accept the business conditions like *"low quality at high prices"*.
1678 | If you don't accept the conditions, the *order* button is disabled and you cannot order!
1679 | We are now able to define the checkbox, the button and the logic to enable or disable the button.
1680 |
1681 | File *src/button01.hs* contains the code:
1682 |
1683 | ~~~ { .haskell }
1684 | {-# LANGUAGE OverloadedStrings #-}
1685 | {-# LANGUAGE ScopedTypeVariables #-}
1686 | import Reflex.Dom
1687 | import qualified Data.Text as T
1688 | import Data.Monoid
1689 |
1690 | main :: IO ()
1691 | main = mainWidget bodyElement
1692 |
1693 | bodyElement :: MonadWidget t m => m ()
1694 | bodyElement = el "div" $ do
1695 | el "h2" $ text "Button enabled / disabled"
1696 | cb <- el "label" $ do
1697 | cb1 <- checkbox True def
1698 | text "Enable or Disable the button"
1699 | return cb1
1700 | el "p" blank
1701 | counter :: Dynamic t Int <- count =<< disaButton (_checkbox_value cb) "Click me"
1702 | el "p" blank
1703 | display counter
1704 |
1705 | -- | A button that can be enabled and disabled
1706 | disaButton :: MonadWidget t m
1707 | => Dynamic t Bool -- ^ enable or disable button
1708 | -> T.Text -- ^ Label
1709 | -> m (Event t ())
1710 | disaButton enabled label = do
1711 | let attrs = ffor enabled $ \e -> monoidGuard (not e) $ "disabled" =: "disabled"
1712 | (btn, _) <- elDynAttr' "button" attrs $ text label
1713 | pure $ domEvent Click btn
1714 |
1715 | -- | A little helper function for data types in the *Monoid* type class:
1716 | -- If the boolean is True, return the first parameter, else return the null or empty element of the monoid
1717 | monoidGuard :: Monoid a => Bool -> a -> a
1718 | monoidGuard p a = if p then a else mempty
1719 | ~~~
1720 |
1721 | The function *disaButton* contains the main logic. It takes a Dynamic Bool, which indicates whether
1722 | the button should be enabled or disabled.
1723 |
1724 | *ffor* is like *fmap* but with flipped parameters: ```ffor :: Functor f => f a -> (a -> b) -> f b```
1725 |
1726 | ## Radio Buttons Revisited
1727 |
1728 | We are now able to write our own function to create radio buttons. File *src/radio02.hs* has the code:
1729 |
1730 | ~~~ { .haskell }
1731 | {-# LANGUAGE OverloadedStrings #-}
1732 | {-# LANGUAGE RecursiveDo #-}
1733 | import Reflex.Dom
1734 | import qualified Data.Text as T
1735 | import qualified Data.Map as Map
1736 | import Data.Monoid((<>))
1737 |
1738 | main :: IO ()
1739 | main = mainWidget bodyElement
1740 |
1741 | bodyElement :: MonadWidget t m => m ()
1742 | bodyElement = el "div" $ do
1743 | rec
1744 | el "h2" $ text "Own Radio buttons"
1745 | let group = "g"
1746 | let dynAttrs = styleMap <$> dynColor
1747 | evRad1 <- radioBtn "orange" group Orange dynAttrs
1748 | evRad2 <- radioBtn "green" group Green dynAttrs
1749 | evRad3 <- radioBtn "red" group Red dynAttrs
1750 | let evRadio = (T.pack . show) <$> leftmost [evRad1, evRad2, evRad3]
1751 | dynColor <- holdDyn "lightgrey" evRadio
1752 | return ()
1753 |
1754 | data Color = White | Red | Orange | Green
1755 | deriving (Eq, Ord, Show)
1756 |
1757 | -- | Helper function to create a radio button
1758 | radioBtn :: (Eq a, Show a, MonadWidget t m) => T.Text -> T.Text -> a -> Dynamic t (Map.Map T.Text T.Text)-> m (Event t a)
1759 | radioBtn label group rid dynAttrs = do
1760 | el "br" blank
1761 | ev <- elDynAttr "label" dynAttrs $ do
1762 | (rb1, _) <- elAttr' "input" ("name" =: group <> "type" =: "radio" <> "value" =: T.pack (show rid)) blank
1763 | text label
1764 | return $ domEvent Click rb1
1765 | return $ rid <$ ev
1766 |
1767 | styleMap :: T.Text -> Map.Map T.Text T.Text
1768 | styleMap c = "style" =: ("background-color: " <> c)
1769 | ~~~
1770 |
1771 | Comments:
1772 |
1773 | * The function *radioBtn* contains the logic to create a radio button.
1774 | * Like the previous radio button example with the function *radioGroup* from the *contrib* library, it uses an user defined datatype as payload for the click event.
1775 | * Maybe you want to add an additonal Boolean parameter to specify whether a radio button is initially checked.
1776 | * Similar to the checkbox example, these radio buttons are userfriendly. You can click on the circle or on the label.
1777 | * Depending on the checked radio button, the background color of the whole radio button changes.
1778 | * Again we use event transformation.
1779 | * We use the reflex function *holdDyn* to convert an Event to a Dynamic value.
1780 |
1781 | # Debugging
1782 |
1783 | ## Tracing Events
1784 |
1785 | In a reflex-dom program a lot of code works with events.
1786 | In big programs you may get lost. To help you out, there are two reflex
1787 | functions *traceEvent* and *traceEventWith*. They allow you to trace events.
1788 |
1789 | The function *traceEvent* has the following type:
1790 |
1791 | ```traceEvent :: (Reflex t, Show a) => String -> Event t a -> Event t a```
1792 |
1793 | It takes a String (not a *Text*!) and an Event with a payload of type *a* and returns a new
1794 | unmodified Event with the same payload.
1795 | The type *a* must have a *Show* instance.
1796 | When the event occurs, it creates a trace entry with the string and the payload of the event using the *show* function.
1797 |
1798 | If the type of your payload is not an instance of *Show* or the string produced by the *show* function
1799 | is far to long you can use the *traceEventWith* function. It has the type:
1800 |
1801 | ```traceEventWith :: Reflex t => (a -> String) -> Event t a -> Event t a```
1802 |
1803 | Instead of using the *Show* instance, you provide a custom function to get a string representation
1804 | of the payload.
1805 |
1806 | In the reflex source code both functions have the following comment:
1807 |
1808 | ```
1809 | -- Note: As with Debug.Trace.trace, the message will only be printed if the
1810 | -- 'Event' is actually used.
1811 | ```
1812 | It's not specified explicitely, but you have to use / consume the **new** event returned by the *traceEvent*/*traceEventWith* function. *Use* or *consume* the event means, that this event or a
1813 | transformed child event must finally have some effect in the DOM.
1814 |
1815 | Let's add tracing to the above radio button example. Here I show only the *bodyElement* function,
1816 | the rest is unmodified. The full code is in *src/trace01.hs*.
1817 |
1818 | ~~~ { .haskell }
1819 | bodyElement = el "div" $ do
1820 | rec
1821 | el "h2" $ text "Some Tracing"
1822 | let group = "g"
1823 | let dynAttrs = styleMap <$> dynColor
1824 | evRad1 <- radioBtn "orange" group Orange dynAttrs
1825 | evRad2 <- radioBtn "green" group Green dynAttrs
1826 | evRad3 <- radioBtn "red" group Red dynAttrs
1827 | let evRadio = (T.pack . show) <$> leftmost [evRad1, evRad2, evRad3]
1828 |
1829 | -- added line:
1830 | let evRadioT = traceEvent ("Clicked rb in group " <> T.unpack group) evRadio
1831 |
1832 | -- modified line: evRadioT instead of evRadio
1833 | dynColor <- holdDyn "lightgrey" evRadioT
1834 |
1835 | return ()
1836 | ~~~
1837 |
1838 | Here is an example of my test output (from WebkitGtk):
1839 |
1840 | ```
1841 | Clicked radio button in group g: "Orange"
1842 | Clicked radio button in group g: "Green"
1843 | Clicked radio button in group g: "Red"
1844 | ```
1845 |
1846 | * If you use a browser, you can see the trace output in your browser-console.
1847 | * If you use WebkitGtk, the trace output is routed to *stderr*.
1848 | * Look at the modified line staring with ```dynColor```: If you change back to the event *evRadio*
1849 | tracing will no longer work. You **have** to use *evRadio**T*** returned by the *traceEvent* function!!
1850 |
1851 | ## Tracing Changes of Dynamics
1852 |
1853 | Similar to Events you can also trace Dynamics.
1854 | The names and types of the functions are:
1855 |
1856 | ```traceDyn :: (Reflex t, Show a) => String -> Dynamic t a -> Dynamic t a```
1857 |
1858 | ```traceDynWith :: Reflex t => (a -> String) -> Dynamic t a -> Dynamic t a```
1859 |
1860 | They work similar to the *traceEvent* and *traceEventWith* functions. Everytime the Dynamic value
1861 | changes a trace entry will be created. Also the new created Dynamic value must finally be used for an effect in the DOM.
1862 |
1863 | # Timers
1864 |
1865 | A timer will send you always an event after a predefined amount of time has expired. Reflex-dom has two timer functions *tickLossy* and *tickLossyFrom*. The function *tickLossyFrom* is used only in applications where you need several parallel timers. Normally you will use *tickLossy*. It will start sending events immediately after the startup of your application. It has the following type
1866 |
1867 | ```tickLossy :: (...) => NominalDiffTime -> UTCTime -> m (Event t TickInfo)```
1868 |
1869 | The types *NominalDiffTime* and *UTCTime* are defined in the basic GHC library *time*. To use them, we need to import Data.Time.
1870 |
1871 | The first parameter *NominalDiffTime* is the length of the time interval between two events. It is measured in seconds. The second parameter is an UTCTime. I never really found out what it's used for.
1872 | You can give an arbitrary data-time field. Normally I use current time.
1873 |
1874 | The result is a series of Events. Their payload is the data structure *TickInfo*:
1875 |
1876 | ~~~ { .haskell }
1877 | data TickInfo
1878 | = TickInfo { _tickInfo_lastUTC :: UTCTime
1879 | -- ^ UTC time immediately after the last tick.
1880 | , _tickInfo_n :: Integer
1881 | -- ^ Number of time periods since t0
1882 | , _tickInfo_alreadyElapsed :: NominalDiffTime
1883 | -- ^ Amount of time already elapsed in the current tick period.
1884 | }
1885 | ~~~
1886 |
1887 | Both functions *tickLossy* and *tickLossyFrom* have the term *lossy* in their name:
1888 | If the system starts running behind, occurrences of events will be dropped rather than buffered.
1889 |
1890 | A simple example is in the file *src/timer01.hs*:
1891 |
1892 | ~~~ { .haskell }
1893 | {-# LANGUAGE OverloadedStrings #-}
1894 | import Reflex.Dom
1895 | import Data.Time
1896 | import Control.Monad.Trans (liftIO)
1897 | import qualified Data.Text as T
1898 |
1899 | main :: IO ()
1900 | main = mainWidget bodyElement
1901 |
1902 | bodyElement :: MonadWidget t m => m()
1903 | bodyElement = do
1904 | el "h2" $ text "A Simple Clock"
1905 | now <- liftIO getCurrentTime
1906 | evTick <- tickLossy 1 now
1907 | let evTime = (T.pack . show . _tickInfo_lastUTC) <$> evTick
1908 | dynText =<< holdDyn "No ticks yet" evTime
1909 | ~~~
1910 |
1911 | # Server Requests
1912 |
1913 | In the next examples we will send requests to a Web server and process the responses in reflex-dom.
1914 |
1915 | Note: **The following examples run only in the browser, but not in WebkitGtk.** There are some security issues with the *same-origin security policy* and with *cross-origin resource sharing* (CORS). Please inform me, if you know how to overcome this issue.
1916 |
1917 | ## Functions and Data Structures to Send Requests and Receive Responses
1918 |
1919 | ### Requests
1920 |
1921 | To send a request we need a data structure called XhrRequest
1922 |
1923 | ~~~ { .haskell }
1924 | data XhrRequest a
1925 | = XhrRequest { _xhrRequest_method :: Text
1926 | , _xhrRequest_url :: Text
1927 | , _xhrRequest_config :: XhrRequestConfig a
1928 | }
1929 | deriving (Show, Read, Eq, Ord, Typeable, Functor)
1930 | ~~~
1931 |
1932 | Again this needs a configuration record:
1933 |
1934 | ~~~ { .haskell }
1935 | data XhrRequestConfig a
1936 | = XhrRequestConfig { _xhrRequestConfig_headers :: Map Text Text
1937 | , _xhrRequestConfig_user :: Maybe Text
1938 | , _xhrRequestConfig_password :: Maybe Text
1939 | , _xhrRequestConfig_responseType :: Maybe XhrResponseType
1940 | , _xhrRequestConfig_sendData :: a
1941 | , _xhrRequestConfig_withCredentials :: Bool
1942 | , _xhrRequestConfig_responseHeaders :: XhrResponseHeaders
1943 | }
1944 | deriving (Show, Read, Eq, Ord, Typeable, Functor)
1945 | ~~~
1946 |
1947 | and again this configuration record is an instance of the type class *Default*
1948 |
1949 | ~~~ { .haskell }
1950 | instance a ~ () => Default (XhrRequestConfig a) where
1951 | def = XhrRequestConfig { _xhrRequestConfig_headers = Map.empty
1952 | , _xhrRequestConfig_user = Nothing
1953 | , _xhrRequestConfig_password = Nothing
1954 | , _xhrRequestConfig_responseType = Nothing
1955 | , _xhrRequestConfig_sendData = ()
1956 | , _xhrRequestConfig_withCredentials = False
1957 | , _xhrRequestConfig_responseHeaders = def
1958 | }
1959 | ~~~
1960 |
1961 | ### Functions *performRequestAsync* and *performRequestAsyncWithError*
1962 |
1963 | To send a request to the server, we use the function
1964 |
1965 | ```performRequestAsync :: (...) => Event t (XhrRequest a) -> m (Event t XhrResponse)```
1966 |
1967 | So we have to pack a XhrRequest into an Event. Of course we'll again use event transformation to accomplish this.
1968 | Sending the request does not block our reflex-dom frontend. When the response arrives from the server, we just get an 'Event t XhrRespone'
1969 |
1970 | The data type XhrResponse is defined as:
1971 |
1972 | ~~~ { .haskell }
1973 | data XhrResponse
1974 | = XhrResponse { _xhrResponse_status :: Word
1975 | , _xhrResponse_statusText :: Text
1976 | , _xhrResponse_response :: Maybe XhrResponseBody
1977 | , _xhrResponse_responseText :: Maybe Text
1978 | , _xhrResponse_headers :: Map Text Text
1979 | }
1980 | deriving (Typeable)
1981 | ~~~
1982 |
1983 | The type *XhrResponseBody* is defined as:
1984 |
1985 | ~~~ { .haskell }
1986 | data XhrResponseBody = XhrResponseBody_Default Text
1987 | | XhrResponseBody_Text Text
1988 | | XhrResponseBody_Blob Blob
1989 | | XhrResponseBody_ArrayBuffer ByteString
1990 | deriving (Eq)
1991 | ~~~
1992 |
1993 | If you want to write rock solid software, you should use the function *performRequestAsyncWithError* instead of *performRequestAsync*.
1994 | It has the following type:
1995 |
1996 | ```performRequestAsyncWithError :: (...) => Event t (XhrRequest a) -> m (Event t (Either XhrException XhrResponse))```
1997 |
1998 | In case of error you get back a XhrException value. It's defined like:
1999 |
2000 | ~~~ { .haskell }
2001 | data XhrException = XhrException_Error
2002 | | XhrException_Aborted
2003 | deriving (Show, Read, Eq, Ord, Typeable)
2004 | ~~~
2005 |
2006 | To keep my examples small, I'll only use *performRequestAsync*.
2007 |
2008 |
2009 | ### Function *getPostBuild*
2010 |
2011 | This is a little helper function we use in the next example. It has the type:
2012 |
2013 | ```getPostBuild :: PostBuild t m => m (Event t ())```
2014 |
2015 | It generates a single event at the time the HTML page has been created. It's similar to the HTML *onload*.
2016 |
2017 |
2018 | # Swiss Meteo Data Example 1
2019 |
2020 | As an example server we'll use a Web service, that returns the measurement data of the last 10 minutes of automatic meteo stations. This Web service has some advantages:
2021 |
2022 | * You can just use this service. It's not necessary to register with your e-mail address.
2023 | * It's very simple: There are only 2 different requests and responses.
2024 | * It returns the data in JSON format.
2025 |
2026 | For the first example we use a dropdown with a fixed list of stations:
2027 |
2028 | * Bern the captal: [http://www.bern.com/en](http://www.bern.com/en)
2029 | * Zurich, the main city: [https://www.zuerich.com/en](https://www.zuerich.com/en)
2030 | * Jungfraujoch, called *Top of Europe* with 3454 meters above sea level really high in the Swiss mountains [https://www.jungfrau.ch/en-gb/](https://www.jungfrau.ch/en-gb/)
2031 | * Zermatt, the world famous mountain resort at the bottom of the Matterhorn: [http://www.zermatt.ch/en](http://www.zermatt.ch/en)
2032 | * Binn, a small and very lovely mountain village, far off mainstream tourism: [http://www.landschaftspark-binntal.ch/en/meta/fotogalerie.php](http://www.landschaftspark-binntal.ch/en/meta/fotogalerie.php)
2033 |
2034 | Every meteo station has a 3-letter code eg "BER" for Bern. We send this 3-letter code to the server and it returns a JSON string wih the measured data. To start, we just show this JSON string as it is, without any nice formatting.
2035 |
2036 | The code is in the file *src/xhr01.hs*:
2037 |
2038 | ~~~ { .haskell }
2039 | {-# LANGUAGE OverloadedStrings #-}
2040 | import Reflex.Dom
2041 | import qualified Data.Text as T
2042 | import qualified Data.Map as Map
2043 | import Data.Maybe (fromMaybe)
2044 | import Data.Monoid ((<>))
2045 |
2046 | main :: IO ()
2047 | main = mainWidget body
2048 |
2049 | body :: MonadWidget t m => m ()
2050 | body = el "div" $ do
2051 | el "h2" $ text "Swiss Meteo Data (raw version)"
2052 | text "Choose station: "
2053 | dd <- dropdown "BER" (constDyn stations) def
2054 | -- Build and send the request
2055 | evStart <- getPostBuild
2056 | let evCode = tagPromptlyDyn (value dd) $ leftmost [ () <$ _dropdown_change dd, evStart]
2057 | evRsp <- performRequestAsync $ buildReq <$> evCode
2058 | -- Display the whole response
2059 | el "h5" $ text "Response Text:"
2060 | let evResult = (fromMaybe "" . _xhrResponse_responseText) <$> evRsp
2061 | dynText =<< holdDyn "" evResult
2062 | return ()
2063 |
2064 | buildReq :: T.Text -> XhrRequest ()
2065 | buildReq code = XhrRequest "GET" ("https://opendata.netcetera.com/smn/smn/" <> code) def
2066 |
2067 | stations :: Map.Map T.Text T.Text
2068 | stations = Map.fromList [("BIN", "Binn"), ("BER", "Bern"), ("KLO", "Zurich airport"), ("ZER", "Zermatt"), ("JUN", "Jungfraujoch")]
2069 | ~~~
2070 |
2071 | Comments:
2072 |
2073 | * We use *tagPromptlyDyn* to lift the 3-letter code from the dropdown onto an event.
2074 | * Then we use this event to create a new event with the XhrRequest.
2075 | * With the function *performRequestAsync* we send this XhrRequest to the server.
2076 | * We send a request immediately after startup and every time the value of the dropdown changes.
2077 | * At the time of this writing, the station *Binn* is not operational. The server returns a code of 204 in the *_xhrResponse_status* field.
2078 |
2079 | # Swiss Meteo Data Example 2
2080 |
2081 | Now we want not just display the raw JSON string. We want to display the meteo data (more or less) nicely.
2082 | We will use a tabbed display to show the returned data. If the station has no data, we will show an error page.
2083 | I'll not reproduce the whole code here. You can find it in the file *src/xhr02.hs*.
2084 |
2085 | ## Handling the Error Case with Event Filtering
2086 |
2087 | From the server we get back responses with an HTTP return code. If this respone code is 200, the server
2088 | returned real meteo data. If the response code has an other value, something strange happend and we issue
2089 | an error message. We need to separate these 2 types of events. Event filtering is the way to do it.
2090 |
2091 | There are 2 functions:
2092 |
2093 | ``` ffilter :: (a -> Bool) -> Event t a -> Event t a ```
2094 |
2095 | ``` fforMaybe :: Event t a -> (a -> Maybe b) -> Event t b ```
2096 |
2097 | The function *ffilter* selects only those events that fullfill the predicate *(a -> Bool)*. Other events are discarded.
2098 | The function *fforMaybe* selects only those events, where the function *(a -> Maybe b)* returns a *Just b* value,
2099 | and does a corresponding event transformation from *a* to *b*.
2100 |
2101 | Separating the good and the bad events is now easy:
2102 |
2103 | ~~~ { .haskell }
2104 | let (evOk, evErr) = checkXhrRsp evRsp
2105 | ~~~
2106 |
2107 | where
2108 |
2109 | ~~~ { .haskell }
2110 | -- | Split up good and bad response events
2111 | checkXhrRsp :: FunctorMaybe f => f XhrResponse -> (f XhrResponse, f XhrResponse)
2112 | checkXhrRsp evRsp = (evOk, evErr)
2113 | where
2114 | evOk = ffilter (\rsp -> _xhrResponse_status rsp == 200) evRsp
2115 | evErr = ffilter (\rsp -> _xhrResponse_status rsp /= 200) evRsp
2116 | ~~~
2117 |
2118 | Remarks:
2119 |
2120 | * Ok, in a professional environnment you would replace the hardcoded number *200* with a nice constant.
2121 | * I asked *ghci* to tell me the type signature of the function *checkXhrRsp*.
2122 | * *Event Filtering* is one of the powerful methods used in reflex-dom programs.
2123 |
2124 | ## Hiding unwanted HTML elements
2125 |
2126 | If the server sent a nice response with a status code of 200 we don't want to show the error page.
2127 | If we receive a response with a bad status code, means different from 200, we don't want to show the data page.
2128 | In the *Dynamic* value *dynPage* we store the page we want to display.
2129 | For this we have our data type *Page* with an enumeration of our pages.
2130 | Again we use *foldDyn* with function application. The last event sets the value of *dynPage*:
2131 |
2132 | ~~~ { .haskell }
2133 | dynPage <- foldDyn ($) PageData $ leftmost [const PageData <$ evOk, const PageError <$ evErr]
2134 | ~~~
2135 |
2136 | For each of our pages we write an own function to display the data. As parameters we use
2137 | the corresponding event and the value *dynPage*:
2138 |
2139 | ~~~ { .haskell }
2140 | pageData evOk dynPage
2141 | pageErr evErr dynPage
2142 | ~~~
2143 |
2144 | Inside the page functions we use the function *visible* to create a dynamic attribute map,
2145 | which contains a visible or hide attribute. The example shows the code for the error page:
2146 |
2147 | ~~~ { .haskell }
2148 | -- | Display the error page
2149 | pageErr :: MonadWidget t m => Event t XhrResponse -> Dynamic t Page -> m ()
2150 | pageErr evErr dynPage = do
2151 | let dynAttr = visible <$> dynPage <*> pure PageError
2152 | elDynAttr "div" dynAttr $ do
2153 | el "h3" $ text "Error"
2154 | ~~~
2155 |
2156 | where
2157 |
2158 | ~~~ { .haskell }
2159 | -- | Helper function to create a dynamic attribute map for the visibility of an element
2160 | visible :: Eq p => p -> p -> Map.Map T.Text T.Text
2161 | visible p1 p2 = "style" =: ("display: " <> choose (p1 == p2) "inline" "none")
2162 | where
2163 | choose True t _ = t
2164 | choose False _ f = f
2165 | ~~~
2166 |
2167 | ## Function *decodeXhrResponse*: Parsing the JSON String
2168 |
2169 | If we receive a response with a status code of 200, we need to parse the JSON string.
2170 | For this we use the popular *aeson* library. We need a data structure that corresponds to the JSON string.
2171 |
2172 | The library *opench-meteo* contains Haskell definitons for the Swiss meteo data and the instances of the
2173 | *FromJSON* type class. You can find this library on [Hackage](http://hackage.haskell.org/package/opench-meteo)
2174 | or on [https://github.com/hansroland/opench/tree/master/meteo](https://github.com/hansroland/opench/tree/master/meteo).
2175 | You must use version 0.2.0.0 or higher. Version 0.1.* used _http_, but the now server expects a _https_ request.
2176 |
2177 | We need to import this library: ```import Data.Meteo.Swiss```.
2178 |
2179 | The function *decodeXhrResponse* converts the response returned by the *performRequestAsync* function to your data type. It has the type:
2180 |
2181 | ```decodeXhrResponse :: FromJSON a => XhrResponse -> Maybe a```
2182 |
2183 | However sometimes you have to give a little bit help to the compiler. The compiler must know the target data type for the
2184 | *decodeXhrResponse* function. In our case we want back a *Event t SmnRecord*.
2185 | Therefore I tried to add the type annotation in a let expression:
2186 |
2187 | ```let evSmnRec :: (Event t SmnRecord) = fmapMaybe decodeXhrResponse evRsp```
2188 |
2189 | However, I couldn't find a syntax, that was accepted by the compiler.
2190 |
2191 | So with *return* I wrap the *fmapMaybe decodeXhrResponse evRsp* into the monad and can use the syntax of the
2192 | *ScopedTypeVariables* language extension:
2193 |
2194 | ```evSmnRec :: (Event t SmnRecord) <- return $ fmapMaybe decodeXhrResponse evRsp```
2195 |
2196 | Note: In our situation the scoped type varaible is not really necessary, because we nicely type annotated all functions.
2197 | However in a lot of situations you may have to give help to the compiler.
2198 |
2199 | The function *decodeXhrResponse* returns a *Maybe (Event t SmnRecord)*.
2200 | Again we are sloppy and ignore error handling.
2201 | Therefore we use *fmapMaybe*.
2202 |
2203 | ## Function *tabDisplay*: Display the data on a tabbed page
2204 |
2205 | Now we need to present the data to the user. We have 2 types of data:
2206 |
2207 | * The Weather Data *SmnRecord*
2208 | * The Station Record *SmnStation*
2209 |
2210 | An event with the SmnRecord we get back from the decodeXhrResponse. An event with the SmnStation record we create with normal event transformation.
2211 |
2212 | ```let evSmnStat = fmapMaybe smnStation evSmnRec```
2213 |
2214 | For each of these data records we will have an own tab on the page. For this we use the function *tabDisplay*.
2215 | It has the following type:
2216 |
2217 | ```tabDisplay :: (...) => T.Text -> T.Text -> Map.Map k (T.Text, m ()) -> m ()```
2218 |
2219 | Instead of real tabs it creates a ```
/
``` HTML list. We then use CSS to transform this list into nice tab headers.
2220 |
2221 | The first *Text* parameter is the CSS class applied to the ```
``` HTML element.
2222 | The second *Text* parameter is the CSS class applied to the currently active ```
``` element.
2223 | The third parameter is a Map from a key *k* to a pair *(T.Text, m ()) -> m ())*. The first component *Text* is the label for the tab header and the function *m ()) -> m ()* in the second component is the function to create the tab data.
2224 | The data type for the key *k* must be an instance of the Haskell type class *Ord*. The tabs will be ordered by the values of *k*.
2225 |
2226 | We need some CSS, so we add ```{-# LANGUAGE TemplateHaskell #-}``` to our list of GHC language exensions and we add
2227 | ```import Data.FileEmbed``` to our import list and use *mainWidgetWithCss* as our main function.
2228 |
2229 | To create the tab display we now use:
2230 |
2231 | ~~~ { .haskell }
2232 | tabDisplay "tab" "tabact" $ tabMap evSmnRec evSmnStat
2233 |
2234 | -- | Create a tabbed display
2235 | tabMap :: MonadWidget t m => Event t SmnRecord -> Event t SmnStation -> Map.Map Int (T.Text, m ())
2236 | tabMap evMeteo evStat = Map.fromList[ (1, ("Station", tabStat evStat)),
2237 | (2, ("MeteoData", tabMeteo evMeteo))]
2238 | ~~~
2239 |
2240 | We need two functions *tabStat* and *tabMeteo* to display the data:
2241 |
2242 | ~~~ { .haskell }
2243 | -- | Create the DOM elements for the Station tab
2244 | tabStat :: MonadWidget t m => Event t SmnStation -> m ()
2245 | tabStat evStat = do
2246 | dispStatField "Code" staCode evStat
2247 | dispStatField "Name" staName evStat
2248 | dispStatField "Y-Coord" (tShow . staCh1903Y) evStat
2249 | dispStatField "X-Coord" (tShow . staCh1903X) evStat
2250 | dispStatField "Elevation" (tShow . staElevation) evStat
2251 | return ()
2252 |
2253 | -- | Create the DOM elements for the Meteo data tab
2254 | tabMeteo :: MonadWidget t m => Event t SmnRecord -> m ()
2255 | tabMeteo evMeteo = do
2256 | dispMeteoField "Date/Time" (tShow . smnDateTime) evMeteo
2257 | dispMeteoField "Temperature" smnTemperature evMeteo
2258 | dispMeteoField "Sunnshine" smnSunshine evMeteo
2259 | dispMeteoField "Precipitation" smnPrecipitation evMeteo
2260 | dispMeteoField "Wind Direction" smnWindDirection evMeteo
2261 | dispMeteoField "Wind Speed" smnWindSpeed evMeteo
2262 | return ()
2263 | ~~~
2264 |
2265 | And again two functions *dispStatField* and *dispStatField* to display the single fields.
2266 | We also need a little helper function to nicely display non text values wrapped in a Maybe.
2267 |
2268 | ~~~ { .haskell }
2269 | -- Display a single field from the SmnStation record
2270 | dispStatField :: MonadWidget t m => T.Text -> (SmnStation -> T.Text) -> Event t SmnStation -> m ()
2271 | dispStatField label rend evStat = do
2272 | el "br" blank
2273 | text $ label <> ": "
2274 | dynText =<< holdDyn "" (fmap rend evStat)
2275 | return ()
2276 |
2277 | -- Display a single field from the SmnRecord record
2278 | dispMeteoField :: MonadWidget t m => T.Text -> (SmnRecord -> T.Text) -> Event t SmnRecord -> m ()
2279 | dispMeteoField label rend evRec = do
2280 | el "br"blank
2281 | text $ label <> ": "
2282 | dynText =<< holdDyn "" (fmap rend evRec)
2283 | return ()
2284 |
2285 | -- | Small helper function to convert showable values wrapped in Maybe to T.Text.
2286 | -- You should use the text-show library from Hackage!!
2287 | tShow :: Show a => Maybe a -> T.Text
2288 | tShow Nothing = ""
2289 | tShow (Just x) = (T.pack . show) x
2290 | ~~~
2291 |
2292 | Note: With the function *tabDisplay* the number of tabs is fixed. You cannot change it during run time.
2293 |
2294 | # Swiss Meteo Data Example 3
2295 |
2296 | The code is in file *src/xhr03.hs*.
2297 |
2298 | Till now we only had our 5 fix predefined meteo stations. Now we want a first page with a list of all stations,
2299 | a second page with the detailed data of one selected station and an error page. We need a XhrRequest to get a list
2300 | of all stations.
2301 | The page with the detailed data of a single station and the error page both need a *Back* button,
2302 | so the user can return to the first page with the station list, and therefore the page rendering functions have to
2303 | return the click events of these *Back* buttons.
2304 |
2305 | We do all this in the function *body*. The additonal events and the two XhrRequests make the logic
2306 | of this function a little bit more complicated.
2307 | Because we refer to events defined later, we have to use *recursive do*. In the function *body* we don't use
2308 | any new function or programming concept!
2309 |
2310 | A word of caution to *recursive do*: It is possible to create event loops that will blow up your program! So be careful!
2311 |
2312 | ## Function *switchPromptlyDyn*
2313 |
2314 | Reflex-Dom has a helper function *switchPromptlyDyn* with the following type:
2315 |
2316 | ```switchPromptlyDyn :: Reflex t => Dynamic t (Event t a) -> Event t a```
2317 |
2318 | It just unwraps an Event out of a Dynamic.
2319 |
2320 | ## Function *simpleList*
2321 |
2322 | The real new thing in this example is: We have a variable number of meteo stations to display on the first page!
2323 | All our examples so far displayed a static predefined number of HTML elements.
2324 | Reflex-dom has several functions to display a variable number of items: *dyn*, *widgetHold*, *listWithKey*,
2325 | *list*, *simpleList*, *listViewWithKey*, *selectViewListWithKey* and *listWithKey'*. We use the function
2326 | *simpleList*. It has the following definition:
2327 |
2328 | ```(...) => Dynamic t [v] -> (Dynamic t v -> m a) -> m (Dynamic t [a])```
2329 |
2330 | The first parameter *Dynamic t [v]* is a dynamic list of items we want to render. After we parse the JSON string
2331 | we have a value of type *Event t [SmnRecord]*. With *holdDyn* we can convert this to *Dynamic t [SmnRecord]*,
2332 | which can be use as first parameter for the function *simpleList*.
2333 |
2334 | The second parameter is a function to render a single element of type *v*, or in our case *SmnRecord*. For each
2335 | station record, we render a *View* button, and the name of the station.
2336 | We pack this into a row of a HTML table, so we need *tr* and *td* HTML elements.
2337 |
2338 | This is done in the function *displayStationRow* with a helper function to create the button:
2339 |
2340 | ~~~ { .haskell }
2341 | -- | Create the HTML element for a single HTML table row
2342 | displayStationRow :: MonadWidget t m => Dynamic t SmnRecord -> m (Event t T.Text)
2343 | displayStationRow dynRec = el "tr" $ do
2344 | evRow <- el "td" $ cmdButton "View" dynRec
2345 | el "td" $ dynText $ staName . fromJust . smnStation <$> dynRec
2346 | return evRow
2347 |
2348 | cmdButton :: MonadWidget t m => T.Text -> Dynamic t SmnRecord -> m (Event t T.Text)
2349 | cmdButton label staRec = do
2350 | (btn, _) <- el' "button" $ text label
2351 | let dynNam = smnCode <$> staRec
2352 | return $ tagPromptlyDyn dynNam $ domEvent Click btn
2353 | ~~~
2354 |
2355 | When the user clicks on one of the buttons, we need to know, to which station this button belongs.
2356 | Therefore clicking on the *view* button, returns an event with the 3-letter code of the station as payload.
2357 |
2358 | The function *simpleList* aggregates all the return values of the rendering function into a list.
2359 |
2360 | So in our case, the function *simpleList* has the type:
2361 |
2362 | ```(...) => Dynamic t [SmnRecord] -> (Dynamic t SmnRecord -> m (Event t T.Text)) -> m (Dynamic t [Event t T.Text])```
2363 |
2364 | We then use the function *switchPromptlyDyn* to unwrap the list of events out of the Dynamic.
2365 | The function *leftmost* then returns the event of the *view* button the user clicked.
2366 | We use this event in the function *body* to fetch the detailed (and possibly changed!) data of this station.
2367 |
2368 | Here is the code:
2369 |
2370 | ~~~ { .haskell }
2371 | -- list stations
2372 | el "table" $ do -- put everything into a HTML table
2373 | dynList :: Dynamic t [SmnRecord] <- holdDyn [] evList -- prepare the first argument for simpleList
2374 | evRowsDyn <- simpleList dynList displayStationRow -- render all station records
2375 | return $ switchPromptlyDyn $ leftmost <$> evRowsDyn -- get the correct click event
2376 | ~~~
2377 |
2378 | Look at *src/xhr03.hs*: Without the comment lines and without the empty lines we have less than 150 lines of code.
2379 |
2380 | **In these 150 LoC we have a SPA application that fetches data from a server,
2381 | displays it on 2 data pages. One of the data pages has a tabbed display and we have also some error handling!!**
2382 |
2383 | Of course, there is room for improvment eg sort the station names on the first page, improve error handling,
2384 | use CSS to make it easier to look at the screens etc, etc.
2385 |
2386 |
2387 | # Appendix - References to Reflex-Dom Resources on the Internet
2388 |
2389 | ## Talks / Videos
2390 |
2391 | * Ryan Trinkle: Reflex - Practical Functional Reactive Programming, NYC Haskell User's Group, Part 1 [https://www.youtube.com/watch?v=mYvkcskJbc4](https://www.youtube.com/watch?v=mYvkcskJbc4)
2392 | * Ryan Trinkle: Reflex - Practical Functional Reactive Programming, NYC Haskell User's Group, Part 2 [https://www.youtube.com/watch?v=3qfc9XFVo2c](https://www.youtube.com/watch?v=3qfc9XFVo2c)
2393 | * Niklas Hambüchen: FRP browser programming with Reflex, HaskellerZ meetup, Zürich [https://www.youtube.com/watch?v=dNGClNsnn24&t=2s](https://www.youtube.com/watch?v=dNGClNsnn24&t=2s)
2394 | * Doug Beardsley: Modular Web Snippets with Reflex [https://www.youtube.com/watch?v=8nMC2jL2iUY](https://www.youtube.com/watch?v=8nMC2jL2iUY)
2395 | * Greg Hale: On Reflex - Boston Haskell [https://www.youtube.com/watch?v=MfXxuy_CJSk](https://www.youtube.com/watch?v=MfXxuy_CJSk)
2396 | * Doug Beardsley: Real Word Reflex [http://mightybyte.net/real-world-reflex/index.html](http://mightybyte.net/real-world-reflex/index.html)
2397 |
2398 | ## Main Libraries
2399 |
2400 | * The official reflex repositories: [https://github.com/reflex-frp/](https://github.com/reflex-frp/)
2401 |
2402 | ## Documentation
2403 |
2404 | * Reflex [http://reflex-frp.github.io/reflex-frp.org/](http://reflex-frp.github.io/reflex-frp.org/)
2405 |
2406 | ## Cheat Sheets
2407 |
2408 | * Cheat Sheet Reflex [https://github.com/reflex-frp/reflex/blob/develop/Quickref.md](https://github.com/reflex-frp/reflex/blob/develop/Quickref.md)
2409 | * Cheat Sheet Reflex.Dom [https://github.com/reflex-frp/reflex-dom/blob/develop/Quickref.md](https://github.com/reflex-frp/reflex-dom/blob/develop/Quickref.md)
2410 |
2411 | ## Reddit
2412 |
2413 | * [https://www.reddit.com/r/reflexfrp/new/](https://www.reddit.com/r/reflexfrp/new/)
2414 |
2415 | ## Stackoverflow
2416 |
2417 | * Questions tagged reflex [http://stackoverflow.com/questions/tagged/reflex?sort=newest&pageSize=15](http://stackoverflow.com/questions/tagged/reflex?sort=newest&pageSize=15)
2418 |
2419 | ## Tutorials
2420 |
2421 | * Queensland FP Lab blog series: An Introduction to reflex [https://qfpl.io/posts/reflex/basics/introduction/](https://qfpl.io/posts/reflex/basics/introduction/)
2422 |
2423 |
2424 | ## Other Libraries / Projects
2425 |
2426 | * A 2048 clone: [https://github.com/mightybyte/reflex-2048/blob/master/src/Main.hs](https://github.com/mightybyte/reflex-2048/blob/master/src/Main.hs)
2427 | * HSnippet: [https://github.com/mightybyte/hsnippet](https://github.com/mightybyte/hsnippet)
2428 | * [https://github.com/imalsogreg/my-reflex-recipes](https://github.com/imalsogreg/my-reflex-recipes)
2429 | * [https://github.com/themoritz/7guis-reflex](https://github.com/themoritz/7guis-reflex)
2430 | * [https://github.com/emmanueltouzery/cigale-timesheet](https://github.com/emmanueltouzery/cigale-timesheet)
2431 | * [http://emmanueltouzery.github.io/reflex-presentation]()
2432 |
--------------------------------------------------------------------------------