packages feed

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 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.  ~~~
+ example/purecss-side-menu/App.hs view
@@ -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
+ example/purecss-side-menu/Dispatcher.hs view
@@ -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]
+ example/purecss-side-menu/Main.hs view
@@ -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 ()
+ example/purecss-side-menu/NavStore.hs view
@@ -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
+ example/purecss-side-menu/PageViews.hs view
@@ -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."
+ example/purecss-side-menu/index.html view
@@ -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"