diff --git a/bower.json b/bower.json index d0a28e5..7c0d96c 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,12 @@ { "name": "purescript-react-basic", "license": "Apache-2.0", - "ignore": ["**/.*", "node_modules", "bower_components", "output"], + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], "repository": { "type": "git", "url": "git://github.com/lumihq/purescript-react-basic.git" @@ -17,7 +22,9 @@ "purescript-typelevel-prelude": "^3.0.0", "purescript-unsafe-coerce": "^4.0.0", "purescript-web-dom": "^1.0.0", - "purescript-web-events": "^1.0.0" + "purescript-web-events": "^1.0.0", + "purescript-profunctor-lenses": "^5.0.0", + "purescript-indexed-monad": "^1.0.0" }, "devDependencies": { "purescript-web-html": "^1.0.0" diff --git a/examples/component/README.md b/examples/component/README.md index cc747f4..aef84a0 100644 --- a/examples/component/README.md +++ b/examples/component/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/component/package.json b/examples/component/package.json index 624a6f3..fbcf2be 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 1e6e877..e960282 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -2,18 +2,19 @@ module Container where import Prelude -import React.Basic (Component, JSX, createComponent, makeStateless) +import React.Basic (CreateComponent, component, element) +import React.Basic as React import React.Basic.DOM as R -import ToggleButton (toggleButton) +import ToggleButton (mkToggleButton) -component :: Component Unit -component = createComponent "Container" +mkToggleButtonContainer :: CreateComponent {} Unit +mkToggleButtonContainer = do + toggleButton <- mkToggleButton -toggleButtonContainer :: JSX -toggleButtonContainer = unit # makeStateless component \_ -> - R.div - { children: - [ toggleButton { label: "A" } - , toggleButton { label: "B" } - ] - } + component "Container" \_ -> + React.pure $ R.div + { children: + [ element toggleButton { label: "A" } + , element toggleButton { label: "B" } + ] + } diff --git a/examples/component/src/Main.purs b/examples/component/src/Main.purs index 67193a8..3c32365 100644 --- a/examples/component/src/Main.purs +++ b/examples/component/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import Container (toggleButtonContainer) +import Container (mkToggleButtonContainer) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = toggleButtonContainer - in render app c + Just c -> do + toggleButtonContainer <- mkToggleButtonContainer + let app = element toggleButtonContainer {} + render app c diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 49430ae..d3fea55 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,40 +3,24 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make) +import React.Basic (CreateComponent, UseEffect, UseState, component, toKey, useEffect, useState, (/\)) +import React.Basic as React import React.Basic.DOM as R - -component :: Component Props -component = createComponent "ToggleButton" - -type Props = - { label :: String - } - -data Action - = Toggle - -toggleButton :: Props -> JSX -toggleButton = make component - { initialState: - { on: false +import React.Basic.DOM.Events (capture_) + +mkToggleButton :: CreateComponent { label :: String } (UseEffect (UseState Boolean Unit)) +mkToggleButton = do + component "ToggleButton" \{ label } -> React.do + on /\ setOn <- useState false + + useEffect [toKey on] do + log $ "State: " <> if on then "On" else "Off" + pure (pure unit) + + React.pure $ R.button + { onClick: capture_ $ setOn not + , children: + [ R.text label + , R.text if on then " On" else " Off" + ] } - - , update: \self -> case _ of - Toggle -> - UpdateAndSideEffects - self.state { on = not self.state.on } - \nextSelf -> do - log $ "next state: " <> show nextSelf.state - - , render: \self -> - R.button - { onClick: capture_ self Toggle - , children: - [ R.text self.props.label - , R.text if self.state.on - then " On" - else " Off" - ] - } - } diff --git a/examples/controlled-input/README.md b/examples/controlled-input/README.md index 2004b87..a36ddf3 100644 --- a/examples/controlled-input/README.md +++ b/examples/controlled-input/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/controlled-input/package.json b/examples/controlled-input/package.json index 624a6f3..fbcf2be 100644 --- a/examples/controlled-input/package.json +++ b/examples/controlled-input/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index 7824483..845730e 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -2,44 +2,51 @@ module ControlledInput where import Prelude +import Control.Applicative.Indexed (ipure) import Data.Maybe (Maybe(..), fromMaybe, maybe) -import React.Basic (Component, JSX, StateUpdate(..), capture, createComponent, make) +import React.Basic (CreateComponent, Render, UseState, component, fragment, useState, (/\)) import React.Basic as React import React.Basic.DOM as R -import React.Basic.DOM.Events (targetValue, timeStamp) -import React.Basic.Events (merge) - -component :: Component Props -component = createComponent "ControlledInput" - -type Props = Unit - -data Action - = ValueChanged String Number - -controlledInput :: Props -> JSX -controlledInput = make component - { initialState: - { value: "hello world" - , timestamp: Nothing - } - - , update: \self -> case _ of - ValueChanged value timestamp -> - Update self.state - { value = value - , timestamp = Just timestamp - } +import React.Basic.DOM.Events (capture, targetValue, timeStamp) +import React.Basic.Events (EventHandler, merge) + +mkControlledInput :: CreateComponent {} (UseInput () (UseInput () Unit)) +mkControlledInput = + component "ControlledInput" \props -> React.do + firstName <- useInput "hello" + lastName <- useInput "world" + + React.pure $ R.form_ + [ renderInput firstName + , renderInput lastName + ] + where + renderInput input = + fragment + [ R.input { onChange: input.onChange, value: input.value } + , R.p_ [ R.text ("Current value = " <> show input.value) ] + , R.p_ [ R.text ("Changed at = " <> maybe "never" show input.lastChanged) ] + ] - , render: \self -> - React.fragment - [ R.input - { onChange: - capture self (merge { targetValue, timeStamp }) - \{ timeStamp, targetValue } -> ValueChanged (fromMaybe "" targetValue) timeStamp - , value: self.state.value +type InputState state = { value :: String, lastChanged :: Maybe Number | state } + +type UseInput state hooks = UseState (InputState state) hooks +useInput + :: forall hooks + . String + -> Render hooks + (UseInput () hooks) + (InputState ( onChange :: EventHandler )) +useInput initialValue = React.do + { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } + ipure + { onChange: capture + (merge { targetValue, timeStamp }) + \{ timeStamp, targetValue } -> do + replaceState \_ -> + { value: fromMaybe "" targetValue + , lastChanged: Just timeStamp } - , R.p_ [ R.text ("Current value = " <> show self.state.value) ] - , R.p_ [ R.text ("Changed at = " <> maybe "never" show self.state.timestamp) ] - ] - } + , value + , lastChanged + } diff --git a/examples/controlled-input/src/Main.purs b/examples/controlled-input/src/Main.purs index 8651738..2af6387 100644 --- a/examples/controlled-input/src/Main.purs +++ b/examples/controlled-input/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import ControlledInput (controlledInput) +import ControlledInput (mkControlledInput) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = controlledInput unit - in render app c + Just c -> do + controlledInput <- mkControlledInput + let app = element controlledInput {} + render app c diff --git a/examples/counter/README.md b/examples/counter/README.md index f2418c0..88847e5 100644 --- a/examples/counter/README.md +++ b/examples/counter/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/counter/package.json b/examples/counter/package.json index 583c22b..4386ff4 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "16.6.0", - "react-dom": "16.6.0" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "16.2.3" diff --git a/examples/counter/src/Counter.js b/examples/counter/src/Counter.js new file mode 100644 index 0000000..97f35c0 --- /dev/null +++ b/examples/counter/src/Counter.js @@ -0,0 +1,7 @@ +"use strict"; + +exports.setDocumentTitle = function(title) { + return function() { + document.title = title; + }; +}; diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index cbea449..9fa9a6e 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -2,31 +2,24 @@ module Counter where import Prelude - -import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make) +import Effect (Effect) +import React.Basic (CreateComponent, UseEffect, UseState, component, toKey, useEffect, useState, (/\)) +import React.Basic as React import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) -component :: Component Props -component = createComponent "Counter" - -type Props = - { label :: String - } - -data Action - = Increment +mkCounter :: CreateComponent {} (UseEffect (UseState Int Unit)) +mkCounter = do + component "Counter" \props -> React.do + counter /\ setCounter <- useState 0 -counter :: Props -> JSX -counter = make component { initialState, update, render } - where - initialState = { counter: 0, dummy: 0 } + useEffect [toKey counter] do + setDocumentTitle $ "Count: " <> show counter + pure mempty - update self = case _ of - Increment -> - Update self.state { counter = self.state.counter + 1 } + React.pure $ R.button + { onClick: capture_ $ setCounter (_ + 1) + , children: [ R.text $ "Increment: " <> show counter ] + } - render self = - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } +foreign import setDocumentTitle :: String -> Effect Unit diff --git a/examples/counter/src/Main.purs b/examples/counter/src/Main.purs index fa07fd4..b702611 100644 --- a/examples/counter/src/Main.purs +++ b/examples/counter/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import Counter (counter) +import Counter (mkCounter) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = counter { label: "Increment" } - in render app c + Just c -> do + counter <- mkCounter + let app = element counter {} + render app c diff --git a/examples/legacy-v2/README.md b/examples/legacy-v2/README.md index f2418c0..7e1799b 100644 --- a/examples/legacy-v2/README.md +++ b/examples/legacy-v2/README.md @@ -1,10 +1,9 @@ -# Counter Example +# Legacy Example ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/legacy-v2/package.json b/examples/legacy-v2/package.json index 624a6f3..4386ff4 100644 --- a/examples/legacy-v2/package.json +++ b/examples/legacy-v2/package.json @@ -1,9 +1,9 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { - "browserify": "^16.2.2" + "browserify": "16.2.3" } } diff --git a/examples/legacy-v2/src/Compat.purs b/examples/legacy-v2/src/Compat.purs deleted file mode 100644 index 30a32cc..0000000 --- a/examples/legacy-v2/src/Compat.purs +++ /dev/null @@ -1,49 +0,0 @@ -module React.Basic.Compat - ( Component - , component - , stateless - , module React.Basic - ) where - -import Prelude - -import Effect (Effect) -import React.Basic (JSX, ReactComponent, Self, StateUpdate(..), createComponent, element, elementKeyed, empty, fragment, make, makeStateless, send, toReactComponent) - -type Component = ReactComponent - --- | Supports a common subset of the v2 API to ease the upgrade process -component - :: forall props state - . { displayName :: String - , initialState :: { | state } - , receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit - , render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX - } - -> ReactComponent { | props } -component { displayName, initialState, receiveProps, render } = - toReactComponent identity (createComponent displayName) - { initialState: initialState - , didMount: receiveProps <<< selfToLegacySelf - , didUpdate: receiveProps <<< selfToLegacySelf - , update: \self stateUpdate -> Update (stateUpdate self.state) - , render: render <<< selfToLegacySelf - } - where - selfToLegacySelf self@{ props, state } = - { props - , state - , setState: send self - } - --- | Supports a common subset of the v2 API to ease the upgrade process -stateless - :: forall props - . { displayName :: String - , render :: { | props } -> JSX - } - -> ReactComponent { | props } -stateless { displayName, render } = - toReactComponent identity (createComponent displayName) - { render: \self -> render self.props - } diff --git a/examples/legacy-v2/src/LegacyCounter.purs b/examples/legacy-v2/src/LegacyCounter.purs index 74d8e26..b578ccb 100644 --- a/examples/legacy-v2/src/LegacyCounter.purs +++ b/examples/legacy-v2/src/LegacyCounter.purs @@ -2,7 +2,7 @@ module LegacyCounter where import Prelude -import React.Basic.Compat (Component, component, element, stateless) +import React.Basic.Compat (Component, UseStatefulComponent, component, element, stateless) import React.Basic.DOM as R import React.Basic.Events as Events @@ -11,7 +11,7 @@ type Props = } -- | checks `component` -legacyCounter :: Component Props +legacyCounter :: Component Props (UseStatefulComponent { counter :: Int }) legacyCounter = component { displayName: "LegacyCounter", initialState, receiveProps, render } where initialState = @@ -29,7 +29,7 @@ legacyCounter = component { displayName: "LegacyCounter", initialState, receiveP } -- | checks `stateless` -buttonLabel :: Component { label :: String, counter :: Int } +buttonLabel :: Component { label :: String, counter :: Int } Unit buttonLabel = stateless { displayName: "ButtonLabel", render } where render props = diff --git a/examples/reducer/.gitignore b/examples/reducer/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/reducer/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/reducer/Makefile b/examples/reducer/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/reducer/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/reducer/README.md b/examples/reducer/README.md new file mode 100644 index 0000000..8c2ad2e --- /dev/null +++ b/examples/reducer/README.md @@ -0,0 +1,11 @@ +# Reducer Example + +## Building + +```sh +make +``` + +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/reducer/html/index.html b/examples/reducer/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/reducer/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/reducer/package.json b/examples/reducer/package.json new file mode 100644 index 0000000..4386ff4 --- /dev/null +++ b/examples/reducer/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/reducer/src/Main.purs b/examples/reducer/src/Main.purs new file mode 100644 index 0000000..0c52b92 --- /dev/null +++ b/examples/reducer/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import Reducer (mkReducer) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (element) +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 -> do + reducer <- mkReducer + let app = element reducer {} + render app c diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Reducer.purs new file mode 100644 index 0000000..36778bd --- /dev/null +++ b/examples/reducer/src/Reducer.purs @@ -0,0 +1,35 @@ +module Reducer where + +import Prelude + +import React.Basic (CreateComponent, UseReducer, component, fragment, useReducer, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) + +data Action + = Increment + | Decrement + +mkReducer :: CreateComponent {} (UseReducer { counter :: Int } Action Unit) +mkReducer = do + component "Reducer" \props -> React.do + + state /\ dispatch <- + useReducer { counter: 0 } \state -> case _ of + Increment -> state { counter = state.counter + 1 } + Decrement -> state { counter = state.counter - 1 } + + React.pure $ fragment + [ R.button + { onClick: capture_ $ dispatch Decrement + , children: [ R.text $ "Decrement" ] + } + , R.button + { onClick: capture_ $ dispatch Increment + , children: [ R.text $ "Increment" ] + } + , R.div_ + [ R.text $ show state.counter + ] + ] diff --git a/examples/refs/.gitignore b/examples/refs/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/refs/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/refs/Makefile b/examples/refs/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/refs/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/refs/README.md b/examples/refs/README.md new file mode 100644 index 0000000..baa86ae --- /dev/null +++ b/examples/refs/README.md @@ -0,0 +1,11 @@ +# Refs Example + +## Building + +```sh +make +``` + +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/refs/html/index.html b/examples/refs/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/refs/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/refs/package.json b/examples/refs/package.json new file mode 100644 index 0000000..4386ff4 --- /dev/null +++ b/examples/refs/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/refs/src/Main.purs b/examples/refs/src/Main.purs new file mode 100644 index 0000000..ec70e25 --- /dev/null +++ b/examples/refs/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import Refs (mkRefs) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (element) +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 -> do + refs <- mkRefs + let app = element refs {} + render app c diff --git a/examples/refs/src/Refs.purs b/examples/refs/src/Refs.purs new file mode 100644 index 0000000..7fd4a3f --- /dev/null +++ b/examples/refs/src/Refs.purs @@ -0,0 +1,90 @@ +module Refs where + +import Prelude + +import Data.Int (round) +import Data.Maybe (Maybe(..)) +import Data.Nullable (Nullable, null) +import Math (pow, sqrt) +import React.Basic (CreateComponent, Ref, Render, UseEffect, UseRef, UseState, Tuple, component, fragment, readRefMaybe, useEffect, useRef, useState, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM (Node) +import Web.Event.Event (EventType(..)) +import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener) +import Web.HTML (window) +import Web.HTML.HTMLElement (getBoundingClientRect) +import Web.HTML.HTMLElement as HTMLElement +import Web.HTML.Window as Window + +mkRefs :: CreateComponent {} (UseNodeDistance (UseNodeDistance (UseNodeDistance Unit))) +mkRefs = do + component "Refs" \props -> React.do + + mouseDistance1 /\ buttonRef1 <- useNodeDistanceFromMouse + mouseDistance2 /\ buttonRef2 <- useNodeDistanceFromMouse + mouseDistance3 /\ buttonRef3 <- useNodeDistanceFromMouse + + React.pure $ fragment + [ R.button + { ref: buttonRef1 + , children: [ R.text $ show mouseDistance1 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "20px", left: "200px" } + } + , R.button + { ref: buttonRef2 + , children: [ R.text $ show mouseDistance2 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "60px", left: "40px" } + } + , R.button + { ref: buttonRef3 + , children: [ R.text $ show mouseDistance3 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "120px", left: "90px" } + } + ] + +type UseNodeDistance hooks = UseEffect (UseState Int (UseRef (Nullable Node) hooks)) + +useNodeDistanceFromMouse + :: forall hooks + . Render + hooks + (UseNodeDistance hooks) + (Tuple Int (Ref (Nullable Node))) +useNodeDistanceFromMouse = React.do + elementRef <- useRef null + mouseDistance /\ setMouseDistance <- useState 0 + + useEffect [] do + maybeElement <- map (HTMLElement.fromNode =<< _) (readRefMaybe elementRef) + case maybeElement of + Nothing -> pure (pure unit) + Just element -> do + mouseMoveListener <- eventListener \e -> do + { top, bottom, left, right } <- getBoundingClientRect element + let + mouseX = (unsafeCoerce e).clientX + mouseY = (unsafeCoerce e).clientY + distanceX = + if mouseX # between left right + then 0.0 + else if mouseX < left + then left - mouseX + else mouseX - right + distanceY = + if mouseY # between top bottom + then 0.0 + else if mouseY < top + then top - mouseY + else mouseY - bottom + distance = sqrt ((distanceX `pow` 2.0) + (distanceY `pow` 2.0)) + setMouseDistance \_ -> round distance + + let mouseMoveEventType = EventType "mousemove" + windowEventTarget <- map Window.toEventTarget window + addEventListener mouseMoveEventType mouseMoveListener false windowEventTarget + pure do + removeEventListener mouseMoveEventType mouseMoveListener false windowEventTarget + + React.pure (mouseDistance /\ elementRef) diff --git a/examples/todo-app/.gitignore b/examples/todo-app/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/todo-app/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/todo-app/Makefile b/examples/todo-app/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/todo-app/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/todo-app/README.md b/examples/todo-app/README.md new file mode 100644 index 0000000..55d0bc0 --- /dev/null +++ b/examples/todo-app/README.md @@ -0,0 +1,11 @@ +# Todo App Example + +## Building + +```sh +make +``` + +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/todo-app/html/index.html b/examples/todo-app/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/todo-app/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json new file mode 100644 index 0000000..80aec4d --- /dev/null +++ b/examples/todo-app/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2", + "uuid": "3.3.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/todo-app/src/Main.purs b/examples/todo-app/src/Main.purs new file mode 100644 index 0000000..86e4dac --- /dev/null +++ b/examples/todo-app/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import TodoApp (mkTodoApp) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (element) +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 -> do + todoApp <- mkTodoApp + let app = element todoApp {} + render app c diff --git a/examples/todo-app/src/TodoApp.purs b/examples/todo-app/src/TodoApp.purs new file mode 100644 index 0000000..2b34c5b --- /dev/null +++ b/examples/todo-app/src/TodoApp.purs @@ -0,0 +1,159 @@ +module TodoApp where + +import Prelude + +import Data.Array as Array +import Data.Foldable (traverse_) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import React.Basic (CreateComponent, UseReducer, UseState, component, element, elementKeyed, empty, memo, useReducer, useState, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture, capture_, targetValue) +import React.Basic.Events as Events + +data Action + = CreateTodo String + | ToggleTodo Int + | DeleteTodo Int + | SetFilter TodoFilter + +data TodoFilter + = All + | Complete + | Incomplete +derive instance eqTodoFilter :: Eq TodoFilter + +type Todo = + { task :: String + , isComplete :: Boolean + } + +type State = + { todos :: Array Todo + , filter :: TodoFilter + } + +reducer :: State -> Action -> State +reducer state = case _ of + CreateTodo task -> + state { todos = Array.cons { task, isComplete: false } state.todos } + + ToggleTodo index -> + case Array.modifyAt index (\todo -> todo { isComplete = not todo.isComplete }) state.todos of + Just todos -> + state { todos = todos } + Nothing -> + state + + DeleteTodo index -> + case Array.deleteAt index state.todos of + Just todos -> + state { todos = todos } + Nothing -> + state + + SetFilter filter -> + state { filter = filter } + +mkTodoApp :: CreateComponent {} (UseReducer State Action Unit) +mkTodoApp = do + let initialState = { todos: [], filter: All } + todoInput <- memo mkTodoInput + todoRow <- memo mkTodoRow + todoFilters <- memo mkTodoFilters + + component "TodoApp" \props -> React.do + state /\ dispatch <- useReducer initialState reducer + React.pure $ R.div + { children: + [ element todoInput { dispatch } + , R.div_ $ flip Array.mapWithIndex state.todos \id todo -> + if state.filter == All || + (todo.isComplete && state.filter == Complete) || + (not todo.isComplete && state.filter == Incomplete) + then elementKeyed todoRow { key: show id, id, todo, dispatch } + else empty + , element todoFilters { filter: state.filter, dispatch } + ] + , style: R.css + { maxWidth: "600px" + , margin: "auto" + , padding: "16px" + , fontFamily: "sans-serif" + , fontSize: "16px" + } + } + + where + todoAppEl = element $ R.unsafeCreateDOMComponent "todo-app" + +mkTodoInput :: CreateComponent { dispatch :: Action -> Effect Unit } (UseState String Unit) +mkTodoInput = do + component "TodoInput" \props -> React.do + value /\ setValue <- useState "" + React.pure $ R.form + { onSubmit: capture_ do + props.dispatch $ CreateTodo value + setValue $ const "" + , children: + [ R.input + { value + , onChange: capture targetValue $ traverse_ (setValue <<< const) + , style: R.css { lineHeight: "32px", width: "100%", boxSizing: "border-box" } + , placeholder: "Enter a task" + } + ] + , style: R.css { marginBottom: "16px", width: "100%" } + } + +mkTodoRow :: CreateComponent { id :: Int, todo :: Todo, dispatch :: Action -> Effect Unit } Unit +mkTodoRow = component "Todo" \props -> React.do + React.pure $ R.div + { children: + [ R.label + { children: + [ R.input + { type: "checkbox" + , checked: props.todo.isComplete + , onChange: Events.handler_ $ props.dispatch $ ToggleTodo props.id + , tabIndex: "0" + } + , R.text props.todo.task + ] + , style: R.css { lineHeight: "32px", fontSize: "24px", flex: "1 0 auto" } + } + , R.a + { children: [ R.text "❌" ] + , onClick: capture_ $ props.dispatch $ DeleteTodo props.id + , style: R.css { cursor: "pointer" } + } + ] + , style: R.css + { display: "flex" + , flexFlow: "row" + , alignItems: "center" + } + } + +mkTodoFilters :: CreateComponent { filter :: TodoFilter, dispatch :: Action -> Effect Unit } Unit +mkTodoFilters = component "TodoFilters" \props -> React.do + let + filterLink f label = R.a + { children: [ R.text label ] + , onClick: capture_ $ props.dispatch $ SetFilter f + , style: if props.filter == f + then R.css { cursor: "pointer", fontWeight: "bold" } + else R.css { cursor: "pointer" } + } + React.pure $ R.div + { children: + [ R.hr { style: R.css { color: "lightgrey" } } + , filterLink All "All" + , R.text " / " + , filterLink Complete "Complete" + , R.text " / " + , filterLink Incomplete "Incomplete" + ] + , style: R.css { marginTop: "16px" } + } diff --git a/generated-docs/React/Basic.md b/generated-docs/React/Basic.md index 2832ee1..3a3a8ac 100644 --- a/generated-docs/React/Basic.md +++ b/generated-docs/React/Basic.md @@ -1,354 +1,227 @@ ## Module React.Basic -#### `ComponentSpec` +#### `Component` ``` purescript -type ComponentSpec props state initialState action = { initialState :: initialState, update :: Self props state action -> action -> StateUpdate props state action, render :: Self props state action -> JSX, shouldUpdate :: Self props state action -> props -> state -> Boolean, didMount :: Self props state action -> Effect Unit, didUpdate :: Self props state action -> Effect Unit, willUnmount :: Self props state action -> Effect Unit, "$$type" :: ComponentType props state action } +newtype Component props hooks ``` -`ComponentSpec` represents a React-Basic component implementation. - -These are the properties your component definition may override -with specific implementations. None are required to be overridden, unless -an overridden function interacts with `state`, in which case `initialState` -is required (the compiler enforces this). While you _can_ use `state` and -dispatch actions without defining `update`, doing so doesn't make much sense -so the default `update` implementation will emit a warning. - -- `initialState` - - The component's starting state. - - Avoid mirroring prop values in state. -- `update` - - All state updates go through `update`. - - `update` is called when `send` is used to dispatch an action. - - State changes are described using `StateUpdate`. Only `Update` and `UpdateAndSideEffects` will cause rerenders and a call to `didUpdate`. - - Side effects requested are only invoked _after_ any corrosponding state update has completed its render cycle and the changes have been applied. This means it is safe to interact with the DOM in a side effect, for example. -- `render` - - Takes a current snapshot of the component (`Self`) and converts it to renderable `JSX`. -- `shouldUpdate` - - Can be useful for occasional performance optimizations. Rarely necessary. -- `didMount` - - The React component's `componentDidMount` lifecycle. Useful for initiating an action on first mount, such as fetching data from a server. -- `didUpdate` - - The React component's `componentDidUpdate` lifecycle. Rarely necessary. -- `willUnmount` - - The React component's `componentWillUpdate` lifecycle. Any subscriptions or timers created in `didMount` or `didUpdate` should be disposed of here. - -The component spec is generally not exported from your component -module and this type is rarely used explicitly. The simplified alias -`Component` is usually sufficient, and `make` will validate whether -your component's types line up. +A React component -For example: +#### `CreateComponent` -```purs -component :: Component -component = createComponent "Counter" - -type Props = - { label :: String - } - -data Action - = Increment +``` purescript +type CreateComponent props hooks = Effect (Component props hooks) +``` -counter :: Props -> JSX -counter = make component - { initialState = { counter: 0 } +Alias for convenience. Creating components is effectful because +React uses the function instance as the component's "identity" +or "type". - , update = \self action -> case action of - Increment -> - Update self.state { counter = self.state.counter + 1 } +#### `JSX` - , render = \self -> - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } - } +``` purescript +data JSX :: Type ``` -This example component overrides `initialState`, `update`, and `render`. - -__*Note:* A `ComponentSpec` is *not* a valid React component by itself. If you would like to use - a React-Basic component from JavaScript, use `toReactComponent`.__ +Represents rendered React VDOM (the result of calling `React.createElement` +in JavaScript). -__*Note:* `$$type` is for internal use only. It needs to be on the type to - preserve its existence during a record update, as in the example above.__ +`JSX` is a `Monoid`: -__*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ +- `append` + - Merge two `JSX` nodes using `React.Fragment`. +- `mempty` + - The `empty` node; renders nothing. -#### `createComponent` +__*Hint:* Many useful utility functions already exist for Monoids. For example, + `guard` can be used to conditionally render a subtree of components.__ +##### Instances ``` purescript -createComponent :: forall props state action. String -> ComponentSpec props state Unit action +Semigroup JSX +Monoid JSX ``` -Creates a `ComponentSpec` with a given Display Name. - -The resulting component spec is usually given the simplified `Component` type: +#### `component` -```purs -component :: Component -component = createComponent "Counter" +``` purescript +component :: forall hooks props. String -> (props -> Render Unit hooks JSX) -> CreateComponent props hooks ``` -This function should be used at the module level and considered side effecting. -This is because React uses referential equality when deciding whether a new -`JSX` tree is a valid update, or if it needs to be replaced entirely -(expensive and clears component state lower in the tree). - -__*Note:* A `Component` is *not* a valid React component by itself. If you would like to use - a React-Basic component from JavaScript, use `toReactComponent`.__ - -__*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ +Create a React component given a display name and render function. -#### `Component` +#### `UseState` ``` purescript -type Component = forall props state action. ComponentSpec props state Unit action +data UseState :: Type -> Type -> Type ``` -A simplified alias for `ComponentSpec`. This type is usually used to represent -the default component type returned from `createComponent`. - -#### `ComponentType` +#### `useState` ``` purescript -data ComponentType props state action +useState :: forall hooks state. state -> Render hooks (UseState state hooks) (Tuple state ((state -> state) -> Effect Unit)) ``` -Opaque component information for internal use. - -__*For the curious:* This is the "class" React will use to render and - identify the component. It receives the `ComponentSpec` as a prop and knows - how to defer behavior to it. It requires very specific props and is not useful by - itself from JavaScript. For JavaScript interop, see `toReactComponent`.__ - -#### `StateUpdate` +#### `UseEffect` ``` purescript -data StateUpdate props state action - = NoUpdate - | Update state - | SideEffects (Self props state action -> Effect Unit) - | UpdateAndSideEffects state (Self props state action -> Effect Unit) +data UseEffect :: Type -> Type ``` -Used by the `update` function to describe the kind of state update and/or side -effects desired. - -__*See also:* `ComponentSpec`__ - -#### `Self` +#### `useEffect` ``` purescript -type Self props state action = { props :: props, state :: state, instance_ :: ReactComponentInstance } +useEffect :: forall hooks. Array Key -> Effect (Effect Unit) -> Render hooks (UseEffect hooks) Unit ``` -`Self` represents the component instance at a particular point in time. - -- `props` - - A snapshot of `props` taken when this `Self` was created. -- `state` - - A snapshot of `state` taken when this `Self` was created. -- `instance_` - - Unsafe escape hatch to the underlying component instance (`this` in the JavaScript React paradigm). Avoid as much as possible, but it's still frequently better than rewriting an entire component in JavaScript. - -__*See also:* `ComponentSpec`, `send`, `capture`, `readProps`, `readState`__ - -#### `send` +#### `UseReducer` ``` purescript -send :: forall props state action. Self props state action -> action -> Effect Unit +data UseReducer :: Type -> Type -> Type -> Type ``` -Dispatch an `action` into the component to be handled by `update`. - -__*See also:* `update`, `capture`__ - -#### `sendAsync` +#### `useReducer` ``` purescript -sendAsync :: forall props state action. Self props state action -> Aff action -> Effect Unit +useReducer :: forall hooks state action. ToKey state => state -> (state -> action -> state) -> Render hooks (UseReducer state action hooks) (Tuple state (action -> Effect Unit)) ``` -Convenience function for sending an action when an `Aff` completes. - -__*Note:* Potential failure should be handled in the given `Aff` and converted - to an action, as the default error handler will simply log the error to - the console.__ - -__*See also:* `send`__ - -#### `capture` +#### `UseRef` ``` purescript -capture :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler +data UseRef :: Type -> Type -> Type ``` -Create a capturing\* `EventHandler` to send an action when an event occurs. For -more complicated event handlers requiring `Effect`, use `handler` from `React.Basic.Events`. - -__\*calls `preventDefault` and `stopPropagation`__ - -__*See also:* `update`, `capture_`, `monitor`, `React.Basic.Events`__ - -#### `capture_` +#### `Ref` ``` purescript -capture_ :: forall props state action. Self props state action -> action -> EventHandler +data Ref :: Type -> Type ``` -Like `capture`, but for actions which don't need to extract information from the Event. - -__*See also:* `update`, `capture`, `monitor_`__ - -#### `monitor` +#### `readRef` ``` purescript -monitor :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler +readRef :: forall a. Ref a -> Effect a ``` -Like `capture`, but does not cancel the event. - -__*See also:* `update`, `capture`, `monitor\_`__ - -#### `monitor_` +#### `readRefMaybe` ``` purescript -monitor_ :: forall props state action. Self props state action -> action -> EventHandler +readRefMaybe :: forall a. Ref (Nullable a) -> Effect (Maybe a) ``` -Like `capture_`, but does not cancel the event. - -__*See also:* `update`, `monitor`, `capture_`, `React.Basic.Events`__ - -#### `readProps` +#### `writeRef` ``` purescript -readProps :: forall props state action. Self props state action -> Effect props +writeRef :: forall a. Ref a -> a -> Effect Unit ``` -Read the most up to date `props` directly from the component instance -associated with this `Self`. - -_Note: This function is for specific, asynchronous edge cases. - Generally, the `props` snapshot on `Self` is sufficient. - -__*See also:* `Self`__ - -#### `readState` +#### `renderRef` ``` purescript -readState :: forall props state action. Self props state action -> Effect state +renderRef :: forall hooks a. Ref a -> Render hooks hooks a ``` -Read the most up to date `state` directly from the component instance -associated with this `Self`. - -_Note: This function is for specific, asynchronous edge cases. - Generally, the `state` snapshot on `Self` is sufficient. - -__*See also:* `Self`__ - -#### `make` +#### `renderRefMaybe` ``` purescript -make :: forall props state action. ComponentSpec props state state action -> props -> JSX +renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe a) ``` -Turn a `Component` into a usable render function. -This is where you will want to provide customized implementations: +#### `useRef` -```purs -component :: Component -component = createComponent "Counter" +``` purescript +useRef :: forall hooks a. a -> Render hooks (UseRef a hooks) (Ref a) +``` -type Props = - { label :: String - } +#### `Key` -data Action - = Increment +``` purescript +data Key +``` -counter :: Props -> JSX -counter = make component - { initialState = { counter: 0 } +Keys represent values React uses to check for changes. +This is done using JavaScript's reference equality (`===`), +so complicated types may want to implement `ToKey` so that +it returns a primative like a `String`. A timestamp appended +to a unique ID, for example. Less strict cases can implement +`ToKey` using `unsafeToKey`, while some extreme cases may +need a hashing or stringifying mechanism. - , update = \self action -> case action of - Increment -> - Update self.state { counter = self.state.counter + 1 } +#### `ToKey` - , render = \self -> - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } - } +``` purescript +class ToKey a where + toKey :: a -> Key ``` -__*See also:* `makeStateless`, `createComponent`, `Component`, `ComponentSpec`__ +##### Instances +``` purescript +ToKey String +ToKey Int +ToKey Number +ToKey Boolean +ToKey { | a } +ToKey (Array a) +ToKey (Nullable a) +ToKey (Maybe a) +``` -#### `makeStateless` +#### `unsafeToKey` ``` purescript -makeStateless :: forall props. ComponentSpec props Unit Unit Unit -> (props -> JSX) -> props -> JSX +unsafeToKey :: forall a. a -> Key ``` -Makes stateless component definition slightly less verbose: - -```purs -component :: Component -component = createComponent "Xyz" +#### `empty` -myComponent :: Props -> JSX -myComponent = makeStateless component \props -> JSX +``` purescript +empty :: JSX ``` -__*Note:* The only difference between a stateless React-Basic component and - a plain `props -> JSX` function is the presense of the component name - in React's dev tools and error stacks. It's just a conceptual boundary. - If this isn't important simply write a `props -> JSX` function.__ +An empty `JSX` node. This is often useful when you would like to conditionally +show something, but you don't want to (or can't) modify the `children` prop +on the parent node. -__*See also:* `make`, `createComponent`, `Component`, `ComponentSpec`__ +__*See also:* `JSX`, Monoid `guard`__ -#### `JSX` +#### `Render` ``` purescript -data JSX :: Type +newtype Render x y a ``` -Represents rendered React VDOM (the result of calling `React.createElement` -in JavaScript). - -`JSX` is a `Monoid`: +Render represents the effects allowed within a React component's +body, i.e. during "render". This includes hooks and ends with +returning JSX (see `pure`), but does not allow arbitrary side +effects. -- `append` - - Merge two `JSX` nodes using `React.Fragment`. -- `mempty` - - The `empty` node; renders nothing. +##### Instances +``` purescript +IxFunctor Render +IxApply Render +IxBind Render +IxApplicative Render +``` -__*Hint:* Many useful utility functions already exist for Monoids. For example, - `guard` can be used to conditionally render a subtree of components.__ +#### `bind` -##### Instances ``` purescript -Semigroup JSX -Monoid JSX +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b ``` -#### `empty` +#### `discard` ``` purescript -empty :: JSX +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b ``` -An empty `JSX` node. This is often useful when you would like to conditionally -show something, but you don't want to (or can't) modify the `children` prop -on the parent node. +#### `pure` -__*See also:* `JSX`, Monoid `guard`__ +``` purescript +pure :: forall a x m. IxApplicative m => a -> m x x a +``` #### `keyed` @@ -375,68 +248,105 @@ __*See also:* `JSX`__ #### `element` ``` purescript -element :: forall props. ReactComponent { | props } -> { | props } -> JSX +element :: forall hooks props. Component { | props } hooks -> { | props } -> JSX ``` -Create a `JSX` node from a `ReactComponent`, by providing the props. +Create a `JSX` node from a `Component`, by providing the props. This function is for non-React-Basic React components, such as those imported from FFI. -__*See also:* `ReactComponent`, `elementKeyed`__ +__*See also:* `Component`, `elementKeyed`__ #### `elementKeyed` ``` purescript -elementKeyed :: forall props. ReactComponent { | props } -> { key :: String | props } -> JSX +elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: String | props } -> JSX ``` -Create a `JSX` node from a `ReactComponent`, by providing the props and a key. - -This function is for non-React-Basic React components, such as those -imported from FFI. +Create a `JSX` node from a `Component`, by providing the props and a key. -__*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__ +__*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ -#### `ReactComponent` +#### `displayName` ``` purescript -data ReactComponent props +displayName :: forall hooks props. Component props hooks -> String ``` -Represents a traditional React component. Useful for JavaScript interop and -FFI. For example: +Retrieve the Display Name from a `Component`. Useful for debugging and improving +error messages in logs. + +__*See also:* `component`__ -```purs -foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String } -``` -__*See also:* `element`, `toReactComponent`__ +### Re-exported from Data.Tuple: -#### `ReactComponentInstance` +#### `Tuple` ``` purescript -data ReactComponentInstance +data Tuple a b + = Tuple a b ``` -An opaque representation of a React component's instance (`this` in the JavaScript -React paradigm). It exists as an escape hatch to unsafe behavior. Use it with -caution. +A simple product type for wrapping a pair of component values. -#### `toReactComponent` +##### Instances +``` purescript +(Show a, Show b) => Show (Tuple a b) +(Eq a, Eq b) => Eq (Tuple a b) +(Eq a) => Eq1 (Tuple a) +(Ord a, Ord b) => Ord (Tuple a b) +(Ord a) => Ord1 (Tuple a) +(Bounded a, Bounded b) => Bounded (Tuple a b) +Semigroupoid Tuple +(Semigroup a, Semigroup b) => Semigroup (Tuple a b) +(Monoid a, Monoid b) => Monoid (Tuple a b) +(Semiring a, Semiring b) => Semiring (Tuple a b) +(Ring a, Ring b) => Ring (Tuple a b) +(CommutativeRing a, CommutativeRing b) => CommutativeRing (Tuple a b) +(HeytingAlgebra a, HeytingAlgebra b) => HeytingAlgebra (Tuple a b) +(BooleanAlgebra a, BooleanAlgebra b) => BooleanAlgebra (Tuple a b) +Functor (Tuple a) +FunctorWithIndex Unit (Tuple a) +Invariant (Tuple a) +Bifunctor Tuple +(Semigroup a) => Apply (Tuple a) +Biapply Tuple +(Monoid a) => Applicative (Tuple a) +Biapplicative Tuple +(Semigroup a) => Bind (Tuple a) +(Monoid a) => Monad (Tuple a) +Extend (Tuple a) +Comonad (Tuple a) +(Lazy a, Lazy b) => Lazy (Tuple a b) +Foldable (Tuple a) +Foldable1 (Tuple a) +FoldableWithIndex Unit (Tuple a) +Bifoldable Tuple +Traversable (Tuple a) +Traversable1 (Tuple a) +TraversableWithIndex Unit (Tuple a) +Bitraversable Tuple +(TypeEquals a Unit) => Distributive (Tuple a) +``` + +### Re-exported from Data.Tuple.Nested: + +#### `tuple2` ``` purescript -toReactComponent :: forall jsProps props state action. ({ | jsProps } -> props) -> ComponentSpec props state state action -> ReactComponent { | jsProps } +tuple2 :: forall a b. a -> b -> Tuple2 a b ``` -Convert a React-Basic `ComponentSpec` to a JavaScript-friendly React component. -This function should only be used for JS interop and not normal React-Basic usage. +Given 2 values, creates a 2-tuple. -__*Note:* Like `createComponent`, `toReactComponent` is side effecting in that - it creates a "class" React will see as unique each time it's called. Lift - any usage up to the module level, usage in `render` or any other function, - and applying any type classes to the `props`.__ +#### `(/\)` -__*See also:* `ReactComponent`__ +``` purescript +infixr 6 Tuple as /\ +``` +Shorthand for constructing n-tuples as nested pairs. +`a /\ b /\ c /\ d /\ unit` becomes `Tuple a (Tuple b (Tuple c (Tuple d unit)))` diff --git a/generated-docs/React/Basic/Compat.md b/generated-docs/React/Basic/Compat.md new file mode 100644 index 0000000..3f32360 --- /dev/null +++ b/generated-docs/React/Basic/Compat.md @@ -0,0 +1,129 @@ +## Module React.Basic.Compat + +#### `UseStatefulComponent` + +``` purescript +type UseStatefulComponent state = UseEffect (UseState state Unit) +``` + +#### `component` + +``` purescript +component :: forall props state. { displayName :: String, initialState :: { | state }, receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit, render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX } -> Component { | props } (UseStatefulComponent { | state }) +``` + +Supports a common subset of the v2 API to ease the upgrade process + +#### `stateless` + +``` purescript +stateless :: forall props. { displayName :: String, render :: { | props } -> JSX } -> Component { | props } Unit +``` + +Supports a common subset of the v2 API to ease the upgrade process + + +### Re-exported from React.Basic: + +#### `UseState` + +``` purescript +data UseState :: Type -> Type -> Type +``` + +#### `UseEffect` + +``` purescript +data UseEffect :: Type -> Type +``` + +#### `JSX` + +``` purescript +data JSX :: Type +``` + +Represents rendered React VDOM (the result of calling `React.createElement` +in JavaScript). + +`JSX` is a `Monoid`: + +- `append` + - Merge two `JSX` nodes using `React.Fragment`. +- `mempty` + - The `empty` node; renders nothing. + +__*Hint:* Many useful utility functions already exist for Monoids. For example, + `guard` can be used to conditionally render a subtree of components.__ + +##### Instances +``` purescript +Semigroup JSX +Monoid JSX +``` + +#### `Component` + +``` purescript +newtype Component props hooks +``` + +A React component + +#### `keyed` + +``` purescript +keyed :: String -> JSX -> JSX +``` + +Apply a React key to a subtree. React-Basic usually hides React's warning about +using `key` props on components in an Array, but keys are still important for +any dynamic lists of child components. + +__*See also:* React's documentation regarding the special `key` prop__ + +#### `fragment` + +``` purescript +fragment :: Array JSX -> JSX +``` + +Render an Array of children without a wrapping component. + +__*See also:* `JSX`__ + +#### `empty` + +``` purescript +empty :: JSX +``` + +An empty `JSX` node. This is often useful when you would like to conditionally +show something, but you don't want to (or can't) modify the `children` prop +on the parent node. + +__*See also:* `JSX`, Monoid `guard`__ + +#### `elementKeyed` + +``` purescript +elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: String | props } -> JSX +``` + +Create a `JSX` node from a `Component`, by providing the props and a key. + +__*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ + +#### `element` + +``` purescript +element :: forall hooks props. Component { | props } hooks -> { | props } -> JSX +``` + +Create a `JSX` node from a `Component`, by providing the props. + +This function is for non-React-Basic React components, such as those +imported from FFI. + +__*See also:* `Component`, `elementKeyed`__ + diff --git a/generated-docs/React/Basic/DOM.md b/generated-docs/React/Basic/DOM.md index 7afcb7b..4a4fc47 100644 --- a/generated-docs/React/Basic/DOM.md +++ b/generated-docs/React/Basic/DOM.md @@ -71,18 +71,6 @@ if an app existed and was unmounted successfully. Note: Relies on `ReactDOM.unmountComponentAtNode` -#### `findDOMNode` - -``` purescript -findDOMNode :: ReactComponentInstance -> Effect (Either Error Node) -``` - -Returns the current DOM node associated with the given -instance, or an Error if no node was found or the given -instance was not mounted. - -Note: Relies on `ReactDOM.findDOMNode` - #### `createPortal` ``` purescript @@ -2178,7 +2166,7 @@ a :: forall attrs attrs_. Union attrs attrs_ (SharedProps Props_a) => { | attrs #### `SharedProps` ``` purescript -type SharedProps specific = (key :: String, about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) +type SharedProps specific = (key :: String, ref :: Ref (Nullable Node), about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) ``` Standard props which are shared by all DOM elements. @@ -2194,6 +2182,6 @@ An abstract type representing records of CSS attributes. #### `unsafeCreateDOMComponent` ``` purescript -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit ``` diff --git a/generated-docs/React/Basic/DOM/Events.md b/generated-docs/React/Basic/DOM/Events.md index 4b86a99..29f8cdb 100644 --- a/generated-docs/React/Basic/DOM/Events.md +++ b/generated-docs/React/Basic/DOM/Events.md @@ -2,6 +2,18 @@ This module defines safe DOM event function and property accessors. +#### `capture` + +``` purescript +capture :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler +``` + +#### `capture_` + +``` purescript +capture_ :: Effect Unit -> EventHandler +``` + #### `bubbles` ``` purescript diff --git a/generated-docs/React/Basic/DOM/Internal.md b/generated-docs/React/Basic/DOM/Internal.md index 8594101..0ace6b5 100644 --- a/generated-docs/React/Basic/DOM/Internal.md +++ b/generated-docs/React/Basic/DOM/Internal.md @@ -11,7 +11,7 @@ An abstract type representing records of CSS attributes. #### `SharedProps` ``` purescript -type SharedProps specific = (key :: String, about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) +type SharedProps specific = (key :: String, ref :: Ref (Nullable Node), about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) ``` Standard props which are shared by all DOM elements. @@ -19,7 +19,7 @@ Standard props which are shared by all DOM elements. #### `unsafeCreateDOMComponent` ``` purescript -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit ``` diff --git a/src/React/Basic.js b/src/React/Basic.js index 2ffd725..3b56355 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,151 +3,48 @@ var React = require("react"); var Fragment = React.Fragment || "div"; -exports.createComponent = (function() { - // Begin component prototype functions - // (`this`-dependent, defined outside `createComponent` - // for a slight performance boost) - function toSelf() { - var self = { - props: this.props.$$props, - state: this.state === null ? null : this.state.$$state, - instance_: this +exports.memo_ = React.memo; + +exports.useState_ = function(tuple, initialState) { + var r = React.useState(initialState); + var state = r[0]; + var setState = r[1]; + return tuple(state, function(update) { + return function() { + return setState(update); }; - return self; - } - - function shouldComponentUpdate(nextProps, nextState) { - var shouldUpdate = this.$$spec.shouldUpdate; - return shouldUpdate === undefined - ? true - : shouldUpdate(this.toSelf())(nextProps.$$props)( - nextState === null ? null : nextState.$$state - ); - } - - function componentDidMount() { - var didMount = this.$$spec.didMount; - if (didMount !== undefined) { - didMount(this.toSelf())(); - } - } - - function componentDidUpdate() { - var didUpdate = this.$$spec.didUpdate; - if (didUpdate !== undefined) { - didUpdate(this.toSelf())(); - } - } - - function componentWillUnmount() { - this.$$mounted = false; - var willUnmount = this.$$spec.willUnmount; - if (willUnmount !== undefined) { - willUnmount(this.toSelf())(); - } - } - - function render() { - return this.$$spec.render(this.toSelf()); - } - // End component prototype functions - - return function(displayName) { - var Component = function constructor(props) { - this.$$mounted = true; - this.$$spec = props.$$spec; - this.state = - // React may optimize components with no state, - // so we leave state null if it was left as - // the default value. - this.$$spec.initialState === undefined - ? null - : { $$state: this.$$spec.initialState }; - return this; - }; - - Component.displayName = displayName; - Component.prototype = Object.create(React.Component.prototype); - Component.prototype.constructor = Component; - Component.prototype.toSelf = toSelf; - Component.prototype.shouldComponentUpdate = shouldComponentUpdate; - Component.prototype.componentDidMount = componentDidMount; - Component.prototype.componentDidUpdate = componentDidUpdate; - Component.prototype.componentWillUnmount = componentWillUnmount; - Component.prototype.render = render; - - return Component; - }; -})(); - -exports.send_ = function(buildStateUpdate) { - return function(self, action) { - if (!self.instance_.$$mounted) { - exports.warningUnmountedComponentAction(self, action); - return; - } - if (self.instance_.$$spec.update === undefined) { - exports.warningDefaultUpdate(self, action); - return; - } - var sideEffects = null; - self.instance_.setState( - function(s) { - var setStateContext = self.instance_.toSelf(); - setStateContext.state = s.$$state; - var updates = buildStateUpdate( - self.instance_.$$spec.update(setStateContext)(action) - ); - if (updates.effects !== null) { - sideEffects = updates.effects; - } - if (updates.state !== null && updates.state !== s.$$state) { - return { $$state: updates.state }; - } else { - return null; - } - }, - function() { - if (sideEffects !== null) { - sideEffects(this.toSelf())(); - } - } - ); - }; + }); }; -exports.readProps = function(self) { - return self.instance_.props.$$props; +exports.useEffect_ = React.useEffect; + +exports.useLayoutEffect_ = React.useLayoutEffect; + +exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { + var r = React.useReducer(reducer, initialState, initialAction); + var state = r[0]; + var dispatch = r[1]; + return tuple(state, function(action) { + return function() { + return dispatch(action); + }; + }); }; -exports.readState = function(self) { - var state = self.instance_.state; - return state === null ? null : state.$$state; +exports.useRef_ = React.useRef; + +exports.readRef_ = function(ref) { + return ref.current; }; -exports.make = function(_unionDict) { - return function($$type) { - return function($$spec) { - var $$specPadded = { - initialState: $$spec.initialState, - update: $$spec.update, - render: $$spec.render, - shouldUpdate: $$spec.shouldUpdate, - didMount: $$spec.didMount, - didUpdate: $$spec.didUpdate, - willUnmount: $$spec.willUnmount - }; - return function($$props) { - var props = { - $$props: $$props, - $$spec: $$specPadded - }; - return React.createElement($$type, props); - }; - }; - }; +exports.writeRef_ = function(ref, a) { + ref.current = a; }; +// exports.useContext_ = React.useContext; + +exports.useMemo_ = React.useMemo; + exports.empty = null; exports.keyed_ = function(key, child) { @@ -167,75 +64,16 @@ exports.fragment = function(children) { return React.createElement.apply(null, [Fragment, {}].concat(children)); }; -exports.displayNameFromComponent = function($$type) { - return $$type.displayName || "[unknown]"; +exports.displayName = function(component) { + return typeof component === "string" + ? component + : component.displayName || "[unknown]"; }; -exports.displayNameFromSelf = function(self) { - return exports.displayNameFromComponent(self.instance_.constructor); -}; - -exports.toReactComponent = function(_unionDict) { - return function(fromJSProps) { - return function($$type) { - return function($$spec) { - var $$specPadded = { - initialState: $$spec.initialState, - update: $$spec.update, - render: $$spec.render, - shouldUpdate: $$spec.shouldUpdate, - didMount: $$spec.didMount, - didUpdate: $$spec.didUpdate, - willUnmount: $$spec.willUnmount - }; - - var Component = function constructor() { - return this; - }; - - Component.prototype = Object.create(React.Component.prototype); - - Component.displayName = $$type.displayName + " (Wrapper)"; - - Component.prototype.render = function() { - var props = { - $$props: fromJSProps(this.props), - $$spec: $$specPadded - }; - return React.createElement($$type, props); - }; - - return Component; - }; - }; +exports.unsafeSetDisplayName = function(displayName, component) { + component.displayName = displayName; + component.toString = function() { + return displayName; }; -}; - -exports.warningDefaultUpdate = function(self, action) { - console.error( - "A " + - exports.displayNameFromSelf(self) + - " component received an action but has no `update` function defined. Override the default `update` function to handle this action." - ); - console.error("Self:", self); - console.error("Action:", action); -}; - -exports.warningUnmountedComponentAction = function(self, action) { - console.error( - "An unmounted " + - exports.displayNameFromSelf(self) + - " component received the action below. Actions received by unmounted components usually indicate a memory leak. Make sure to unsubscribe from any async work in `willUnmount`." - ); - console.error("Self:", self); - console.error("Action:", action); -}; - -exports.warningFailedAsyncAction = function(self, error) { - console.error( - "An async action failed in a " + - exports.displayNameFromSelf(self) + - " component." - ); - console.error(error); + return component; }; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index b6bae67..3a5438f 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,321 +1,212 @@ module React.Basic - ( ComponentSpec - , createComponent - , Component - , StateUpdate(..) - , Self - , send - , sendAsync - , capture - , capture_ - , monitor - , monitor_ - , readProps - , readState - , make - , makeStateless + ( Component + , CreateComponent , JSX + , component + , memo + , UseState + , useState + , UseEffect + , useEffect + , UseLayoutEffect + , useLayoutEffect + , UseReducer + , useReducer + , UseRef + , Ref + , readRef + , readRefMaybe + , writeRef + , renderRef + , renderRefMaybe + , useRef + -- , UseContext + -- , Context + -- , useContext + , UseMemo + , useMemo + , Key + , class ToKey + , toKey + , unsafeToKey , empty + , Render + , bind + , discard + , pure , keyed , fragment , element , elementKeyed - , displayNameFromComponent - , displayNameFromSelf - , ReactComponent - , ReactComponentInstance - , toReactComponent + , displayName + , module Data.Tuple + , module Data.Tuple.Nested ) where -import Prelude - -import Data.Either (Either(..)) -import Data.Function.Uncurried (Fn1, Fn2, runFn1, runFn2) -import Data.Nullable (Nullable, notNull, null) +import Prelude hiding (bind,discard,pure) + +import Control.Applicative.Indexed (class IxApplicative, ipure) +import Control.Apply.Indexed (class IxApply) +import Control.Bind.Indexed (class IxBind, ibind) +import Data.Function.Uncurried (Fn2, mkFn2, runFn2) +import Data.Functor.Indexed (class IxFunctor) +import Data.Maybe (Maybe) +import Data.Nullable (Nullable, toMaybe, toNullable) +import Data.Tuple (Tuple(..)) +import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Aff (Aff, Error, runAff_) -import Effect.Uncurried (EffectFn2, runEffectFn2) -import React.Basic.DOM.Events (preventDefault, stopPropagation) -import React.Basic.Events (EventFn, EventHandler, SyntheticEvent, handler) -import Type.Row (class Union) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) +import Prelude (bind, pure) as Prelude +import Unsafe.Coerce (unsafeCoerce) --- | `ComponentSpec` represents a React-Basic component implementation. --- | --- | These are the properties your component definition may override --- | with specific implementations. None are required to be overridden, unless --- | an overridden function interacts with `state`, in which case `initialState` --- | is required (the compiler enforces this). While you _can_ use `state` and --- | dispatch actions without defining `update`, doing so doesn't make much sense --- | so the default `update` implementation will emit a warning. --- | --- | - `initialState` --- | - The component's starting state. --- | - Avoid mirroring prop values in state. --- | - `update` --- | - All state updates go through `update`. --- | - `update` is called when `send` is used to dispatch an action. --- | - State changes are described using `StateUpdate`. Only `Update` and `UpdateAndSideEffects` will cause rerenders and a call to `didUpdate`. --- | - Side effects requested are only invoked _after_ any corrosponding state update has completed its render cycle and the changes have been applied. This means it is safe to interact with the DOM in a side effect, for example. --- | - `render` --- | - Takes a current snapshot of the component (`Self`) and converts it to renderable `JSX`. --- | - `shouldUpdate` --- | - Can be useful for occasional performance optimizations. Rarely necessary. --- | - `didMount` --- | - The React component's `componentDidMount` lifecycle. Useful for initiating an action on first mount, such as fetching data from a server. --- | - `didUpdate` --- | - The React component's `componentDidUpdate` lifecycle. Rarely necessary. --- | - `willUnmount` --- | - The React component's `componentWillUpdate` lifecycle. Any subscriptions or timers created in `didMount` or `didUpdate` should be disposed of here. --- | --- | The component spec is generally not exported from your component --- | module and this type is rarely used explicitly. The simplified alias --- | `Component` is usually sufficient, and `make` will validate whether --- | your component's types line up. --- | --- | For example: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | --- | type Props = --- | { label :: String --- | } --- | --- | data Action --- | = Increment --- | --- | counter :: Props -> JSX --- | counter = make component --- | { initialState = { counter: 0 } --- | --- | , update = \self action -> case action of --- | Increment -> --- | Update self.state { counter = self.state.counter + 1 } --- | --- | , render = \self -> --- | R.button --- | { onClick: capture_ self Increment --- | , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] --- | } --- | } --- | ``` --- | --- | This example component overrides `initialState`, `update`, and `render`. --- | --- | __*Note:* A `ComponentSpec` is *not* a valid React component by itself. If you would like to use --- | a React-Basic component from JavaScript, use `toReactComponent`.__ --- | --- | __*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ -type ComponentSpec props state action = - ( initialState :: state - , update :: Self props state action -> action -> StateUpdate props state action - , render :: Self props state action -> JSX - , shouldUpdate :: Self props state action -> props -> state -> Boolean - , didMount :: Self props state action -> Effect Unit - , didUpdate :: Self props state action -> Effect Unit - , willUnmount :: Self props state action -> Effect Unit - ) - --- | Creates a `ComponentSpec` with a given Display Name. --- | --- | The resulting component spec is usually given the simplified `Component` type: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | ``` --- | --- | This function should be used at the module level and considered side effecting. --- | This is because React uses referential equality when deciding whether a new --- | `JSX` tree is a valid update, or if it needs to be replaced entirely --- | (expensive and clears component state lower in the tree). --- | --- | __*Note:* A specific type for the props in `Component props` should always be chosen at this point. --- | It's technically possible to declare the component with `forall props. Component props` --- | but doing so is unsafe. Leaving the prop type open allows the use of a single `Component` --- | definition in multiple React-Basic components that may have different prop types. Because --- | component lifecycles are managed by React, it's possible for incompatible prop values to --- | be passed into a lifecycle function.__ --- | --- | __*Note:* A `Component` is *not* a valid React component by itself. If you would like to use --- | a React-Basic component from JavaScript, use `toReactComponent`.__ --- | --- | __*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ -foreign import createComponent - :: forall props +-- | A React component +newtype Component props hooks = Component (EffectFn1 props JSX) + +-- | Alias for convenience. Creating components is effectful because +-- | React uses the function instance as the component's "identity" +-- | or "type". +type CreateComponent props hooks = Effect (Component props hooks) + +-- | Create a React component given a display name and render function. +component + :: forall hooks props . String - -> Component props + -> (props -> Render Unit hooks JSX) + -> CreateComponent props hooks +component name renderFn = + let c = Component (mkEffectFn1 (\props -> case renderFn props of Render a -> a)) + in runEffectFn2 unsafeSetDisplayName name c + +memo + :: forall hooks props + . CreateComponent hooks props + -> CreateComponent hooks props +memo = flip Prelude.bind (runEffectFn1 memo_) + +foreign import data UseState :: Type -> Type -> Type + +useState + :: forall hooks state + . state + -> Render hooks (UseState state hooks) (Tuple state ((state -> state) -> Effect Unit)) +useState initialState = Render do + runEffectFn2 useState_ (mkFn2 Tuple) initialState + +foreign import data UseEffect :: Type -> Type + +useEffect + :: forall hooks + . Array Key + -> Effect (Effect Unit) + -> Render hooks (UseEffect hooks) Unit +useEffect keys effect = Render (runEffectFn2 useEffect_ effect keys) + +foreign import data UseLayoutEffect :: Type -> Type + +useLayoutEffect + :: forall hooks + . Array Key + -> Effect (Effect Unit) + -> Render hooks (UseLayoutEffect hooks) Unit +useLayoutEffect keys effect = Render (runEffectFn2 useLayoutEffect_ effect keys) --- | 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 +foreign import data UseReducer :: Type -> Type -> Type -> Type --- | Opaque component information for internal use. --- | --- | __*For the curious:* This is the "class" React will use to render and --- | identify the component. It receives the `ComponentSpec` as a prop and knows --- | how to defer behavior to it. It requires very specific props and is not useful by --- | itself from JavaScript. For JavaScript interop, see `toReactComponent`.__ -data Component props - --- | Used by the `update` function to describe the kind of state update and/or side --- | effects desired. --- | --- | __*See also:* `ComponentSpec`__ -data StateUpdate props state action - = NoUpdate - | Update state - | SideEffects (Self props state action -> Effect Unit) - | UpdateAndSideEffects state (Self props state action -> Effect Unit) - --- | `Self` represents the component instance at a particular point in time. --- | --- | - `props` --- | - A snapshot of `props` taken when this `Self` was created. --- | - `state` --- | - A snapshot of `state` taken when this `Self` was created. --- | - `instance_` --- | - Unsafe escape hatch to the underlying component instance (`this` in the JavaScript React paradigm). Avoid as much as possible, but it's still frequently better than rewriting an entire component in JavaScript. --- | --- | __*See also:* `ComponentSpec`, `send`, `capture`, `readProps`, `readState`__ -type Self props state action = - { props :: props - , state :: state - , instance_ :: ReactComponentInstance - } - --- | Dispatch an `action` into the component to be handled by `update`. --- | --- | __*See also:* `update`, `capture`__ -send :: forall props state action. Self props state action -> action -> Effect Unit -send = runEffectFn2 (runFn1 send_ buildStateUpdate) +useReducer + :: forall hooks state action + . ToKey state + => state + -> (state -> action -> state) + -> Render hooks (UseReducer state action hooks) (Tuple state (action -> Effect Unit)) +useReducer initialState reducer = Render do + runEffectFn3 useReducer_ + (mkFn2 Tuple) + (mkFn2 reducer) + initialState --- | Convenience function for sending an action when an `Aff` completes. --- | --- | __*Note:* Potential failure should be handled in the given `Aff` and converted --- | to an action, as the default error handler will simply log the error to --- | the console.__ --- | --- | __*See also:* `send`__ -sendAsync - :: forall props state action - . Self props state action - -> Aff action - -> Effect Unit -sendAsync self work = runAff_ handle work - where - handle (Right action) = send self action - handle (Left err) = runEffectFn2 warningFailedAsyncAction self err - --- | Create a capturing\* `EventHandler` to send an action when an event occurs. For --- | more complicated event handlers requiring `Effect`, use `handler` from `React.Basic.Events`. --- | --- | __\*calls `preventDefault` and `stopPropagation`__ --- | --- | __*See also:* `update`, `capture_`, `monitor`, `React.Basic.Events`__ -capture :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler -capture self eventFn = monitor self (preventDefault >>> stopPropagation >>> eventFn) +foreign import data UseRef :: Type -> Type -> Type --- | Like `capture`, but for actions which don't need to extract information from the Event. --- | --- | __*See also:* `update`, `capture`, `monitor_`__ -capture_ :: forall props state action. Self props state action -> action -> EventHandler -capture_ self action = capture self identity \_ -> action +foreign import data Ref :: Type -> Type --- | Like `capture`, but does not cancel the event. --- | --- | __*See also:* `update`, `capture`, `monitor\_`__ -monitor :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler -monitor self eventFn makeAction = handler eventFn \a -> send self (makeAction a) +useRef + :: forall hooks a + . a + -> Render hooks (UseRef a hooks) (Ref a) +useRef initialValue = Render do + runEffectFn1 useRef_ initialValue --- | Like `capture_`, but does not cancel the event. --- | --- | __*See also:* `update`, `monitor`, `capture_`, `React.Basic.Events`__ -monitor_ :: forall props state action. Self props state action -> action -> EventHandler -monitor_ self action = monitor self identity \_ -> action +readRef :: forall a. Ref a -> Effect a +readRef = runEffectFn1 readRef_ --- | Read the most up to date `props` directly from the component instance --- | associated with this `Self`. --- | --- | _Note: This function is for specific, asynchronous edge cases. --- | Generally, the `props` snapshot on `Self` is sufficient. --- | --- | __*See also:* `Self`__ -foreign import readProps :: forall props state action. Self props state action -> Effect props +readRefMaybe :: forall a. Ref (Nullable a) -> Effect (Maybe a) +readRefMaybe a = map toMaybe (readRef a) --- | Read the most up to date `state` directly from the component instance --- | associated with this `Self`. --- | --- | _Note: This function is for specific, asynchronous edge cases. --- | Generally, the `state` snapshot on `Self` is sufficient. --- | --- | __*See also:* `Self`__ -foreign import readState :: forall props state action. Self props state action -> Effect state +writeRef :: forall a. Ref a -> a -> Effect Unit +writeRef = runEffectFn2 writeRef_ --- | Turn a `Component` into a usable render function. --- | This is where you will want to provide customized implementations: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | --- | type Props = --- | { label :: String --- | } --- | --- | data Action --- | = Increment --- | --- | counter :: Props -> JSX --- | counter = make component --- | { initialState = { counter: 0 } --- | --- | , update = \self action -> case action of --- | Increment -> --- | Update self.state { counter = self.state.counter + 1 } --- | --- | , render = \self -> --- | R.button --- | { onClick: capture_ self Increment --- | , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] --- | } --- | } --- | ``` --- | --- | __*See also:* `makeStateless`, `createComponent`, `Component`, `ComponentSpec`__ -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 } - -> props - -> JSX +renderRef :: forall hooks a. Ref a -> Render hooks hooks a +renderRef ref = Render (readRef ref) --- | Makes stateless component definition slightly less verbose: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Xyz" --- | --- | myComponent :: Props -> JSX --- | myComponent = makeStateless component \props -> JSX --- | ``` --- | --- | __*Note:* The only difference between a stateless React-Basic component and --- | a plain `props -> JSX` function is the presense of the component name --- | in React's dev tools and error stacks. It's just a conceptual boundary. --- | If this isn't important simply write a `props -> JSX` function.__ --- | --- | __*See also:* `make`, `createComponent`, `Component`, `ComponentSpec`__ -makeStateless - :: forall props - . Component props - -> (props -> JSX) - -> props - -> JSX -makeStateless component render = - make component { render: \self -> render self.props } +renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe a) +renderRefMaybe a = Render (readRefMaybe a) + +-- foreign import data UseContext :: Type -> Type -> Type + +-- foreign import data Context :: Type -> Type + +-- useContext +-- :: forall hooks a +-- . Context a +-- -> Render hooks (UseContext a hooks) a +-- useContext context = Render (runEffectFn1 useContext_ context) + +foreign import data UseMemo :: Type -> Type -> Type + +useMemo + :: forall hooks a + . Array Key + -> (Unit -> a) + -> Render hooks (UseMemo a hooks) a +useMemo keys factory = Render (runEffectFn2 useMemo_ factory keys) + +-- | Keys represent values React uses to check for changes. +-- | This is done using JavaScript's reference equality (`===`), +-- | so complicated types may want to implement `ToKey` so that +-- | it returns a primative like a `String`. A timestamp appended +-- | to a unique ID, for example. Less strict cases can implement +-- | `ToKey` using `unsafeToKey`, while some extreme cases may +-- | need a hashing or stringifying mechanism. +data Key + +class ToKey a where + toKey :: a -> Key + +unsafeToKey :: forall a. a -> Key +unsafeToKey = unsafeCoerce + +instance trString :: ToKey String where + toKey = unsafeToKey + +instance trInt :: ToKey Int where + toKey = unsafeToKey + +instance trNumber :: ToKey Number where + toKey = unsafeToKey + +instance trBoolean :: ToKey Boolean where + toKey = unsafeToKey + +instance trRecord :: ToKey (Record a) where + toKey = unsafeToKey + +instance trArray :: ToKey (Array a) where + toKey = unsafeToKey + +instance trNullable :: ToKey (Nullable a) where + toKey = unsafeToKey + +instance trMaybe :: ToKey (Maybe a) where + toKey a = toKey (toNullable a) -- | Represents rendered React VDOM (the result of calling `React.createElement` -- | in JavaScript). @@ -344,6 +235,33 @@ instance monoidJSX :: Monoid JSX where -- | __*See also:* `JSX`, Monoid `guard`__ foreign import empty :: JSX +-- | Render represents the effects allowed within a React component's +-- | body, i.e. during "render". This includes hooks and ends with +-- | returning JSX (see `pure`), but does not allow arbitrary side +-- | effects. +newtype Render x y a = Render (Effect a) + +instance ixFunctorRender :: IxFunctor Render where + imap f (Render a) = Render (map f a) + +instance ixApplyRender :: IxApply Render where + iapply (Render f) (Render a) = Render (apply f a) + +instance ixBindRender :: IxBind Render where + ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) + +instance ixApplicativeRender :: IxApplicative Render where + ipure a = Render (Prelude.pure a) + +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +bind = ibind + +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +discard = ibind + +pure :: forall a x m. IxApplicative m => a -> m x x a +pure = ipure + -- | Apply a React key to a subtree. React-Basic usually hides React's warning about -- | using `key` props on components in an Array, but keys are still important for -- | any dynamic lists of child components. @@ -357,150 +275,119 @@ keyed = runFn2 keyed_ -- | __*See also:* `JSX`__ foreign import fragment :: Array JSX -> JSX --- | Create a `JSX` node from a `ReactComponent`, by providing the props. +-- | Create a `JSX` node from a `Component`, by providing the props. -- | -- | This function is for non-React-Basic React components, such as those -- | imported from FFI. -- | --- | __*See also:* `ReactComponent`, `elementKeyed`__ +-- | __*See also:* `Component`, `elementKeyed`__ element - :: forall props - . ReactComponent { | props } - -> { | props } + :: forall hooks props + . Component {| props } hooks + -> {| props } -> JSX -element = runFn2 element_ +element (Component c) props = runFn2 element_ c props --- | Create a `JSX` node from a `ReactComponent`, by providing the props and a key. --- | --- | This function is for non-React-Basic React components, such as those --- | imported from FFI. +-- | Create a `JSX` node from a `Component`, by providing the props and a key. -- | --- | __*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__ +-- | __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ elementKeyed - :: forall props - . ReactComponent { | props } + :: forall hooks props + . Component {| props } hooks -> { key :: String | props } -> JSX elementKeyed = runFn2 elementKeyed_ --- | Retrieve the Display Name from a `ComponentSpec`. Useful for debugging and improving --- | error messages in logs. --- | --- | __*See also:* `displayNameFromSelf`, `createComponent`__ -foreign import displayNameFromComponent - :: forall props - . Component props - -> String - --- | Retrieve the Display Name from a `Self`. Useful for debugging and improving +-- | Retrieve the Display Name from a `Component`. Useful for debugging and improving -- | error messages in logs. -- | --- | __*See also:* `displayNameFromComponent`, `createComponent`__ -foreign import displayNameFromSelf - :: forall props state action - . Self props state action +-- | __*See also:* `component`__ +foreign import displayName + :: forall hooks props + . Component props hooks -> String --- | Represents a traditional React component. Useful for JavaScript interop and --- | FFI. For example: --- | --- | ```purs --- | foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String } --- | ``` --- | --- | __*See also:* `element`, `toReactComponent`__ -data ReactComponent props - --- | An opaque representation of a React component's instance (`this` in the JavaScript --- | React paradigm). It exists as an escape hatch to unsafe behavior. Use it with --- | caution. -data ReactComponentInstance - --- | Convert a React-Basic `ComponentSpec` to a JavaScript-friendly React component. --- | This function should only be used for JS interop and not normal React-Basic usage. --- | --- | __*Note:* Like `createComponent`, `toReactComponent` is side effecting in that --- | it creates a "class" React will see as unique each time it's called. Lift --- | any usage up to the module level, usage in `render` or any other function, --- | and applying any type classes to the `props`.__ --- | --- | __*See also:* `ReactComponent`__ -foreign import toReactComponent - :: forall spec spec_ jsProps props state action - . Union spec spec_ (ComponentSpec props state action) - => ({ | jsProps } -> props) - -> Component props - -> { render :: Self props state action -> JSX | spec } - -> ReactComponent { | jsProps } -- | -- | Internal utility or FFI functions -- | -buildStateUpdate - :: forall props state action - . StateUpdate props state action - -> { state :: Nullable state - , effects :: Nullable (Self props state action -> Effect Unit) - } -buildStateUpdate = case _ of - NoUpdate -> - { state: null - , effects: null - } - Update state_ -> - { state: notNull state_ - , effects: null - } - SideEffects effects -> - { state: null - , effects: notNull effects - } - UpdateAndSideEffects state_ effects -> - { state: notNull state_ - , effects: notNull effects - } - -foreign import send_ - :: forall props state action - . Fn1 - (StateUpdate props state action - -> { state :: Nullable state - , effects :: Nullable (Self props state action -> Effect Unit) - }) - (EffectFn2 - (Self props state action) - action - Unit) - -foreign import keyed_ :: Fn2 String JSX JSX - -foreign import element_ - :: forall props - . Fn2 (ReactComponent { | props }) { | props } JSX +foreign import memo_ + :: forall hooks props + . EffectFn1 + (Component props hooks) + (Component props hooks) -foreign import elementKeyed_ - :: forall props - . Fn2 (ReactComponent { | props }) { key :: String | props } JSX +foreign import unsafeSetDisplayName + :: forall hooks props + . EffectFn2 String (Component props hooks) (Component props hooks) -foreign import warningDefaultUpdate - :: forall props state action +foreign import useState_ + :: forall state . EffectFn2 - (Self props state action) - action + (forall a b. Fn2 a b (Tuple a b)) + state + (Tuple state ((state -> state) -> Effect Unit)) + +foreign import useEffect_ + :: EffectFn2 + (Effect (Effect Unit)) + (Array Key) Unit -foreign import warningUnmountedComponentAction - :: forall props state action - . EffectFn2 - (Self props state action) - action +foreign import useLayoutEffect_ + :: EffectFn2 + (Effect (Effect Unit)) + (Array Key) Unit -foreign import warningFailedAsyncAction - :: forall props state action +foreign import useReducer_ + :: forall state action + . EffectFn3 + (forall a b. Fn2 a b (Tuple a b)) + (Fn2 state action state) + state + (Tuple state (action -> Effect Unit)) + +foreign import readRef_ + :: forall a + . EffectFn1 + (Ref a) + a + +foreign import writeRef_ + :: forall a . EffectFn2 - (Self props state action) - Error + (Ref a) + a Unit + +foreign import useRef_ + :: forall a + . EffectFn1 + a + (Ref a) + +-- foreign import useContext_ +-- :: forall a +-- . EffectFn1 +-- (Context a) +-- a + +foreign import useMemo_ + :: forall a + . EffectFn2 + (Unit -> a) + (Array Key) + a + +foreign import keyed_ :: Fn2 String JSX JSX + +foreign import element_ + :: forall props + . Fn2 (EffectFn1 { | props } JSX) { | props } JSX + +foreign import elementKeyed_ + :: forall hooks props + . Fn2 (Component {| props } hooks) { key :: String | props } JSX diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs new file mode 100644 index 0000000..94e951a --- /dev/null +++ b/src/React/Basic/Compat.purs @@ -0,0 +1,43 @@ +module React.Basic.Compat + ( UseStatefulComponent + , component + , stateless + , module React.Basic + ) where + +import Prelude + +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import React.Basic (Component, JSX, UseEffect, UseState, element, elementKeyed, empty, fragment, keyed) +import React.Basic (Tuple(..), bind, discard, pure, component, toKey, useEffect, useState) as React + +type UseStatefulComponent state = (UseEffect (UseState state Unit)) + +-- | Supports a common subset of the v2 API to ease the upgrade process +component + :: forall props state + . { displayName :: String + , initialState :: {| state } + , receiveProps :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> Effect Unit + , render :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> JSX + } + -> Component {| props } (UseStatefulComponent {| state }) +component { displayName, initialState, receiveProps, render } = unsafePerformEffect do + React.component displayName \props -> React.do + React.Tuple state setState <- React.useState initialState + React.useEffect [React.toKey props, React.toKey state] do + receiveProps { props, state, setState } + pure (pure unit) + React.pure (render { props, state, setState }) + +-- | Supports a common subset of the v2 API to ease the upgrade process +stateless + :: forall props + . { displayName :: String + , render :: {| props } -> JSX + } + -> Component {| props } Unit +stateless { displayName, render } = unsafePerformEffect do + React.component displayName \props -> React.do + React.pure (render props) diff --git a/src/React/Basic/DOM.js b/src/React/Basic/DOM.js index eee88bc..f4b8acd 100644 --- a/src/React/Basic/DOM.js +++ b/src/React/Basic/DOM.js @@ -14,14 +14,10 @@ exports.unmountComponentAtNode_ = function(node) { return ReactDOM.unmountComponentAtNode(node); }; -exports.findDOMNode_ = function(instance) { - return ReactDOM.findDOMNode(instance); -}; - exports.createPortal_ = function(jsx, node) { return ReactDOM.createPortal(jsx, node); }; exports.mergeStyles = function(styles) { - return Object.assign.apply(null, [ {} ].concat(styles)); + return Object.assign.apply(null, [{}].concat(styles)); }; diff --git a/src/React/Basic/DOM.purs b/src/React/Basic/DOM.purs index 85add7e..32a0d7d 100644 --- a/src/React/Basic/DOM.purs +++ b/src/React/Basic/DOM.purs @@ -12,7 +12,6 @@ module React.Basic.DOM , hydrate , hydrate' , unmount - , findDOMNode , createPortal , text , css @@ -22,18 +21,14 @@ module React.Basic.DOM import Prelude -import Data.Either (Either) import Data.Function.Uncurried (Fn2, runFn2) -import Data.Maybe (Maybe(..)) -import Data.Nullable (Nullable, toMaybe) import Effect (Effect) -import Effect.Exception (Error, throw, try) import Effect.Uncurried (EffectFn1, EffectFn3, runEffectFn1, runEffectFn3) -import React.Basic (ReactComponentInstance, JSX) +import React.Basic (JSX) import React.Basic.DOM.Generated (Props_a, Props_abbr, Props_address, Props_area, Props_article, Props_aside, Props_audio, Props_b, Props_base, Props_bdi, Props_bdo, Props_blockquote, Props_body, Props_br, Props_button, Props_canvas, Props_caption, Props_cite, Props_code, Props_col, Props_colgroup, Props_data, Props_datalist, Props_dd, Props_del, Props_details, Props_dfn, Props_dialog, Props_div, Props_dl, Props_dt, Props_em, Props_embed, Props_fieldset, Props_figcaption, Props_figure, Props_footer, Props_form, Props_h1, Props_h2, Props_h3, Props_h4, Props_h5, Props_h6, Props_head, Props_header, Props_hgroup, Props_hr, Props_html, Props_i, Props_iframe, Props_img, Props_input, Props_ins, Props_kbd, Props_keygen, Props_label, Props_legend, Props_li, Props_link, Props_main, Props_map, Props_mark, Props_math, Props_menu, Props_menuitem, Props_meta, Props_meter, Props_nav, Props_noscript, Props_object, Props_ol, Props_optgroup, Props_option, Props_output, Props_p, Props_param, Props_picture, Props_pre, Props_progress, Props_q, Props_rb, Props_rp, Props_rt, Props_rtc, Props_ruby, Props_s, Props_samp, Props_script, Props_section, Props_select, Props_slot, Props_small, Props_source, Props_span, Props_strong, Props_style, Props_sub, Props_summary, Props_sup, Props_svg, Props_table, Props_tbody, Props_td, Props_template, Props_textarea, Props_tfoot, Props_th, Props_thead, Props_time, Props_title, Props_tr, Props_track, Props_u, Props_ul, Props_var, Props_video, Props_wbr, a, a_, abbr, abbr_, address, address_, area, article, article_, aside, aside_, audio, audio_, b, b_, base, bdi, bdi_, bdo, bdo_, blockquote, blockquote_, body, body_, br, button, button_, canvas, canvas_, caption, caption_, cite, cite_, code, code_, col, colgroup, colgroup_, data', data_, datalist, datalist_, dd, dd_, del, del_, details, details_, dfn, dfn_, dialog, dialog_, div, div_, dl, dl_, dt, dt_, em, em_, embed, fieldset, fieldset_, figcaption, figcaption_, figure, figure_, footer, footer_, form, form_, h1, h1_, h2, h2_, h3, h3_, h4, h4_, h5, h5_, h6, h6_, head, head_, header, header_, hgroup, hgroup_, hr, html, html_, i, i_, iframe, iframe_, img, input, ins, ins_, kbd, kbd_, keygen, keygen_, label, label_, legend, legend_, li, li_, link, main, main_, map, map_, mark, mark_, math, math_, menu, menu_, menuitem, menuitem_, meta, meter, meter_, nav, nav_, noscript, noscript_, object, object_, ol, ol_, optgroup, optgroup_, option, option_, output, output_, p, p_, param, picture, picture_, pre, pre_, progress, progress_, q, q_, rb, rb_, rp, rp_, rt, rt_, rtc, rtc_, ruby, ruby_, s, s_, samp, samp_, script, script_, section, section_, select, select_, slot, slot_, small, small_, source, span, span_, strong, strong_, style, style_, sub, sub_, summary, summary_, sup, sup_, svg, svg_, table, table_, tbody, tbody_, td, td_, template, template_, textarea, textarea_, tfoot, tfoot_, th, th_, thead, thead_, time, time_, title, title_, tr, tr_, track, u, u_, ul, ul_, var, var_, video, video_, wbr) as Generated import React.Basic.DOM.Internal (CSS, SharedProps, unsafeCreateDOMComponent) as Internal import Unsafe.Coerce (unsafeCoerce) -import Web.DOM (Element, Node) +import Web.DOM (Element) -- | Render or update/re-render a component tree into -- | a DOM element. @@ -85,21 +80,6 @@ unmount = runEffectFn1 unmountComponentAtNode_ foreign import unmountComponentAtNode_ :: EffectFn1 Element Boolean --- | Returns the current DOM node associated with the given --- | instance, or an Error if no node was found or the given --- | instance was not mounted. --- | --- | Note: Relies on `ReactDOM.findDOMNode` -findDOMNode :: ReactComponentInstance -> Effect (Either Error Node) -findDOMNode instance_ = try do - node <- runEffectFn1 findDOMNode_ instance_ - case toMaybe node of - Nothing -> throw "Node not found." - Just n -> pure n - --- | Warning: Relies on `ReactDOM.findDOMNode` which may throw exceptions -foreign import findDOMNode_ :: EffectFn1 ReactComponentInstance (Nullable Node) - -- | Divert a render tree into a separate DOM node. The node's -- | content will be overwritten and managed by React, similar -- | to `render` and `hydrate`. diff --git a/src/React/Basic/DOM/Events.purs b/src/React/Basic/DOM/Events.purs index 55fa568..df19d27 100644 --- a/src/React/Basic/DOM/Events.purs +++ b/src/React/Basic/DOM/Events.purs @@ -1,7 +1,9 @@ -- | This module defines safe DOM event function and property accessors. module React.Basic.DOM.Events - ( bubbles + ( capture + , capture_ + , bubbles , cancelable , eventPhase , eventPhaseNone @@ -44,11 +46,18 @@ import Prelude import Data.Maybe (Maybe) import Data.Nullable (toMaybe) +import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic.Events (EventFn, SyntheticEvent, unsafeEventFn) +import React.Basic.Events (EventFn, EventHandler, SyntheticEvent, handler, unsafeEventFn) import Unsafe.Coerce (unsafeCoerce) import Web.Event.Internal.Types (Event, EventTarget) +capture :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler +capture eventFn = handler (preventDefault >>> stopPropagation >>> eventFn) + +capture_ :: Effect Unit -> EventHandler +capture_ = capture identity <<< const + -- | General event fields bubbles :: EventFn SyntheticEvent Boolean diff --git a/src/React/Basic/DOM/Internal.purs b/src/React/Basic/DOM/Internal.purs index 156dd44..8c3ff52 100644 --- a/src/React/Basic/DOM/Internal.purs +++ b/src/React/Basic/DOM/Internal.purs @@ -1,8 +1,12 @@ module React.Basic.DOM.Internal where -import React.Basic (ReactComponent) +import Prelude + +import Data.Nullable (Nullable) +import React.Basic (Component, Ref) import React.Basic.Events (EventHandler) import Unsafe.Coerce (unsafeCoerce) +import Web.DOM (Node) -- | An abstract type representing records of CSS attributes. foreign import data CSS :: Type @@ -12,6 +16,9 @@ type SharedProps specific = -- | `key` is not really a DOM attribute - React intercepts it ( key :: String + -- | `ref` is not really a DOM attribute - React intercepts it + , ref :: Ref (Nullable Node) + , about :: String , acceptCharset :: String , accessKey :: String @@ -93,5 +100,5 @@ type SharedProps specific = | specific ) -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit unsafeCreateDOMComponent = unsafeCoerce