Skip to content

Add async component #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/async/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
output
html/index.js
package-lock.json
node_modules
8 changes: 8 additions & 0 deletions examples/async/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
all: node_modules
purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs'
purs bundle -m Main --main Main output/*/*.js > output/bundle.js
node_modules/.bin/browserify output/bundle.js -o html/index.js

node_modules:
npm install

12 changes: 12 additions & 0 deletions examples/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Async Counter Example

## Building

```
npm install
make all
```

This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle.

Then open `html/index.html` in your browser.
10 changes: 10 additions & 0 deletions examples/async/html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>react-basic example</title>
</head>
<body>
<div id="container"></div>
<script src="index.js"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions examples/async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"react": "16.6.0",
"react-dom": "16.6.0"
},
"devDependencies": {
"browserify": "16.2.3"
}
}
51 changes: 51 additions & 0 deletions examples/async/src/AsyncCounter.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module AsyncCounter where

import Prelude

import Effect.Aff (Milliseconds(..), delay)
import Effect.Class (liftEffect)
import Effect.Console (log)
import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, fragment, keyed, make)
import React.Basic.Components.Async (asyncWithLoader)
import React.Basic.DOM as R

component :: Component Props
component = createComponent "AsyncCounter"

type Props =
{ label :: String
}

data Action
= Increment

asyncCounter :: Props -> JSX
asyncCounter = make component { initialState, update, render }
where
initialState = { counter: 0 }

update self = case _ of
Increment ->
Update self.state { counter = self.state.counter + 1 }

render self =
fragment
[ R.p_ [ R.text "Notes:" ]
, R.ol_
[ R.li_ [ R.text "The two counts should never be out of sync" ]
, 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)" ]
]
, R.button
{ onClick: capture_ self Increment
, children: [ R.text (self.props.label <> ": " <> show self.state.counter) ]
}
, R.text " "
, keyed (show self.state.counter) $
asyncWithLoader (R.text "Loading...") do
liftEffect $ log "start"
delay $ Milliseconds 2000.0
liftEffect $ log "done"
pure $ R.text $ "Done: " <> show self.state.counter
]


22 changes: 22 additions & 0 deletions examples/async/src/Main.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Main where

import Prelude

import AsyncCounter (asyncCounter)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Exception (throw)
import React.Basic.DOM (render)
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)

main :: Effect Unit
main = do
container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window)
case container of
Nothing -> throw "Container element not found."
Just c ->
let app = asyncCounter { label: "Async Increment" }
in render app c
8 changes: 2 additions & 6 deletions src/React/Basic.purs
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,6 @@ foreign import createComponent
. String
-> Component props

-- | A simplified alias for `ComponentSpec`. This type is usually used to represent
-- | the default component type returned from `createComponent`.
-- type Component props = forall state action. ComponentSpec props state action

-- | Opaque component information for internal use.
-- |
-- | __*Note:* Never define a component with
Expand Down Expand Up @@ -291,7 +287,7 @@ foreign import make
:: forall spec spec_ props state action
. Union spec spec_ (ComponentSpec props state action)
=> Component props
-> { render :: Self props state action -> JSX | spec }
-> { initialState :: state, render :: Self props state action -> JSX | spec }
-> props
-> JSX

Expand All @@ -318,7 +314,7 @@ makeStateless
-> props
-> JSX
makeStateless component render =
make component { render: \self -> render self.props }
make component { initialState: unit, render: \self -> render self.props }

-- | Represents rendered React VDOM (the result of calling `React.createElement`
-- | in JavaScript).
Expand Down
64 changes: 64 additions & 0 deletions src/React/Basic/Components/Async.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module React.Basic.Components.Async
( async
, asyncWithLoader
) where

import Prelude

import Data.Maybe (Maybe(..), fromMaybe)
import Effect.Aff (Aff, Fiber, error, killFiber, launchAff, launchAff_)
import Effect.Class (liftEffect)
import React.Basic (Component, JSX, StateUpdate(..), createComponent, empty, make, send)

component :: Component (Aff JSX)
component = createComponent "Async"

data FetchAction
= ReplaceFiber (Fiber Unit)
| UpdateJSX JSX

async :: Aff JSX -> JSX
async = asyncWithLoader empty

asyncWithLoader :: JSX -> Aff JSX -> JSX
asyncWithLoader loader = make component
{ initialState
, update
, render
, didMount: launch
-- , didUpdate: No! Implementing `didUpdate` breaks the
-- Aff/Component lifecycle relationship.
-- To update the Aff over time, wrap this
-- component with `keyed`.
, willUnmount: cleanup
}
where
initialState =
{ jsx: Nothing
, pendingFiber: pure unit
}

update { props, state } = case _ of
ReplaceFiber newFiber ->
UpdateAndSideEffects
state { jsx = Nothing, pendingFiber = newFiber }
\_ -> kill state.pendingFiber

UpdateJSX jsx ->
Update
state { jsx = Just jsx }

render self =
fromMaybe loader self.state.jsx

launch self = do
fiber <- launchAff do
jsx <- self.props
liftEffect $ send self $ UpdateJSX jsx
send self $ ReplaceFiber fiber

cleanup self =
kill self.state.pendingFiber

kill =
launchAff_ <<< killFiber (error "Cancelled")