Skip to content

Commit 8ccef07

Browse files
author
Marcin Szamotulski
authored
Link eff (#4)
* Use URL type from DOM.HTML.History * LinkProps * a newtype * add action field Rename `goTo` to `goto`. * Update tests. * Review the link class Do not create react class as a function (this is an anti pattern). * Change link and link' functions. * Fixed bower.json
1 parent afbc898 commit 8ccef07

File tree

7 files changed

+79
-74
lines changed

7 files changed

+79
-74
lines changed

bower.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,5 @@
3636
"devDependencies": {
3737
"purescript-psci-support": "^3.0.0",
3838
"purescript-test-unit": "^13.0.0"
39-
},
40-
"resolutions": {
41-
"purescript-react": "^4.0.0"
4239
}
4340
}

example/Main.purs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Control.Comonad.Cofree ((:<))
66
import Control.Monad.Eff (Eff)
77
import DOM (DOM)
88
import DOM.HTML (window)
9+
import DOM.HTML.History (URL(..))
910
import DOM.HTML.Types (htmlDocumentToDocument)
1011
import DOM.HTML.Window (document)
1112
import DOM.Node.NonElementParentNode (getElementById)
@@ -18,6 +19,7 @@ import Partial.Unsafe (unsafePartial)
1819
import React (ReactClass, ReactElement, createClass, createElement, getChildren, getProps, spec)
1920
import React.DOM (div', h1', h2', h3', h4', text)
2021
import React.Router (IndexRoute(..), Route(..), RouteProps(..), Router, browserRouterClass, defaultConfig, link', (:+))
22+
import React.Router.Utils (showLocation)
2123
import ReactDOM (render)
2224
import Routing.Match.Class (int, lit, str)
2325

@@ -37,7 +39,7 @@ home = createClass $ (spec unit render) { displayName = "Home" }
3739
render this = do
3840
chrn <- getChildren this
3941
pure $ div'
40-
[ h1' [ link' defaultConfig (show Home) [text "Home component"] ]
42+
[ h1' [ link' defaultConfig (showLocation (Home : Nil)) [text "Home component"] ]
4143
, div' chrn
4244
]
4345

@@ -47,9 +49,9 @@ usersIndex = createClass $ (spec unit render) { displayName = "UsersIndex" }
4749
render this = do
4850
pure $ div'
4951
[ h2' [ text "UserIndex component" ]
50-
, div' [ div' [link' defaultConfig "/1" [ text "User 1" ]]
51-
, div' [link' defaultConfig "/2" [ text "User 2" ]]
52-
, div' [link' defaultConfig "/3" [ text "User 3" ]]
52+
, div' [ div' [link' defaultConfig (URL "/1") [ text "User 1" ]]
53+
, div' [link' defaultConfig (URL "/2") [ text "User 2" ]]
54+
, div' [link' defaultConfig (URL "/3") [ text "User 3" ]]
5355
]
5456
]
5557

@@ -80,11 +82,11 @@ userBooksIndex = createClass $ (spec unit render) { displayName = "UserBooksInde
8082
_ -> 0
8183
chrn <- getChildren this
8284
pure $ div'
83-
[ h3' [ link' defaultConfig ("/" <> show uID) [ text "UserBooksIndex component" ] ]
85+
[ h3' [ link' defaultConfig (URL $ "/" <> show uID) [ text "UserBooksIndex component" ] ]
8486
, div'
85-
[ div' [ link' defaultConfig ("/" <> show uID <> "/book/fp-programming") [ text "Functional Programming" ] ]
86-
, div' [ link' defaultConfig ("/" <> show uID <> "/book/grothendieck-galois-theory") [ text "Grothendick Galois Theory" ] ]
87-
, div' [ link' defaultConfig ("/" <> show uID <> "/book/category-theory") [ text "Category Theory for the Working Mathematician" ]]
87+
[ div' [ link' defaultConfig (URL $ "/" <> show uID <> "/book/fp-programming") [ text "Functional Programming" ] ]
88+
, div' [ link' defaultConfig (URL $ "/" <> show uID <> "/book/grothendieck-galois-theory") [ text "Grothendick Galois Theory" ] ]
89+
, div' [ link' defaultConfig (URL $ "/" <> show uID <> "/book/category-theory") [ text "Category Theory for the Working Mathematician" ]]
8890
]
8991
, div' chrn
9092
]

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"scripts": {
2525
"clean": "rimraf output && rimraf .pulp-cache",
2626
"build": "pulp build",
27-
"pretest": "pulp browserify --main 'Test.Main' -I test --to karma/index.js",
2827
"test": "pulp test",
2928
"example": "node utils/serve-example.js & pulp -w browserify --main 'Example.Main' -I example --to dist/index.js"
3029
},

src/React/Router/Components.purs

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module React.Router.Components
22
( browserRouter
33
, browserRouterClass
4+
, LinkProps(LinkProps)
45
, linkSpec
56
, link
67
, link'
7-
, to
8-
, goTo
8+
, goto
99
) where
1010

1111

@@ -25,11 +25,11 @@ import DOM.HTML.Types (HISTORY, windowToEventTarget)
2525
import DOM.HTML.Window (history, location)
2626
import Data.Foreign (toForeign)
2727
import Data.List (List)
28-
import Data.Maybe (Maybe(..), fromMaybe, isNothing, maybe')
28+
import Data.Maybe (Maybe(..), fromMaybe, isNothing, maybe, maybe')
2929
import Data.Newtype (un)
3030
import Data.Tuple (Tuple)
3131
import Prelude (Unit, bind, const, discard, pure, show, unit, void, ($), (<<<), (<>), (>>=), (||))
32-
import React (ReactClass, ReactElement, ReactSpec, createClass, createElement, getChildren, getProps, preventDefault, readState, spec, spec', transformState)
32+
import React (ReactClass, ReactElement, ReactProps, ReactRefs, ReactSpec, ReactState, ReadOnly, ReadWrite, createClass, createElement, getChildren, getProps, preventDefault, readState, spec, spec', transformState)
3333
import React.DOM (a, div')
3434
import React.DOM.Props (Props, href, onClick)
3535
import React.Router.Class (class RoutePropsClass)
@@ -40,7 +40,7 @@ import React.Router.Utils (hasBaseName, joinUrls, stripBaseName, warning)
4040
-- | RouterState type
4141
type RouterState =
4242
{ hash :: String
43-
, pathname :: String
43+
, pathname :: URL
4444
, search :: String
4545
}
4646

@@ -53,23 +53,23 @@ type RouterProps props arg notFoundProps =
5353
}
5454
}
5555

56-
foreign import createPopStateEvent :: String -> Event
56+
foreign import createPopStateEvent :: URL -> Event
5757

5858
getLocation
5959
:: forall e
6060
. RouterConfig
61-
-> Eff (dom :: DOM, console :: CONSOLE | e) { hash :: String, pathname :: String, search :: String }
61+
-> Eff (dom :: DOM, console :: CONSOLE | e) { hash :: String, pathname :: URL, search :: String }
6262
getLocation cfg = do
6363
l <- window >>= location
6464
h <- hash l
6565
p <- pathname l
6666
s <- search l
6767
let cfgR = un RouterConfig cfg
6868
warning
69-
(isNothing cfgR.baseName || (hasBaseName cfgR.baseName p))
69+
(isNothing cfgR.baseName || (hasBaseName cfgR.baseName (URL p)))
7070
("""You are using baseName on a page which URL path does not begin with. Expecting path: """
71-
<> p <> """ to begin with: """ <> (fromMaybe "" cfgR.baseName))
72-
pure { hash: h, pathname: stripBaseName cfgR.baseName p, search: s }
71+
<> p <> """ to begin with: """ <> (maybe "" (un URL) cfgR.baseName))
72+
pure { hash: h, pathname: stripBaseName cfgR.baseName (URL p), search: s }
7373

7474

7575
-- | `ReactSpec` for the `browserRouterClass` - the main entry point react
@@ -90,7 +90,7 @@ browserRouter cfg = (spec' initialState render) { displayName = "BrowserRouter",
9090
render this = do
9191
props <- getProps this
9292
state <- readState this
93-
let loc = state.pathname <> state.search <> state.hash
93+
let loc = un URL state.pathname <> state.search <> state.hash
9494

9595
case runRouter loc props.router of
9696
Nothing -> do
@@ -121,57 +121,60 @@ browserRouterClass
121121
-> ReactClass (RouterProps props arg notfound)
122122
browserRouterClass cfg = createClass (browserRouter cfg)
123123

124-
type LinkProps = {to :: String, props :: Array Props}
125-
126-
to :: String -> LinkProps
127-
to = { to: _, props: [] }
124+
newtype LinkProps eff = LinkProps
125+
{ url :: URL
126+
, props :: Array Props
127+
, action :: Eff (props :: ReactProps, refs :: ReactRefs ReadOnly, state :: ReactState ReadWrite | eff) Unit
128+
}
128129

129130
-- | `ReactSpec` for the `link` element; it takes a record of type `LinkProps`
130131
-- | as properties. The `props` record property is directly passed to underlying
131132
-- | `a` element, e.g. this can be used to add css classes.
132-
linkSpec :: RouterConfig -> ReactSpec LinkProps Unit ()
133-
linkSpec cfg = (spec unit render) { displayName = "Link" }
133+
linkSpec :: forall eff. ReactSpec (LinkProps eff) Unit ()
134+
linkSpec = (spec unit render) { displayName = "Link" }
134135
where
135136
render this = do
136-
p <- getProps this
137+
LinkProps p <- getProps this
137138
chrn <- getChildren this
138139
pure $ a
139-
([href p.to, (onClick $ clickHandler this)] <> p.props)
140+
([href (un URL p.url), (onClick $ clickHandler this)] <> p.props)
140141
chrn
141142

142143
clickHandler this ev = do
143144
_ <- preventDefault ev
144-
{ to: url } <- getProps this
145-
goTo cfg url
145+
LinkProps { action } <- getProps this
146+
action
146147

147148
-- | React class for the `link` element.
148-
linkClass :: RouterConfig -> ReactClass LinkProps
149-
linkClass = createClass <<< linkSpec
149+
linkClass :: forall eff. ReactClass (LinkProps eff)
150+
linkClass = createClass linkSpec
150151

151-
-- | `link` element; use it instead of `a` to route the user through application.
152-
link :: RouterConfig -> LinkProps -> Array ReactElement -> ReactElement
153-
link cfg = createElement (linkClass cfg)
152+
-- | `link` element; use it instead of `a` to route the user through
153+
-- | application with the default action `goto cfg url` and custom properties.
154+
link :: RouterConfig -> URL -> Array Props -> Array ReactElement -> ReactElement
155+
link cfg url props = createElement linkClass (LinkProps { url, props, action: goto cfg url})
154156

155-
-- | as `link`, but with empty properties passed to the underlying `a` element.
156-
link' :: RouterConfig -> String -> Array ReactElement -> ReactElement
157-
link' cfg = link cfg <<< {to: _, props: []}
157+
-- | as `link`, but with empty props.
158+
link' :: RouterConfig -> URL -> Array ReactElement -> ReactElement
159+
link' cfg url = link cfg url []
158160

159-
goTo
161+
-- | goto url
162+
goto
160163
:: forall eff
161164
. RouterConfig
162-
-> String
165+
-> URL
163166
-> Eff ( console :: CONSOLE
164167
, dom :: DOM
165168
, history :: HISTORY
166169
| eff
167170
) Unit
168-
goTo cfg url = catchException
171+
goto cfg url = catchException
169172
(error <<< show)
170173
(do
171174
w <- window
172175
h <- history w
173-
let url_ = joinUrls (fromMaybe "" (un RouterConfig cfg).baseName) url
174-
pushState (toForeign unit) (DocumentTitle url_) (URL url_) h
176+
let url_ = joinUrls (fromMaybe (URL "") (un RouterConfig cfg).baseName) url
177+
pushState (toForeign unit) (DocumentTitle (un URL url_)) url_ h
175178
void $ coerceEff $ dispatchEvent (createPopStateEvent url) (windowToEventTarget w))
176179
where
177180
coerceEff :: forall e a. Eff (err :: EXCEPTION | e) a -> Eff (exception :: EXCEPTION | e) a

src/React/Router/Types.purs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module React.Router.Types
1515
import Prelude
1616

1717
import Control.Comonad.Cofree ((:<), Cofree)
18+
import DOM.HTML.History (URL)
1819
import Data.Lens (Lens', lens)
1920
import Data.List (List)
2021
import Data.Map (Map)
@@ -111,7 +112,7 @@ withoutIndex r rs = Tuple r Nothing :< rs
111112
-- | `:+` lets define routes without index route
112113
infixr 5 withoutIndex as :+
113114

114-
newtype RouterConfig = RouterConfig { baseName :: Maybe String }
115+
newtype RouterConfig = RouterConfig { baseName :: Maybe URL }
115116

116117
derive instance newtypeRouterConfig :: Newtype RouterConfig _
117118

src/React/Router/Utils.purs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Prelude
1919
import Control.Comonad.Cofree (Cofree, head, tail)
2020
import Control.Monad.Eff (Eff)
2121
import Control.Monad.Eff.Console (CONSOLE)
22+
import DOM.HTML.History (URL(..))
2223
import Data.Foldable (class Foldable, foldMap, foldl, foldr)
2324
import Data.List (List(..), concatMap, (:))
2425
import Data.List as L
@@ -40,36 +41,36 @@ routeToString url = L.intercalate "/" $ unwrap <$> url
4041

4142
foreign import warning :: forall e. Boolean -> String -> Eff (console :: CONSOLE | e) Unit
4243

43-
hasBaseName :: Maybe String -> String -> Boolean
44+
hasBaseName :: Maybe URL -> URL -> Boolean
4445
hasBaseName Nothing _ = true
45-
hasBaseName (Just b) s = isJust (S.stripPrefix (S.Pattern b) s) && (S.null rest || next == "/" || next == "#" || next == "?")
46+
hasBaseName (Just (URL b)) (URL s) = isJust (S.stripPrefix (S.Pattern b) s) && (S.null rest || next == "/" || next == "#" || next == "?")
4647
where
4748
rest = S.drop (S.length b) s
4849
next = S.take 1 rest
4950

50-
stripBaseName :: Maybe String -> String -> String
51+
stripBaseName :: Maybe URL -> URL -> URL
5152
stripBaseName Nothing s = s
52-
stripBaseName (Just b) s = S.drop (S.length b) s
53+
stripBaseName (Just (URL b)) (URL s) = URL $ S.drop (S.length b) s
5354

5455
-- | Join two url strings putting `/` separator in between if necessary.
55-
joinUrls :: String -> String -> String
56-
joinUrls a b | S.null a = b
57-
| S.null b = a
56+
joinUrls :: URL -> URL -> URL
57+
joinUrls (URL a) (URL b) | S.null a = URL b
58+
| S.null b = URL a
5859
| otherwise =
5960
let _a = if S.charAt (S.length a - 1) a == Just '/'
6061
then S.take (S.length a - 1) a
6162
else a
6263
_b = if S.charAt 0 b == Just '/'
6364
then S.drop 1 b
6465
else b
65-
in _a <> "/" <> _b
66+
in URL $ _a <> "/" <> _b
6667

6768
-- | Fold over list of locations and join them as urls.
6869
-- | ``` purescript
6970
-- | showLocation (Home : User 1 : Settings) -- /user/1/settings
7071
-- | ```
71-
showLocation :: forall a t. Show a => Foldable t => t a -> String
72-
showLocation t = foldl (\url -> joinUrls url <<< show) "" t
72+
showLocation :: forall a t. Show a => Foldable t => t a -> URL
73+
showLocation t = foldl (\url -> joinUrls url <<< URL <<< show) (URL "") t
7374

7475
-- | Find location inside a tail of `Cofree List`. This is useful for
7576
-- | querying about children that are mounted under a given component.

test/Utils.purs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ module Test.Utils
44
import Prelude
55

66
import Control.Comonad.Cofree ((:<))
7+
import DOM.HTML.History (URL(..))
78
import Data.Either (Either(..))
89
import Data.List (List(..), (:))
910
import Data.Maybe (Maybe(Just, Nothing))
11+
import Data.Newtype (un)
1012
import Data.Tuple (Tuple(..))
1113
import React.Router.Utils (findLocation, hasBaseName, joinUrls, showLocation, stripBaseName, (:<<<), (:>>>))
1214
import Test.Unit (TestSuite, failure, suite, test)
@@ -23,38 +25,38 @@ testSuite :: forall eff. TestSuite eff
2325
testSuite = suite "Utils" do
2426

2527
test "hasBaseName" do
26-
assert "should return true when basename is Nothing" $ hasBaseName Nothing "/home"
27-
assert "expected /prefix to have basename (Just /prefix)" $ hasBaseName (Just "/prefix") "/prefix"
28-
assert "expected /prefix/ to have basename (Just /prefix)" $ hasBaseName (Just "/prefix") "/prefix/"
29-
assert "expected /prefix? to have basename (Just /prefix)" $ hasBaseName (Just "/prefix") "/prefix?"
30-
assert "expected /prefix# to have basename (Just /prefix)" $ hasBaseName (Just "/prefix") "/prefix#"
31-
assert "expected /prefixx to not have basename (Just /prefix)" $ not $ hasBaseName (Just "/prefix") "/prefixx"
32-
assert "expected /home to not have basename (Just /prefix)" $ not $ hasBaseName (Just "/prefix") "/home"
28+
assert "should return true when basename is Nothing" $ hasBaseName Nothing $ URL "/home"
29+
assert "expected /prefix to have basename (Just /prefix)" $ hasBaseName (Just $ URL "/prefix") $ URL "/prefix"
30+
assert "expected /prefix/ to have basename (Just /prefix)" $ hasBaseName (Just $ URL "/prefix") $ URL "/prefix/"
31+
assert "expected /prefix? to have basename (Just /prefix)" $ hasBaseName (Just $ URL "/prefix") $ URL "/prefix?"
32+
assert "expected /prefix# to have basename (Just /prefix)" $ hasBaseName (Just $ URL "/prefix") $ URL "/prefix#"
33+
assert "expected /prefixx to not have basename (Just /prefix)" $ not $ hasBaseName (Just $ URL "/prefix") $ URL "/prefixx"
34+
assert "expected /home to not have basename (Just /prefix)" $ not $ hasBaseName (Just $ URL "/prefix") $ URL "/home"
3335

3436
test "stripBaseName" do
35-
let s = stripBaseName (Just "/home") "/home/url"
36-
assert ("expected /home/url to be /url but got: " <> s) $ s == "/url"
37+
let s = stripBaseName (Just $ URL "/home") $ URL "/home/url"
38+
assert ("expected /home/url to be /url but got: " <> un URL s) $ un URL s == "/url"
3739

3840
test "joinUrls" do
39-
let r1 = joinUrls "/a" "/b"
41+
let r1 = un URL $ joinUrls (URL "/a") (URL "/b")
4042
assert ("expected /a/b but got " <> r1) $ r1 == "/a/b"
41-
let r2 = joinUrls "/a/" "/b"
43+
let r2 = un URL $ joinUrls (URL "/a/") (URL "/b")
4244
assert ("expected /a/b but got " <> r2) $ r2 == "/a/b"
43-
let r3 = joinUrls "/a/" "b"
45+
let r3 = un URL $ joinUrls (URL "/a/") (URL "b")
4446
assert ("expected /a/b but got " <> r3) $ r3 == "/a/b"
45-
let r4 = joinUrls "" "/b"
47+
let r4 = un URL $ joinUrls (URL "") (URL "/b")
4648
assert ("expected /b but got " <> r4) $ r4 == "/b"
47-
let r5 = joinUrls "/a" ""
49+
let r5 = un URL $ joinUrls (URL "/a") (URL "")
4850
assert ("expected /a but got " <> r5) $ r5 == "/a"
4951

5052
test "showLocation" do
51-
let r1 = showLocation (Home : User 1 : Nil)
53+
let r1 = un URL $ showLocation (Home : User 1 : Nil)
5254
e1 = "/home/user/1"
5355
assert ("expected " <> e1 <> " but got " <> r1) $ r1 == e1
54-
let r2 = showLocation (Home : User 1 : Settings : Nil)
56+
let r2 = un URL $ showLocation (Home : User 1 : Settings : Nil)
5557
e2 = "/home/user/1/settings/"
5658
assert ("expected " <> e2 <> " but got " <> r2) $ r2 == e2
57-
let r3 = showLocation [Home, Settings, User 1]
59+
let r3 = un URL $ showLocation [Home, Settings, User 1]
5860
e3 = "/home/settings/user/1"
5961
assert ("expected " <> e3 <> " but got " <> r3) $ r3 == e3
6062

0 commit comments

Comments
 (0)