Skip to content

Commit 9999161

Browse files
authored
Add async component (#61)
Example: https://github.com/lumihq/purescript-react-basic/compare/michael/add-async-component?expand=1#diff-640d53ee0622b4db5f8d7fa4aed8f46eR44 Considering using React's suspend features for this but it might be too soon..
1 parent 7c19e84 commit 9999161

File tree

9 files changed

+182
-6
lines changed

9 files changed

+182
-6
lines changed

examples/async/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
output
2+
html/index.js
3+
package-lock.json
4+
node_modules

examples/async/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
all: node_modules
2+
purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs'
3+
purs bundle -m Main --main Main output/*/*.js > output/bundle.js
4+
node_modules/.bin/browserify output/bundle.js -o html/index.js
5+
6+
node_modules:
7+
npm install
8+

examples/async/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Async Counter Example
2+
3+
## Building
4+
5+
```
6+
npm install
7+
make all
8+
```
9+
10+
This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle.
11+
12+
Then open `html/index.html` in your browser.

examples/async/html/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>react-basic example</title>
5+
</head>
6+
<body>
7+
<div id="container"></div>
8+
<script src="index.js"></script>
9+
</body>
10+
</html>

examples/async/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"dependencies": {
3+
"react": "16.6.0",
4+
"react-dom": "16.6.0"
5+
},
6+
"devDependencies": {
7+
"browserify": "16.2.3"
8+
}
9+
}

examples/async/src/AsyncCounter.purs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module AsyncCounter where
2+
3+
import Prelude
4+
5+
import Effect.Aff (Milliseconds(..), delay)
6+
import Effect.Class (liftEffect)
7+
import Effect.Console (log)
8+
import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, fragment, keyed, make)
9+
import React.Basic.Components.Async (asyncWithLoader)
10+
import React.Basic.DOM as R
11+
12+
component :: Component Props
13+
component = createComponent "AsyncCounter"
14+
15+
type Props =
16+
{ label :: String
17+
}
18+
19+
data Action
20+
= Increment
21+
22+
asyncCounter :: Props -> JSX
23+
asyncCounter = make component { initialState, update, render }
24+
where
25+
initialState = { counter: 0 }
26+
27+
update self = case _ of
28+
Increment ->
29+
Update self.state { counter = self.state.counter + 1 }
30+
31+
render self =
32+
fragment
33+
[ R.p_ [ R.text "Notes:" ]
34+
, R.ol_
35+
[ R.li_ [ R.text "The two counts should never be out of sync" ]
36+
, R.li_ [ R.text "\"done\" should only be logged to the console once for any loading period (in-flight requests get cancelled as the next request starts)" ]
37+
]
38+
, R.button
39+
{ onClick: capture_ self Increment
40+
, children: [ R.text (self.props.label <> ": " <> show self.state.counter) ]
41+
}
42+
, R.text " "
43+
, keyed (show self.state.counter) $
44+
asyncWithLoader (R.text "Loading...") do
45+
liftEffect $ log "start"
46+
delay $ Milliseconds 2000.0
47+
liftEffect $ log "done"
48+
pure $ R.text $ "Done: " <> show self.state.counter
49+
]
50+
51+

examples/async/src/Main.purs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module Main where
2+
3+
import Prelude
4+
5+
import AsyncCounter (asyncCounter)
6+
import Data.Maybe (Maybe(..))
7+
import Effect (Effect)
8+
import Effect.Exception (throw)
9+
import React.Basic.DOM (render)
10+
import Web.DOM.NonElementParentNode (getElementById)
11+
import Web.HTML (window)
12+
import Web.HTML.HTMLDocument (toNonElementParentNode)
13+
import Web.HTML.Window (document)
14+
15+
main :: Effect Unit
16+
main = do
17+
container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window)
18+
case container of
19+
Nothing -> throw "Container element not found."
20+
Just c ->
21+
let app = asyncCounter { label: "Async Increment" }
22+
in render app c

src/React/Basic.purs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,6 @@ foreign import createComponent
146146
. String
147147
-> Component props
148148

149-
-- | A simplified alias for `ComponentSpec`. This type is usually used to represent
150-
-- | the default component type returned from `createComponent`.
151-
-- type Component props = forall state action. ComponentSpec props state action
152-
153149
-- | Opaque component information for internal use.
154150
-- |
155151
-- | __*Note:* Never define a component with
@@ -291,7 +287,7 @@ foreign import make
291287
:: forall spec spec_ props state action
292288
. Union spec spec_ (ComponentSpec props state action)
293289
=> Component props
294-
-> { render :: Self props state action -> JSX | spec }
290+
-> { initialState :: state, render :: Self props state action -> JSX | spec }
295291
-> props
296292
-> JSX
297293

@@ -318,7 +314,7 @@ makeStateless
318314
-> props
319315
-> JSX
320316
makeStateless component render =
321-
make component { render: \self -> render self.props }
317+
make component { initialState: unit, render: \self -> render self.props }
322318

323319
-- | Represents rendered React VDOM (the result of calling `React.createElement`
324320
-- | in JavaScript).

src/React/Basic/Components/Async.purs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module React.Basic.Components.Async
2+
( async
3+
, asyncWithLoader
4+
) where
5+
6+
import Prelude
7+
8+
import Data.Maybe (Maybe(..), fromMaybe)
9+
import Effect.Aff (Aff, Fiber, error, killFiber, launchAff, launchAff_)
10+
import Effect.Class (liftEffect)
11+
import React.Basic (Component, JSX, StateUpdate(..), createComponent, empty, make, send)
12+
13+
component :: Component (Aff JSX)
14+
component = createComponent "Async"
15+
16+
data FetchAction
17+
= ReplaceFiber (Fiber Unit)
18+
| UpdateJSX JSX
19+
20+
async :: Aff JSX -> JSX
21+
async = asyncWithLoader empty
22+
23+
asyncWithLoader :: JSX -> Aff JSX -> JSX
24+
asyncWithLoader loader = make component
25+
{ initialState
26+
, update
27+
, render
28+
, didMount: launch
29+
-- , didUpdate: No! Implementing `didUpdate` breaks the
30+
-- Aff/Component lifecycle relationship.
31+
-- To update the Aff over time, wrap this
32+
-- component with `keyed`.
33+
, willUnmount: cleanup
34+
}
35+
where
36+
initialState =
37+
{ jsx: Nothing
38+
, pendingFiber: pure unit
39+
}
40+
41+
update { props, state } = case _ of
42+
ReplaceFiber newFiber ->
43+
UpdateAndSideEffects
44+
state { jsx = Nothing, pendingFiber = newFiber }
45+
\_ -> kill state.pendingFiber
46+
47+
UpdateJSX jsx ->
48+
Update
49+
state { jsx = Just jsx }
50+
51+
render self =
52+
fromMaybe loader self.state.jsx
53+
54+
launch self = do
55+
fiber <- launchAff do
56+
jsx <- self.props
57+
liftEffect $ send self $ UpdateJSX jsx
58+
send self $ ReplaceFiber fiber
59+
60+
cleanup self =
61+
kill self.state.pendingFiber
62+
63+
kill =
64+
launchAff_ <<< killFiber (error "Cancelled")

0 commit comments

Comments
 (0)