diff --git a/examples/async/.gitignore b/examples/async/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/async/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/async/Makefile b/examples/async/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/async/Makefile @@ -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 + diff --git a/examples/async/README.md b/examples/async/README.md new file mode 100644 index 0000000..8d2e1b8 --- /dev/null +++ b/examples/async/README.md @@ -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. diff --git a/examples/async/html/index.html b/examples/async/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/async/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/async/package.json b/examples/async/package.json new file mode 100644 index 0000000..583c22b --- /dev/null +++ b/examples/async/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "react": "16.6.0", + "react-dom": "16.6.0" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/async/src/AsyncCounter.purs b/examples/async/src/AsyncCounter.purs new file mode 100644 index 0000000..ff0be72 --- /dev/null +++ b/examples/async/src/AsyncCounter.purs @@ -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 + ] + + diff --git a/examples/async/src/Main.purs b/examples/async/src/Main.purs new file mode 100644 index 0000000..08480c7 --- /dev/null +++ b/examples/async/src/Main.purs @@ -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 diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 3dd0715..4b247c2 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -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 @@ -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 @@ -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). diff --git a/src/React/Basic/Components/Async.purs b/src/React/Basic/Components/Async.purs new file mode 100644 index 0000000..17f3991 --- /dev/null +++ b/src/React/Basic/Components/Async.purs @@ -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")