├── 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 | ![Events](https://github.com/hansroland/reflex-dom-inbits/raw/master/images//event.png "Events") 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 | ![Behavior](https://github.com/hansroland/reflex-dom-inbits/raw/master/images//behavior.png "Behavior") 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 | ![Behavior](https://github.com/hansroland/reflex-dom-inbits/raw/master/images//dynamic.png "Behavior") 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 | 1602 | 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 | --------------------------------------------------------------------------------