react-flux 1.0.0 → 1.0.1
raw patch · 24 files changed
+666/−89 lines, 24 filesdep ~aesondep ~basenew-component:exe:purecss-side-menu
Dependency ranges changed: aeson, base
Files
- ChangeLog.md +14/−0
- example/README.md +31/−1
- example/purecss-side-menu/App.hs +55/−0
- example/purecss-side-menu/Dispatcher.hs +11/−0
- example/purecss-side-menu/Main.hs +10/−0
- example/purecss-side-menu/NavStore.hs +92/−0
- example/purecss-side-menu/PageViews.hs +38/−0
- example/purecss-side-menu/index.html +16/−0
- example/todo/TodoViews.hs +8/−11
- example/todo/todo.html +1/−1
- jsbits/views.js +6/−0
- react-flux.cabal +20/−1
- src/React/Flux.hs +5/−3
- src/React/Flux/Addons/Intl.hs +123/−12
- src/React/Flux/Combinators.hs +117/−0
- src/React/Flux/Internal.hs +12/−2
- src/React/Flux/PropertiesAndEvents.hs +38/−22
- src/React/Flux/Views.hs +9/−32
- test/client/TestClient.hs +2/−1
- test/client/TestClient14.hs +36/−1
- test/client/msgs/android.xml +3/−0
- test/client/msgs/jsonmsgs.json +1/−1
- test/client/msgs/jsonnodescr.json +1/−1
- test/spec/TestClientSpec.hs +17/−0
ChangeLog.md view
@@ -1,3 +1,17 @@+# 1.0.1++* Add formatting support for properties to `React.Flux.Addons.Intl`. These are needed for example to translate+ the placeholder text for an input element. This improvement caused a few changes to the types the Internal module.++* Add a new example [purecss-side-menu](https://bitbucket.org/wuzzeb/react-flux/src/tip/example/purecss-side-menu)+ showing a responsive side menu built with PureCSS.++* Add `classNames` function to `React.Flux.PropertiesAndEvents` to allow easily setting class names+ based on calculations.++* Add a new module `React.Flux.Combinators` which is re-exported by `React.Flux`. The `Combinators` module+ contains useful utilities that while not required, make your life a little simpler.+ # 1.0.0 * Bindings to react-intl (http://formatjs.io/react/) for i18n support. This is useful even if your app is
example/README.md view
@@ -57,9 +57,39 @@ contains an [hspec-webdriver](https://hackage.haskell.org/package/hspec-webdriver) spec for the TODO example application. +# PureCSS++The second example application shows building a [responsive side+menu](http://purecss.io/layouts/side-menu/) using [PureCSS](http://purecss.io/). A similar+technique with slightly different CSS classes can be used to create any of the menu+[layouts](http://purecss.io/layouts/). The code is ogranzied as:++* `NavStore.hs` contains a store which stores the current page being viewed and a boolean if the+ responsive side menu is open or closed.+* `Dispatcher.hs` contains a function `changePageTo` which allows changing the page. In a larger+ application, `Dispatcher.hs` should also contain functions to dispatch to the other stores+ containing the actual page data.+* `PageViews.hs` contains the actual page content. In a real application, each page should probably+ be split into its own module and be a controller view for the content store.+* `App.hs` contains the layout for the entire application, with the navigation bar and header.+* `Main.hs` renders the application into the DOM.++It is also built with `-fexample`. It uses the browser [history+API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) which does not work if you open `index.html` directly from the filesystem. Instead, the index.html file must be served.++~~~+cabal configure -fexample+cabal build+cd example/purecss-side-menu+ln -s ../../dist/build/purecss-side-menu/purecss-side-menu.jsexe/all.js purecss-side-menu.js+python3 -m http.server 8000+~~~++Then open your browser to `localhost:8000`.+ # Routing Example -The second example application shows routing with the [web-routes](https://hackage.haskell.org/package/web-routes) package.+The third example application shows routing with the [web-routes](https://hackage.haskell.org/package/web-routes) package. It is also built by passing `-fexample` to cabal. ~~~
@@ -0,0 +1,55 @@+{-# LANGUAGE OverloadedStrings #-}+module App (myApp) where++import React.Flux+import NavStore+import Dispatcher+import PageViews++-- | Each page rendered by the application becomes an instance of this structure.+-- I define and fill this here inside App.hs instead of in each individual page view so+-- that the navigation definition can control how the pages are displayed and organized (dropdowns,+-- categories, etc.) in the menu.+data Page = Page+ { pageId :: NavPageId+ , pageTitle :: ReactElementM ViewEventHandler ()+ -- ^ the title showed in the sidebar menu+ , pageContent :: ReactView ()+ -- ^ the content of the page+ }++-- | Convert a page id to a page+pageFor :: NavPageId -> Page+pageFor Page1 = Page Page1 "Page 1" page1+pageFor Page2 = Page Page2 "Page 2" page2+pageFor Page3 = Page Page3 "Page 3" page3++-- | A single menu item entry in the sidebar.+menuItem_ :: NavPageId -> NavPageId -> ReactElementM ViewEventHandler ()+menuItem_ curPageId linkPageId =+ let linkPage = pageFor linkPageId+ in+ li_ [classNames [("pure-menu-item", True), ("pure-menu-selected", curPageId == pageId linkPage)]] $+ a_ ["className" $= "pure-menu-link", onClick $ \_ _ -> changePageTo $ pageId linkPage] $+ pageTitle linkPage++-- | The navigation menu+navMenu_ :: NavPageId -> ReactElementM ViewEventHandler ()+navMenu_ curPageId =+ cldiv_ "pure-menu" $ do+ span_ ["className" $= "pure-menu-heading"] "My Brand"+ ul_ ["className" $= "pure-menu-list"] $+ mapM_ (menuItem_ curPageId) allPageIds++-- | The entire layout of the app, consisting of the menu and the main content section.+myApp :: ReactView ()+myApp = defineControllerView "my application" currentNavPageStore $ \navState () -> do+ div_ ["id" $= "layout", classNames [("active", sideMenuOpen navState)]] $ do+ a_ ["id" $= "menuLink"+ , classNames [("menu-link", True), ("active", sideMenuOpen navState)]+ , onClick $ \_ _ -> [SomeStoreAction currentNavPageStore ToggleSideMenu]+ ] $ span_ mempty+ div_ ["id" $= "menu", classNames [("active", sideMenuOpen navState)]] $+ navMenu_ $ currentPageId navState+ div_ ["id" $= "main"] $+ view (pageContent $ pageFor $ currentPageId navState) () mempty
@@ -0,0 +1,11 @@+module Dispatcher (+ -- re-export NavPageId so views don't have to import NavStore directly+ NavPageId(..)+ , changePageTo+) where++import React.Flux+import NavStore++changePageTo :: NavPageId -> [SomeStoreAction]+changePageTo p = [SomeStoreAction currentNavPageStore $ ChangePageTo p]
@@ -0,0 +1,10 @@+module Main (main) where++import React.Flux+import App+import NavStore (initHistory)++main :: IO ()+main = do+ initHistory+ reactRender "side-menu-app" myApp ()
@@ -0,0 +1,92 @@+{-# LANGUAGE TypeFamilies, DeriveGeneric, DeriveAnyClass, OverloadedStrings #-}+-- | A store to hold the current page being viewed.+module NavStore where++import Control.DeepSeq (NFData)+import Data.Maybe (fromMaybe)+import Data.Typeable (Typeable)+import GHC.Generics (Generic)+import React.Flux++import GHCJS.Types (JSRef, JSString)+import GHCJS.Foreign.Callback (Callback, syncCallback1, OnBlocked(..))+import GHCJS.Marshal (fromJSRef)+import qualified Data.JSString as JSString++data NavPageId = Page1+ | Page2+ | Page3+ deriving (Show, Eq, Enum, Bounded, Typeable, Generic, NFData)++-- | The page title (used for `document.title`) and the url (used for the history API and displayed+-- in the browser location bar).+pageTitleAndUrl :: NavPageId -> (String, String)+pageTitleAndUrl Page1 = ("Page 1 Title", "/page1")+pageTitleAndUrl Page2 = ("Page 2222", "/pages/page2")+pageTitleAndUrl Page3 = ("Page Three Title", "/page3.html")++allPageIds :: [NavPageId]+allPageIds = [(minBound :: NavPageId) .. ]++data NavAction = ChangePageTo NavPageId -- ^ use to change the page+ | BackToPage NavPageId -- ^ used from the history handler to go back+ | ToggleSideMenu+ deriving (Show, Eq, Typeable, Generic, NFData)++data NavState = NavState+ { sideMenuOpen :: Bool+ , currentPageId :: NavPageId+ } deriving (Show, Eq, Generic, Typeable)++instance StoreData NavState where+ type StoreAction NavState = NavAction+ transform (ChangePageTo p) _ = do+ historyPushState p+ setDocTitle p+ return NavState { currentPageId = p, sideMenuOpen = False }+ transform (BackToPage p) _ = do+ setDocTitle p+ return NavState { currentPageId = p, sideMenuOpen = False }+ transform ToggleSideMenu s =+ return $ s { sideMenuOpen = not (sideMenuOpen s) } -- use a lens!++currentNavPageStore :: ReactStore NavState+currentNavPageStore = mkStore (NavState False Page1)++--------------------------------------------------------------------------+--- History API+--------------------------------------------------------------------------++foreign import javascript unsafe+ "window['history']['pushState']({page: $1}, $2)"+ js_historyPushState :: Int -> JSString -> IO ()++historyPushState :: NavPageId -> IO ()+historyPushState pid = do+ let (_, url) = pageTitleAndUrl pid+ js_historyPushState (fromEnum pid) $ JSString.pack url++foreign import javascript unsafe+ "document['title'] = $1;"+ js_setDocTitle :: JSString -> IO ()++setDocTitle :: NavPageId -> IO ()+setDocTitle pid = do+ let (title, _) = pageTitleAndUrl pid+ js_setDocTitle $ JSString.pack title++foreign import javascript unsafe+ "window['onpopstate'] = function(e) { $1(e['state'] ? e['state'].page : 0); };"+ js_setOnPopState :: Callback (JSRef -> IO ()) -> IO ()++initHistory :: IO ()+initHistory = do+ -- set the document title to match page1+ let (title, _) = pageTitleAndUrl Page1+ js_setDocTitle $ JSString.pack title++ -- register a callback for onpopstate event+ c <- syncCallback1 ContinueAsync $ \pageRef -> do+ pageInt <- fromMaybe (error "Unable to parse page") <$> fromJSRef pageRef+ alterStore currentNavPageStore $ BackToPage $ toEnum pageInt+ js_setOnPopState c
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}++-- | The pages. Normally each page should go into its own module, but for simplicity in this+-- example they are all defined here.+module PageViews where++import React.Flux+import Dispatcher++page1 :: ReactView ()+page1 = defineView "page 1" $ \() -> div_ $ do+ cldiv_ "header" $+ h1_ "Page 1!!"+ cldiv_ "content" $ do+ p_ $ do+ "Page 1 content. "+ clbutton_ "pure-button" (changePageTo Page2) "Change to page 2"+ "Also, try reducing the browser width to see the responsive menu."+ p_ $ do+ "You must load this file from a server. If you did not, no page changes will work. Use for example "+ code_ "python3 -m http.server 8000"+ " or "+ code_ "python2 -m SimpleHTTPServer 8000"+ " from the example/purecss-side-menu directory."++page2 :: ReactView ()+page2 = defineView "page 2" $ \() -> div_ $ do+ cldiv_ "header" $+ h1_ "Just page 2"+ cldiv_ "content" $+ p_ "Page 2 content. Try the back button."++page3 :: ReactView ()+page3 = defineView "page 3" $ \() -> div_ $ do+ cldiv_ "header" $+ h1_ "Page Three"+ cldiv_ "content" $+ p_ "Page 3 content. Try the back button."
@@ -0,0 +1,16 @@+<!doctype html>+<html lang="en">+ <head>+ <meta charset="utf-8">+ <meta name="viewport" content="width=device-width, initial-scale=1.0">+ <title>Responsive Side Menu</title>+ <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">+ <link rel="stylesheet" href="http://purecss.io/css/layouts/side-menu.css">+ </head>+ <body>+ <div id="side-menu-app"/>+ <script src="https://fb.me/react-0.14.0-rc1.min.js"></script>+ <script src="https://fb.me/react-dom-0.14.0-rc1.min.js"></script>+ <script src="purecss-side-menu.js"></script>+ </body>+</html>
example/todo/TodoViews.hs view
@@ -40,13 +40,12 @@ -- inside the rendering function. mainSection_ :: TodoState -> ReactElementM ViewEventHandler () mainSection_ st = section_ ["id" $= "main"] $ do- input_ [ "id" $= "toggle-all"- , "type" $= "checkbox"- , "checked" $= if all (todoComplete . snd) $ todoList st then "checked" else ""- , onChange $ \_ -> dispatchTodo ToggleAllComplete- ]+ labeledInput_ "toggle-all" "Mark all as complete"+ [ "type" $= "checkbox"+ , "checked" $= if all (todoComplete . snd) $ todoList st then "checked" else ""+ , onChange $ \_ -> dispatchTodo ToggleAllComplete+ ] - label_ [ "htmlFor" $= "toggle-all"] "Mark all as complete" ul_ [ "id" $= "todo-list" ] $ mapM_ todoItem_ $ todoList st -- | A view for each todo item. We specifically use a ReactView here to take advantage of the@@ -56,11 +55,11 @@ -- section of the React.Flux documentation. todoItem :: ReactView (Int, Todo) todoItem = defineView "todo item" $ \(todoIdx, todo) ->- li_ [ "className" @= (unwords $ [ "completed" | todoComplete todo] ++ [ "editing" | todoIsEditing todo ])+ li_ [ classNames [("completed", todoComplete todo), ("editing", todoIsEditing todo)] , "key" @= todoIdx ] $ do - div_ [ "className" $= "view"] $ do+ cldiv_ "view" $ do input_ [ "className" $= "toggle" , "type" $= "checkbox" , "checked" @= todoComplete todo@@ -70,9 +69,7 @@ label_ [ onDoubleClick $ \_ _ -> dispatchTodo $ TodoEdit todoIdx] $ elemText $ todoText todo - button_ [ "className" $= "destroy"- , onClick $ \_ _ -> dispatchTodo $ TodoDelete todoIdx- ] mempty+ clbutton_ "destroy" (dispatchTodo $ TodoDelete todoIdx) mempty when (todoIsEditing todo) $ todoTextInput_ TextInputArgs
example/todo/todo.html view
@@ -8,7 +8,7 @@ <body> <section id="todoapp"/> <script src="https://fb.me/react-0.14.0-rc1.js"></script>- <script src=" https://fb.me/react-dom-0.14.0-rc1.js"></script>+ <script src="https://fb.me/react-dom-0.14.0-rc1.js"></script> <script src="js-build/todo.min.js"></script> </body> </html>
jsbits/views.js view
@@ -41,6 +41,12 @@ return this['props'].hs.root != newProps.hs.root; }; }++ if (typeof ReactIntl != "undefined") {+ cl['contextTypes'] = {+ 'intl': ReactIntl['intlShape']+ };+ } return cl; }
react-flux.cabal view
@@ -1,5 +1,5 @@ name: react-flux-version: 1.0.0+version: 1.0.1 synopsis: A binding to React based on the Flux application architecture for GHCJS category: Web homepage: https://bitbucket.org/wuzzeb/react-flux@@ -22,6 +22,8 @@ example/todo/run-in-node.js, example/routing/*.hs, example/routing/*.html,+ example/purecss-side-menu/*.hs,+ example/purecss-side-menu/*.html, test/client/*.html, test/client/msgs/*.json, test/client/msgs/*.xml,@@ -53,6 +55,7 @@ exposed-modules: React.Flux React.Flux.DOM React.Flux.PropertiesAndEvents+ React.Flux.Combinators React.Flux.Lifecycle React.Flux.Internal React.Flux.Addons.React@@ -115,6 +118,21 @@ , deepseq , text +executable purecss-side-menu+ if !flag(example)+ Buildable: False+ ghc-options: -Wall+ cpp-options: -DGHCJS_BROWSER++ default-language: Haskell2010+ hs-source-dirs: example/purecss-side-menu+ main-is: Main.hs+ build-depends: base+ , react-flux+ , deepseq+ if impl(ghcjs)+ build-depends: ghcjs-base+ executable test-client-13 if !flag(test-client) Buildable: False@@ -146,6 +164,7 @@ , react-flux , time , deepseq+ , aeson if impl(ghcjs) build-depends: ghcjs-base
src/React/Flux.hs view
@@ -85,6 +85,7 @@ , foreignClass , module React.Flux.DOM , module React.Flux.PropertiesAndEvents+ , module React.Flux.Combinators -- * Main , reactRender@@ -101,10 +102,11 @@ import React.Flux.DOM import React.Flux.Internal import React.Flux.PropertiesAndEvents+import React.Flux.Combinators import React.Flux.Store #ifdef __GHCJS__-import GHCJS.Types (JSString, JSRef)+import GHCJS.Types (JSString, JSRef, nullRef) import GHCJS.Marshal (fromJSRef) #endif @@ -123,7 +125,7 @@ #ifdef __GHCJS__ reactRender htmlId rc props = do- (e, _) <- mkReactElement id (return []) $ view rc props mempty+ (e, _) <- mkReactElement id (return nullRef) (return []) $ view rc props mempty js_ReactRender e (toJSString htmlId) foreign import javascript unsafe@@ -149,7 +151,7 @@ #ifdef __GHCJS__ reactRenderToString includeStatic rc props = do- (e, _) <- mkReactElement id (return []) $ view rc props mempty+ (e, _) <- mkReactElement id (return nullRef) (return []) $ view rc props mempty sRef <- (if includeStatic then js_ReactRenderStaticMarkup else js_ReactRenderToString) e --return sRef --return $ JSS.unpack sRef
src/React/Flux/Addons/Intl.hs view
@@ -96,6 +96,7 @@ , int_ , double_ , formattedNumber_+ , formattedNumberProp -- ** Dates and Times , DayFormat(..)@@ -105,18 +106,23 @@ , shortDateTime , utcTime_ , formattedDate_+ , formattedDateProp -- ** Relative Times , relativeTo_ , formattedRelative_+ , formattedRelativeProp -- ** Plural , plural_+ , pluralProp -- ** Messages , MessageId , message , message'+ , messageProp+ , messageProp' , htmlMsg , htmlMsg' @@ -129,7 +135,8 @@ ) where import Control.Monad (when, forM_)-import Data.Aeson (Object, Value(Object))+import Data.Aeson (Object, Value(Object), object, (.=))+import Data.Aeson.Types (Pair) import Data.Char (ord, isPrint) import Data.List (sortBy) import Data.Maybe (catMaybes, fromMaybe)@@ -139,6 +146,7 @@ import Language.Haskell.TH (runIO, Q, Loc, location, ExpQ) import Language.Haskell.TH.Syntax (liftString, qGetQ, qPutQ, reportWarning, Dec) import React.Flux+import React.Flux.Internal (PropertyOrHandler(PropertyFromContext)) import System.IO (withFile, IOMode(..)) import qualified Data.Aeson as Aeson import qualified Data.ByteString as B@@ -149,7 +157,9 @@ #ifdef __GHCJS__ -import GHCJS.Types (JSRef)+import GHCJS.Types (JSRef, JSString)+import GHCJS.Marshal (ToJSRef(..))+import qualified Data.JSString as JSS foreign import javascript unsafe "$r = ReactIntl['IntlProvider']"@@ -202,6 +212,23 @@ (sec, fracSec) = properFraction pSec micro = round $ fracSec * 1000000 ++foreign import javascript unsafe+ "$1['intl'][$2]($3, $4)"+ js_callContextAPI :: JSRef -> JSString -> JSRef -> JSRef -> IO JSRef+++data ContextApiCall a = ContextApiCall String a [Pair] JSRef++instance ToJSRef a => ToJSRef (ContextApiCall a) where+ toJSRef (ContextApiCall name a b ctx) = do+ aRef <- toJSRef a+ bRef <- toJSRef $ object b+ js_callContextAPI ctx (JSS.pack name) aRef bRef++formatCtx :: ToJSRef a => String -> String -> a -> [Pair] -> PropertyOrHandler handler+formatCtx name func val options = PropertyFromContext name $ ContextApiCall func val options+ #else type JSRef = ()@@ -233,6 +260,11 @@ timeToRef :: UTCTime -> JSRef timeToRef _ = () +class ToJSRef a++formatCtx :: String -> String -> a -> [Pair] -> PropertyOrHandler handler+formatCtx name _ _ _ = PropertyFromContext name $ \() -> ()+ #endif -- | Use the IntlProvider to set the @locale@, @formats@, and @messages@ property.@@ -275,6 +307,18 @@ formattedNumber_ :: [PropertyOrHandler eventHandler] -> ReactElementM eventHandler () formattedNumber_ props = foreignClass js_formatNumber props mempty +-- | Format a number as a string, and then use it as the value for a property. 'int_', 'double_',+-- or 'formattedNumber_' should be prefered because as components they can avoid re-rendering when+-- the number has not changed. 'formattedNumberProp' is needed if the formatted number has to be+-- a property on another element, such as the placeholder for an input element.+formattedNumberProp :: ToJSRef num+ => String -- ^ the property to set+ -> num -- ^ the number to format+ -> [Pair] -- ^ any options accepted by+ -- <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat Intl.NumberFormat>+ -> PropertyOrHandler handler+formattedNumberProp name x options = formatCtx name "formatNumber" x options+ -------------------------------------------------------------------------------- -- Date/Time --------------------------------------------------------------------------------@@ -371,6 +415,19 @@ where valProp = property "value" $ either dayToRef timeToRef t +-- | Format a day or time as a string, and then use it as the value for a property. 'day_',+-- 'utcTime_', or 'formattedDate_' should be prefered because as components they can avoid re-rendering when+-- the date has not changed. 'formattedDateProp' is needed if the formatted date has to be+-- a property on another element, such as the placeholder for an input element.+formattedDateProp :: String -- ^ the property to set+ -> Either Day UTCTime -- ^ the day or time to format+ -> [Pair] -- ^ Any options supported by+ -- <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat Intl.DateTimeFormat>.+ -> PropertyOrHandler eventHandler+formattedDateProp name (Left day) options =+ formatCtx name "formatDate" (dayToRef day) options+formattedDateProp name (Right time) options =+ formatCtx name "formatTime" (timeToRef time) options -- | Display the 'UTCTime' as a relative time. In addition, wrap the display in a HTML5 -- <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time time> element.@@ -386,6 +443,17 @@ formattedRelative_ :: UTCTime -> [PropertyOrHandler eventHandler] -> ReactElementM eventHandler () formattedRelative_ t props = foreignClass js_formatRelative (property "value" (timeToRef t) : props) mempty +-- | Format a time as a relative time string, and then use it as the value for a property.+-- 'relativeTo_' or 'formattedRelative_' should be prefered because as components they can avoid re-rendering when+-- the date has not changed. 'formattedRelativeProp' is needed if the formatted date has to be+-- a property on another element, such as the placeholder for an input element.+formattedRelativeProp :: String -- ^ te property to set+ -> UTCTime -- ^ the time to format+ -> [Pair] -- ^ an object with properties \"units\" and \"style\". \"units\" accepts values second, minute, hour+ -- day, month, or year and \"style\" accepts only the value \"numeric\".+ -> PropertyOrHandler eventHandler+formattedRelativeProp name time options = formatCtx name "formatRelative" (timeToRef time) options+ -------------------------------------------------------------------------------- -- Plural --------------------------------------------------------------------------------@@ -397,6 +465,12 @@ plural_ :: [PropertyOrHandler eventHandler] -> ReactElementM eventHandler () plural_ props = foreignClass js_formatPlural props mempty +-- | Format a number properly based on pluralization, and then use it as the value for a property.+-- 'plural_' should be preferred, but 'pluralProp' can be used in places where a component is not+-- possible such as the placeholder of an input element.+pluralProp :: ToJSRef val => String -> val -> [Pair] -> PropertyOrHandler eventHandler+pluralProp name val options = formatCtx name "formatPlural" val options+ -------------------------------------------------------------------------------- -- Messages --------------------------------------------------------------------------------@@ -447,6 +521,41 @@ -> ExpQ --Q (TExp ([PropertyOrHandler eventHandler] -> ReactElementM eventHandler ())) message ident m = formattedMessage [|js_formatMsg|] ident $ Message "" m +-- | Similar to 'message', but produce an expression of type @['Pair'] -> PropertyOrHandler handler@,+-- which should be passed the values for the message. This allows you to format messages in places+-- where using a component like 'message' is not possible, such as the placeholder of input+-- elements. 'message' should be prefered since it can avoid re-rendering the formatting if the+-- value has not changed.+--+-- >import Data.Aeson ((.=))+-- >+-- >input_ [ "type" $= "numeric"+-- > , $(messageProp "placeholder" "ageplaceholder" "Hello {name}, enter your age")+-- > [ "name" .= nameFrom storeData ]+-- > ]+messageProp :: String -- ^ the property name to set+ -> MessageId -- ^ the message identifier+ -> T.Text -- ^ the default message written in ICU message syntax.+ -> ExpQ+messageProp name ident m =+ formatMessageProp "formatMessage" name ident $ Message "" m++-- | A variant of 'message' which allows you to specify some context for translators.+message' :: MessageId+ -> T.Text -- ^ A description indented to provide context for translators+ -> T.Text -- ^ The default message written in ICU message syntax+ -> ExpQ --Q (TExp ([PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()))+message' ident descr m = formattedMessage [|js_formatMsg|] ident $ Message descr m++-- | A varient of 'messageProp' which allows you to specify some context for translators.+messageProp' :: String -- ^ property to set+ -> MessageId+ -> T.Text -- ^ A description intended to provide context for translators+ -> T.Text -- ^ The default message written in ICU message syntax+ -> ExpQ+messageProp' name ident descr m =+ formatMessageProp "formatMessage" name ident $ Message descr m+ -- | Similar to 'message' but use a @FormattedHTMLMessage@ which allows HTML inside the message. It -- is recomended that you instead use 'message' together with 'elementProperty' to include rich text -- inside the message. This splice produces a value of type @[PropertyOrHandler@@ -456,13 +565,6 @@ -> ExpQ htmlMsg ident m = formattedMessage [|js_formatHtmlMsg|] ident $ Message "" m --- | A variant of 'message' which allows you to specify some context for translators.-message' :: MessageId- -> T.Text -- ^ A description indented to provide context for translators- -> T.Text -- ^ The default message written in ICU message syntax- -> ExpQ --Q (TExp ([PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()))-message' ident descr m = formattedMessage [|js_formatMsg|] ident $ Message descr m- -- | A variant of 'htmlMsg' that allows you to specify some context for translators. htmlMsg' :: MessageId -> T.Text -- ^ A description intended to provide context for translators@@ -470,9 +572,8 @@ -> ExpQ htmlMsg' ident descr m = formattedMessage [|js_formatHtmlMsg|] ident $ Message descr m --- | Utility function for messages-formattedMessage :: ExpQ -> MessageId -> Message -> ExpQ --Q (TExp ([PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()))-formattedMessage cls ident m = do+recordMessage :: MessageId -> Message -> Q ()+recordMessage ident m = do curLoc <- location mmap :: MessageMap <- fromMaybe H.empty <$> qGetQ case H.lookup ident mmap of@@ -485,9 +586,19 @@ _ -> return () qPutQ $ H.insert ident (m, curLoc) mmap +-- | Utility function for messages+formattedMessage :: ExpQ -> MessageId -> Message -> ExpQ --Q (TExp ([PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()))+formattedMessage cls ident m = do+ recordMessage ident m let liftText x = [| T.pack $(liftString $ T.unpack x)|] liftedMsg = [| Message $(liftText $ msgDescription m) $(liftText $ msgDefaultMsg m) |] [|\vals -> foreignClass $cls (messageToProps $(liftText ident) $liftedMsg vals) mempty |]++formatMessageProp :: String -> String -> MessageId -> Message -> ExpQ -- Q (TExp ([Pair] -> PropertyOrHandler eventHandler))+formatMessageProp func name ident m = do+ recordMessage ident m+ let liftedMsg = [| object ["id" .= T.pack $(liftString $ T.unpack ident), "defaultMessage" .= T.pack $(liftString $ T.unpack $ msgDefaultMsg m) ] |]+ [|\options -> formatCtx $(liftString name) $(liftString func) $liftedMsg options |] -- | Perform an arbitrary IO action on the accumulated messages at compile time, which usually -- should be to write the messages to a file. Despite producing a value of type @Q [Dec]@,
+ src/React/Flux/Combinators.hs view
@@ -0,0 +1,117 @@+-- | This module contains some useful combinators I have come across as I built a large+-- react-flux application. None of these are required to use React.Flux, they just reduce somewhat+-- the typing needed to create rendering functions.+module React.Flux.Combinators (+ clbutton_+ , cldiv_+ , faIcon_+ , foreign_+ , labeledInput_+) where++import Data.Monoid ((<>))+import React.Flux.DOM+import React.Flux.Internal+import React.Flux.PropertiesAndEvents+import React.Flux.Views++#ifdef __GHCJS__+import GHCJS.Types (JSString, JSRef)++foreign import javascript unsafe+ "$r = window[$1]"+ js_lookupWindow :: JSString -> JSRef+#else+js_lookupWindow :: a -> ()+js_lookupWindow _ = ()+#endif++-- | A wrapper around 'foreignClass' that looks up the class on the `window`. I use it for several+-- third-party react components. For example, with <https://github.com/rackt/react-modal+-- react-modal>, assuming `window.Modal` contains the definition of the classes,+--+-- >foreign_ "Modal" [ "isOpen" @= isModelOpen myProps+-- > , callback "onRequestClose" $ dispatch closeModel+-- > , "style" @= Aeson.object [ "overlay" @= Aeson.object ["left" $= "50%", "right" $= "50%"]]+-- > ] $ do+-- > h1_ "Hello, World!"+-- > p_ "...."+--+-- Here is another example using <https://github.com/JedWatson/react-select react-select>:+--+-- >reactSelect_ :: [PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()+-- >reactSelect_ props = foreign_ "Select" props mempty+-- >+-- >someView :: ReactView ()+-- >someView = defineView "some view" $ \() ->+-- > reactSelect_+-- > [ "name" $= "form-field-name"+-- > , "value" $= "one"+-- > , "options" @= [ object [ "value" .= "one", "label" .= "One" ]+-- > , object [ "value" .= "two", "label" .= "Two" ]+-- > ]+-- > , callback "onChange" $ \(i :: String) -> dispatch $ ItemChangedTo i+-- > ]+foreign_ :: String -- ^ this should be the name of a property on `window` which contains a react class.+ -> [PropertyOrHandler handler] -- ^ properties+ -> ReactElementM handler a -- ^ children+ -> ReactElementM handler a+foreign_ x = foreignClass (js_lookupWindow $ toJSString x)++-- | A 'div_' with the given class name (multiple classes can be separated by spaces). This is+-- useful for defining rows and columns in your CSS framework of choice. I use+-- <http://purecss.io/forms/ Pure CSS> so I use it something like:+--+-- >cldiv_ "pure-g" $ do+-- > cldiv_ "pure-u-1-3" $ p_ "First Third"+-- > cldiv_ "pure-u-1-3" $ p_ "Middle Third"+-- > cldiv_ "pure-u-1-3" $ p_ "Last Third"+--+-- You should consider writing something like the following for the various components in your frontend+-- of choice. In PureCSS, I use:+--+-- >prow_ :: ReactElementM handler a -> ReactElementM handler a+-- >prow_ = cldiv_ "pure-g"+-- >+-- >pcol_ :: String -> ReactElementM handler a -> ReactElementM handler a+-- >pcol_ cl = cldiv_ (unwords $ map ("pure-u-"++) $ words cl)+cldiv_ :: String -> ReactElementM handler a -> ReactElementM handler a+cldiv_ cl = div_ ["className" @= cl]++-- | A 'button_' with the given class names and `onClick` handler.+--+-- >clbutton_ ["pure-button button-success"] (dispatch LaunchMissiles) $ do+-- > faIcon_ "rocket"+-- > "Launch the missiles!"+clbutton_ :: String -- ^ class names separated by spaces+ -> handler -- ^ the onClick handler for the button+ -> ReactElementM handler a -- ^ the children+ -> ReactElementM handler a+clbutton_ cl h = button_ ["className" @= cl, onClick (\_ _ -> h)]++-- | A 'label_' and an 'input_' together. Useful for laying out forms. For example, a+-- stacked <http://purecss.io/forms/ Pure CSS Form> could be+--+-- >form_ ["className" $= "pure-form pure-form-stacked"] $+-- > fieldset_ $ do+-- > legend_ "A stacked form"+-- > labeledInput_ "email" "Email" ["type" $= "email"]+-- > labeledInput_ "password"+-- > ($(message "password-label" "Your password") [])+-- > ["type" $= "password"]+--+-- The second 'labeledInput_' shows an example using "React.Flux.Addons.Intl".+labeledInput_ :: String -- ^ the ID for the input element+ -> ReactElementM handler () -- ^ the label content. This is wrapped in a 'label_' with a `htmlFor` property+ -- equal to the given ID.+ -> [PropertyOrHandler handler] -- ^ the properties to pass to 'input_'. A property with key `id` is added to this list of properties.+ -> ReactElementM handler ()+labeledInput_ ident lbl props = label_ ["htmlFor" @= ident] lbl <> input_ (("id" @= ident):props)++-- | A <http://fortawesome.github.io/Font-Awesome/ Font Awesome> icon. The given string is prefixed+-- by `fa fa-` and then used as the class for an `i` element. This allows you to icons such as+--+-- >faIcon_ "fighter-jet" -- produces <i class="fa fa-fighter-jet">+-- >faIcon_ "refresh fa-spin" -- produces <i class="fa fa-refresh fa-spin">+faIcon_ :: String -> ReactElementM handler ()+faIcon_ cl = i_ ["className" @= ("fa fa-" ++ cl)] mempty
src/React/Flux/Internal.hs view
@@ -75,6 +75,10 @@ { propertyName :: String , propertyVal :: ref }+ | forall ref. ToJSRef ref => PropertyFromContext + { propFromThisName :: String+ , propFromThisVal :: JSRef -> ref -- ^ will be passed this.context+ } | NestedProperty { nestedPropertyName :: String , nestedPropertyVals :: [PropertyOrHandler handler]@@ -94,6 +98,7 @@ instance Functor PropertyOrHandler where fmap _ (Property name val) = Property name val+ fmap _ (PropertyFromContext name f) = PropertyFromContext name f fmap f (NestedProperty name vals) = NestedProperty name (map (fmap f) vals) fmap f (ElementProperty name (ReactElementM mkElem)) = ElementProperty name $ ReactElementM $ mapWriter (\((),e) -> ((), fmap f e)) mkElem@@ -233,13 +238,14 @@ -- once the class is re-rendered. mkReactElement :: forall eventHandler. (eventHandler -> IO ())+ -> IO JSRef -- ^ this.context -> IO [ReactElementRef] -- ^ this.props.children -> ReactElementM eventHandler () -> IO (ReactElementRef, [Callback (JSRef -> IO ())]) #ifdef __GHCJS__ -mkReactElement runHandler getPropsChildren = runWriterT . mToElem+mkReactElement runHandler getContext getPropsChildren = runWriterT . mToElem where -- Run the ReactElementM monad to create a ReactElementRef. mToElem :: ReactElementM eventHandler () -> MkReactElementM ReactElementRef@@ -262,6 +268,10 @@ addPropOrHandlerToObj obj (Property n val) = lift $ do vRef <- toJSRef val JSO.setProp (toJSString n) vRef obj+ addPropOrHandlerToObj obj (PropertyFromContext n f) = lift $ do+ ctx <- getContext+ vRef <- toJSRef $ f ctx+ JSO.setProp (toJSString n) vRef obj addPropOrHandlerToObj obj (NestedProperty n vals) = do nested <- lift $ JSO.create mapM_ (addPropOrHandlerToObj nested) vals@@ -351,7 +361,7 @@ toJSString = JSS.pack #else-mkReactElement _ _ _ = return (ReactElementRef (), [])+mkReactElement _ _ _ _ = return (ReactElementRef (), []) toJSString :: String -> String toJSString = id
src/React/Flux/PropertiesAndEvents.hs view
@@ -4,15 +4,20 @@ {-# LANGUAGE UndecidableInstances #-} module React.Flux.PropertiesAndEvents ( PropertyOrHandler- , (@=)- , ($=)++ -- * Creating Properties , property , elementProperty , nestedProperty , CallbackFunction , callback - -- * Events+ -- ** Combinators+ , (@=)+ , ($=)+ , classNames++ -- * Creating Events , Event(..) , EventTarget(..) , eventTargetProp@@ -22,23 +27,23 @@ , capturePhase , on - -- * Keyboard+ -- ** Keyboard , KeyboardEvent(..) , onKeyDown , onKeyPress , onKeyUp - -- * Focus+ -- ** Focus , FocusEvent(..) , onBlur , onFocus - -- * Form+ -- ** Form , onChange , onInput , onSubmit - -- * Mouse+ -- ** Mouse , MouseEvent(..) , onClick , onContextMenu@@ -59,7 +64,7 @@ , onMouseOver , onMouseUp - -- * Touch+ -- ** Touch , initializeTouchEvents , Touch(..) , TouchEvent(..)@@ -68,14 +73,14 @@ , onTouchMove , onTouchStart - -- * UI+ -- ** UI , onScroll - -- * Wheel+ -- ** Wheel , WheelEvent(..) , onWheel - -- * Image+ -- ** Image , onLoad , onError ) where@@ -86,6 +91,7 @@ import System.IO.Unsafe (unsafePerformIO) import qualified Data.Text as T import qualified Data.Aeson as A+import qualified Data.HashMap.Strict as M import React.Flux.Internal import React.Flux.Store@@ -109,17 +115,6 @@ nullRef = () #endif --- TOOD: change these to take a String next time we bump the major version---- | Create a property.-(@=) :: A.ToJSON a => T.Text -> a -> PropertyOrHandler handler-n @= a = Property (T.unpack n) (A.toJSON a)---- | Create a text-valued property. This is here to avoid problems when OverloadedStrings extension--- is enabled-($=) :: T.Text -> T.Text -> PropertyOrHandler handler-n $= a = Property (T.unpack n) a- -- | Some third-party React classes allow passing React elements as properties. This function -- will first run the given 'ReactElementM' to obtain an element or elements, and then use that -- element as the value for a property with the given key.@@ -183,6 +178,27 @@ -- For another example, see the haddock comments in "React.Flux.Addons.Bootstrap". callback :: CallbackFunction handler func => String -> func -> PropertyOrHandler handler callback name func = CallbackPropertyWithArgumentArray name $ \arr -> applyFromArguments arr 0 func++----------------------------------------------------------------------------------------------------+--- Combinators+----------------------------------------------------------------------------------------------------++-- | Create a property.+(@=) :: A.ToJSON a => T.Text -> a -> PropertyOrHandler handler+n @= a = Property (T.unpack n) (A.toJSON a)++-- | Create a text-valued property. This is here to avoid problems when OverloadedStrings extension+-- is enabled+($=) :: T.Text -> T.Text -> PropertyOrHandler handler+n $= a = Property (T.unpack n) a++-- | Set the <https://facebook.github.io/react/docs/class-name-manipulation.html className> property to consist+-- of all the names which are matched with True, allowing you to easily toggle class names based on+-- a computation.+classNames :: [(T.Text, Bool)] -> PropertyOrHandler handler+classNames xs = "className" @= T.intercalate " " names+ where+ names = M.keys $ M.filter id $ M.fromList xs ---------------------------------------------------------------------------------------------------- --- Generic Event
src/React/Flux/Views.hs view
@@ -119,7 +119,7 @@ -- > -- >todoItem :: ReactView (Int, Todo) -- >todoItem = defineView "todo item" $ \(todoIdx, todo) ->--- > li_ [ "className" @= (unwords $ [ "completed" | todoComplete todo] ++ [ "editing" | todoIsEditing todo ])+-- > li_ [ classNames [("completed", todoComplete todo), ("editing", todoIsEditing todo)] -- > , "key" @= todoIdx -- > ] $ do -- > @@ -285,6 +285,10 @@ js_ReactGetProps :: ReactThis state props -> IO (Export props) foreign import javascript unsafe+ "$1['context']"+ js_ReactGetContext :: ReactThis state props -> IO JSRef++foreign import javascript unsafe "hsreact$children_to_array($1['props']['children'])" js_ReactGetChildren :: ReactThis state props -> IO JSArray @@ -352,8 +356,9 @@ let getPropsChildren = do childRef <- js_ReactGetChildren this return $ map ReactElementRef $ toList childRef+ getContext = js_ReactGetContext this - (element, evtCallbacks) <- mkReactElement (runHandler this) getPropsChildren node+ (element, evtCallbacks) <- mkReactElement (runHandler this) getContext getPropsChildren node evtCallbacksRef <- toJSRef $ map jsref evtCallbacks js_RenderCbSetResults arg evtCallbacksRef element@@ -396,36 +401,8 @@ let (a, childEl) = runWriter child in elementToM a $ ViewElement (reactView rc) (Just key) props childEl --- | Create a 'ReactElement' for a class defined in javascript. For example, if you would like to--- use <https://github.com/JedWatson/react-select react-select>, you could do so as follows:------ >foreign import javascript unsafe--- > "window['Select']"--- > js_ReactSelectClass :: JSRef--- >--- >reactSelect_ :: [PropertyOrHandler eventHandler] -> ReactElementM eventHandler ()--- >reactSelect_ props = foreignClass js_ReactSelectClass props mempty--- >--- >onSelectChange :: FromJSON a--- > => (a -> handler) -- ^ receives the new value and performs an action.--- > -> PropertyOrHandler handler--- >onSelectChange f = callback "onChange" (f . parse)--- > where--- > parse v =--- > case fromJSON v of--- > Error err -> error $ "Unable to parse new value for select onChange: " ++ err--- > Success e -> e------ This could then be used as part of a rendering function like so:------ >reactSelect_--- > [ "name" $= "form-field-name"--- > , "value" $= "one"--- > , "options" @= [ object [ "value" .= "one", "label" .= "One" ]--- > , object [ "value" .= "two", "label" .= "Two" ]--- > ]--- > , onSelectChange dispatchSomething--- > ]+-- | Create a 'ReactElement' for a class defined in javascript. See+-- 'React.Flux.Combinators.foreign_' for a convenient wrapper and some examples. foreignClass :: JSRef -- ^ The javascript reference to the class -> [PropertyOrHandler eventHandler] -- ^ properties and handlers to pass when creating an instance of this class. -> ReactElementM eventHandler a -- ^ The child element or elements
test/client/TestClient.hs view
@@ -197,7 +197,8 @@ displayChildren :: ReactView String displayChildren = defineView "display children" $ \ident ->- span_ ["className" $= "display-children", "id" @= ident] childrenPassedToView+ span_ [classNames [("display-children", True), ("missing-name", False)], "id" @= ident]+ childrenPassedToView displayChildren_ :: String -> ReactElementM handler () -> ReactElementM handler () displayChildren_ = view displayChildren
test/client/TestClient14.hs view
@@ -5,6 +5,7 @@ import React.Flux import React.Flux.Addons.Intl import GHCJS.Types (JSRef)+import Data.Aeson ((.=)) import TestClient @@ -17,12 +18,19 @@ js_translations :: JSRef intlSpec :: ReactView ()-intlSpec = defineView "intl" $ \() -> div_ ["id" $= "intl-spec"] $ intlProvider_ "en-US" (Just js_translations) Nothing $+intlSpec = defineView "intl" $ \() ->+ intlProvider_ "en-US" (Just js_translations) Nothing $+ view intlSpecBody () mempty++intlSpecBody :: ReactView ()+intlSpecBody = defineView "intl body" $ \() -> div_ ["id" $= "intl-spec"] $ ul_ $ do li_ ["id" $= "f-number"] $ formattedNumber_ [ "value" @= (0.9 :: Double), "style" $= "percent" ] li_ ["id" $= "f-int"] $ int_ 100000 li_ ["id" $= "f-double"] $ double_ 40000.2+ li_ ["id" $= "f-number-prop"] $+ input_ [formattedNumberProp "placeholder" (123456 :: Int) []] let moon = fromGregorian 1969 7 20 fullDayF = DayFormat { weekdayF = Just "long", eraF = Just "short", yearF = Just "2-digit", monthF = Just "long", dayF = Just "2-digit" }@@ -31,6 +39,8 @@ li_ ["id" $= "f-fullday"] $ day_ fullDayF moon li_ ["id" $= "f-date"] $ formattedDate_ (Left moon) [ "weekday" $= "short", "month" $= "short", "day" $= "numeric", "year" $= "2-digit" ]+ li_ ["id" $= "f-date-prop"] $+ input_ [formattedDateProp "placeholder" (Left moon) []] let step = UTCTime moon (2*60*60 + 56*60) -- 1969-7-20 02:56 UTC fullT = (fullDayF, TimeFormat { hourF = Just "numeric", minuteF = Just "2-digit", secondF = Just "numeric", timeZoneNameF = Just "long" })@@ -43,6 +53,15 @@ , "timeZoneName" $= "short" , "timeZone" $= "Pacific/Tahiti" ]+ li_ ["id" $= "f-time-prop"] $+ input_ [formattedDateProp "placeholder" (Right step)+ [ "year" .= ("2-digit" :: String)+ , "month" .= ("short" :: String)+ , "day" .= ("2-digit" :: String)+ , "hour" .= ("numeric" :: String)+ , "timeZone" .= ("Pacific/Tahiti" :: String)+ ]+ ] {- li_ ["id" $= "f-relative"] $ relativeTo_ step@@ -50,6 +69,8 @@ -} li_ ["id" $= "f-plural"] $ plural_ [ "value" @= (100 :: Int), "one" $= "plural one", "other" $= "plural other"]+ li_ ["id" $= "f-plural-prop"] $+ input_ [pluralProp "placeholder" (100 :: Int) ["one" .= ("plural one" :: String), "other" .= ("plural other" :: String)]] li_ ["id" $= "f-msg"] $ $(message "photos" "{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}.")@@ -58,6 +79,13 @@ , elementProperty "takenAgo" $ span_ ["id" $= "takenAgoSpan"] "years ago" ] + li_ ["id" $= "f-msg-prop"] $+ input_ [ $(messageProp "placeholder" "photosprop" "{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}")+ [ "name" .= ("Neil Armstrong" :: String)+ , "numPhotos" .= (100 :: Int)+ ]+ ]+ li_ ["id" $= "f-msg-with-trans"] $ $(message "with_trans" "this is not used {abc}") ["abc" $= "xxx"] @@ -66,6 +94,13 @@ [ "name" $= "Neil Armstrong" , "numPhotos" @= (0 :: Int) ]++ li_ ["id" $= "f-msg-prop-with-descr"] $+ input_ [$(messageProp' "placeholder" "photosprop2" "How many photos?" "{name} took {numPhotos, number} photos")+ [ "name" .= ("Neil Armstrong" :: String)+ , "numPhotos" .= (0 :: Int)+ ]+ ] li_ ["id" $= "f-html-msg"] $ $(htmlMsg "html1" "<b>{num}</b> is the answer to life, the universe, and everything")
test/client/msgs/android.xml view
@@ -6,5 +6,8 @@ <string name="photos">{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}.</string> <!-- How many photos? --> <string name="photos2">{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}.</string>+<string name="photosprop">{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}</string>+<!-- How many photos? -->+<string name="photosprop2">{name} took {numPhotos, number} photos</string> <string name="with_trans">this is not used {abc}</string> </resources>
test/client/msgs/jsonmsgs.json view
@@ -1,1 +1,1 @@-{"photos":{"message":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}."},"html2":{"message":"{num} is the <b>answer</b> to life, the universe, and everything","description":"Hitchhiker's Guide"},"photos2":{"message":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}.","description":"How many photos?"},"html1":{"message":"<b>{num}</b> is the answer to life, the universe, and everything"},"with_trans":{"message":"this is not used {abc}"}}+{"photos":{"message":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}."},"html2":{"message":"{num} is the <b>answer</b> to life, the universe, and everything","description":"Hitchhiker's Guide"},"photos2":{"message":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}.","description":"How many photos?"},"photosprop2":{"message":"{name} took {numPhotos, number} photos","description":"How many photos?"},"html1":{"message":"<b>{num}</b> is the answer to life, the universe, and everything"},"with_trans":{"message":"this is not used {abc}"},"photosprop":{"message":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}"}}
test/client/msgs/jsonnodescr.json view
@@ -1,1 +1,1 @@-{"photos":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}.","html2":"{num} is the <b>answer</b> to life, the universe, and everything","photos2":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}.","html1":"<b>{num}</b> is the answer to life, the universe, and everything","with_trans":"this is not used {abc}"}+{"photos":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} {takenAgo}.","html2":"{num} is the <b>answer</b> to life, the universe, and everything","photos2":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}.","photosprop2":"{name} took {numPhotos, number} photos","html1":"<b>{num}</b> is the answer to life, the universe, and everything","with_trans":"this is not used {abc}","photosprop":"{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}}"}
test/spec/TestClientSpec.hs view
@@ -48,6 +48,12 @@ e <- findElem (ById $ T.pack ident) getText e `shouldReturn` T.pack txt +intlPlaceholderShouldBe :: String -> String -> WD ()+intlPlaceholderShouldBe ident txt = do+ e <- findElem (ById $ T.pack ident)+ input <- findElemFrom e $ ByTag "input"+ (input `attr` "placeholder") `shouldReturn` Just (T.pack txt)+ -- | Only up to 999,999 since this is just used for the number of days since 1969 showWithComma :: Integer -> String showWithComma i = show x ++ "," ++ show y@@ -187,16 +193,19 @@ it "does not display null children" $ runWD $ do s <- findElem $ ById "empty-children"+ s `attr` "class" `shouldReturn` Just "display-children" findElemsFrom s (ByCSS "*") `shouldReturn` [] getText s `shouldReturn` "" it "displays a single child" $ runWD $ do s <- findElem $ ById "single-child-wrapper"+ s `attr` "class" `shouldReturn` Just "display-children" s' <- findElemFrom s $ ByCSS "span#single-child" getText s' `shouldReturn` "Single Child!!" it "displays a child list" $ runWD $ do s <- findElem $ ById "multi-child"+ s `attr` "class" `shouldReturn` Just "display-children" c1 <- findElemFrom s $ ByCSS "span#child1" getText c1 `shouldReturn` "Child 1" c2 <- findElemFrom s $ ByCSS "span#child2"@@ -316,3 +325,11 @@ getText htmlMsg' `shouldReturn` "42 is the answer to life, the universe, and everything" (findElemFrom htmlMsg' (ByTag "b") >>= getText) `shouldReturn` "answer"++ it "displays formatted properties" $ runWD $ do+ "f-number-prop" `intlPlaceholderShouldBe` "123,456"+ "f-date-prop" `intlPlaceholderShouldBe` "7/20/1969"+ "f-time-prop" `intlPlaceholderShouldBe` "Jul 19, 69, 4 PM"+ "f-plural-prop" `intlPlaceholderShouldBe` "other"+ "f-msg-prop" `intlPlaceholderShouldBe` "Neil Armstrong took 100 photos"+ "f-msg-prop-with-descr" `intlPlaceholderShouldBe` "Neil Armstrong took 0 photos"