diff --git a/.gitignore b/.gitignore
index 7ebb89b..f9fa4f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
/.psc*
/.purs*
/.psa*
+/.vscode/
diff --git a/README.md b/README.md
index be55df5..ca45c7c 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,16 @@
[](https://travis-ci.org/lumihq/purescript-react-basic)
-This package implements an opinionated set of bindings to the React library, optimizing for the most basic use cases.
+This package implements an opinionated set of bindings over [React](https://reactjs.org), optimizing for correctness and simplifying basic use cases.
## Features
- All React DOM elements and attributes are supported (soon, events are a work in progress).
-- An intuitive API for specifying props - no arrays of key value pairs, just records.
+- An intuitive API for specifying props - simple records, no arrays of key value pairs.
- Attributes are optional, but type-checked. It is a type error to specify `href` as an integer, for example.
+- An action/update pattern for local component state, inspired by [ReasonReact](https://reasonml.github.io/reason-react/).
+- React lifecycles are available, but not in your way when you don't need them.
+- Typeclasses, like `Eq props`, can be used in component definitions.
## Getting Started
@@ -18,49 +21,12 @@ You can install this package using Bower:
bower install git@github.com:lumihq/purescript-react-basic.git
```
-Here is an example component which renders a label read from props along with a counter:
+See [the documentation](https://pursuit.purescript.org/packages/purescript-react-basic/docs/React.Basic) for a detailed overview, or take a look at one of the examples:
-```purescript
-module Counter where
+- [A Counter](./examples/counter/src/Counter.purs)
+- [A controlled input](./examples/controlled-input/src/ControlledInput.purs)
+- [Components](./examples/component/src/ToggleButton.purs) in [components](./examples/component/src/Container.purs)
-import Prelude
+## Migrating to v4 from v2 or v3
-import React.Basic as React
-import React.Basic.DOM as R
-import React.Basic.Events as Events
-
--- The props for the component
-type Props =
- { label :: String
- }
-
--- Create a component by passing a record to the `react` function.
--- The `render` function takes the props and current state, as well as a
--- state update callback, and produces a document.
-component :: React.Component Props
-component = React.component { displayName: "Counter", initialState, receiveProps, render }
- where
- initialState =
- { counter: 0
- }
-
- receiveProps _ =
- pure unit
-
- render { props, state, setState } =
- R.button
- { onClick: Events.handler_ do
- setState \s -> s { counter = s.counter + 1 }
- , children: [ R.text (props.label <> ": " <> show state.counter) ]
- }
-```
-
-This component can be used directly from JavaScript. For example, if you are using `purs-loader`:
-
-```jsx
-import {example as Example} from 'React.Basic.Example.purs';
-
-const myComponent = () => (
-
-);
-```
+v4 includes a new (but deprecated) module, `React.Basic.Compat`. It matches most of the old API and types (except `setStateThen` and `isFirstMount`) to make upgrading easier and more gradual. You can find `^import\sReact\.Basic\b` and replace with `import React.Basic.Compat`, upgrade the package version, and proceed from there one component at a time (or only new components). See the documentation link above for more info on the v4 API.
diff --git a/bower.json b/bower.json
index 182b0f3..d0a28e5 100644
--- a/bower.json
+++ b/bower.json
@@ -1,29 +1,25 @@
{
"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"
},
"dependencies": {
+ "purescript-aff": "^5.0.2",
+ "purescript-console": "^4.1.0",
+ "purescript-effect": "^2.0.0",
+ "purescript-exceptions": "^4.0.0",
"purescript-functions": "^4.0.0",
- "purescript-unsafe-coerce": "^4.0.0",
- "purescript-nullable": "^4.0.0",
- "purescript-typelevel-prelude": "^3.0.0",
+ "purescript-nullable": "^4.1.0",
"purescript-record": "^1.0.0",
- "purescript-effect": "^2.0.0",
- "purescript-web-events": "^1.0.0",
+ "purescript-typelevel-prelude": "^3.0.0",
+ "purescript-unsafe-coerce": "^4.0.0",
"purescript-web-dom": "^1.0.0",
- "purescript-exceptions": "^4.0.0"
+ "purescript-web-events": "^1.0.0"
},
"devDependencies": {
- "purescript-web-html": "^1.0.0",
- "purescript-console": "^4.1.0"
+ "purescript-web-html": "^1.0.0"
}
}
diff --git a/codegen/index.js b/codegen/index.js
index 98a0e9d..88aab99 100644
--- a/codegen/index.js
+++ b/codegen/index.js
@@ -1,5 +1,5 @@
-const fs = require('fs');
-const { props, voids, types, reserved } = require('./consts');
+const fs = require("fs");
+const { props, voids, types, reserved } = require("./consts");
const genFile = "../src/React/Basic/DOM/Generated.purs";
const header = `-- | ----------------------------------------
@@ -15,14 +15,15 @@ import React.Basic.Events (EventHandler)
`;
-const printRecord = (elProps) => elProps.length ? `
- ( ${ elProps.map((p) =>
- `${p} :: ${types[p] || "String"}`).join("\n , ")
- }
- )` : "()"
+const printRecord = elProps =>
+ elProps.length
+ ? `
+ ( ${elProps.map(p => `${p} :: ${types[p] || "String"}`).join("\n , ")}
+ )`
+ : "()";
const domTypes = props.elements.html
- .map((e) => {
+ .map(e => {
const noChildren = voids.includes(e);
const symbol = reserved.includes(e) ? `${e}'` : e;
return `
@@ -36,13 +37,17 @@ const domTypes = props.elements.html
=> Record attrs
-> JSX
${symbol} = element (unsafeCreateDOMComponent "${e}")${
- noChildren ? "" : `
+ noChildren
+ ? ""
+ : `
${e}_ :: Array JSX -> JSX
${e}_ children = ${symbol} { children }`
}
`;
-}).map((x) => x.replace(/^\n\ {4}/, "").replace(/\n\ {4}/g, "\n")).join("\n");
+ })
+ .map(x => x.replace(/^\n\ {4}/, "").replace(/\n\ {4}/g, "\n"))
+ .join("\n");
console.log(`Writing "${genFile}" ...`);
fs.writeFileSync(genFile, header + domTypes);
diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs
index 212fa03..1e6e877 100644
--- a/examples/component/src/Container.purs
+++ b/examples/component/src/Container.purs
@@ -1,16 +1,19 @@
module Container where
-import React.Basic as React
+import Prelude
+
+import React.Basic (Component, JSX, createComponent, makeStateless)
import React.Basic.DOM as R
-import ToggleButton as ToggleButton
+import ToggleButton (toggleButton)
+
+component :: Component Unit
+component = createComponent "Container"
-component :: React.Component {}
-component = React.stateless { displayName: "Container", render }
- where
- render _ =
- R.div
- { children:
- [ React.element ToggleButton.component { label: "A" }
- , React.element ToggleButton.component { label: "B" }
- ]
- }
+toggleButtonContainer :: JSX
+toggleButtonContainer = unit # makeStateless component \_ ->
+ R.div
+ { children:
+ [ toggleButton { label: "A" }
+ , toggleButton { label: "B" }
+ ]
+ }
diff --git a/examples/component/src/Main.purs b/examples/component/src/Main.purs
index d6cf566..67193a8 100644
--- a/examples/component/src/Main.purs
+++ b/examples/component/src/Main.purs
@@ -2,11 +2,10 @@ module Main where
import Prelude
-import Container as Container
+import Container (toggleButtonContainer)
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)
@@ -19,5 +18,5 @@ main = do
case container of
Nothing -> throw "Container element not found."
Just c ->
- let app = element Container.component {}
+ let app = toggleButtonContainer
in render app c
diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs
index 9b546bc..49430ae 100644
--- a/examples/component/src/ToggleButton.purs
+++ b/examples/component/src/ToggleButton.purs
@@ -3,33 +3,40 @@ module ToggleButton where
import Prelude
import Effect.Console (log)
-import React.Basic as React
+import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make)
import React.Basic.DOM as R
-import React.Basic.Events as Events
+
+component :: Component Props
+component = createComponent "ToggleButton"
type Props =
{ label :: String
}
-component :: React.Component Props
-component = React.component { displayName: "ToggleButton", initialState, receiveProps, render }
- where
- initialState =
+data Action
+ = Toggle
+
+toggleButton :: Props -> JSX
+toggleButton = make component
+ { initialState:
{ on: false
}
- receiveProps _ =
- pure unit
+ , update: \self -> case _ of
+ Toggle ->
+ UpdateAndSideEffects
+ self.state { on = not self.state.on }
+ \nextSelf -> do
+ log $ "next state: " <> show nextSelf.state
- render { props, state, setStateThen } =
+ , render: \self ->
R.button
- { onClick: Events.handler_ do
- setStateThen (\s -> s { on = not s.on }) \nextState -> do
- log $ "nextState: " <> show nextState
+ { onClick: capture_ self Toggle
, children:
- [ R.text props.label
- , R.text if state.on
- then " On"
- else " Off"
+ [ R.text self.props.label
+ , R.text if self.state.on
+ then " On"
+ else " Off"
]
}
+ }
diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs
index 509c746..7824483 100644
--- a/examples/controlled-input/src/ControlledInput.purs
+++ b/examples/controlled-input/src/ControlledInput.purs
@@ -3,34 +3,43 @@ module ControlledInput where
import Prelude
import Data.Maybe (Maybe(..), fromMaybe, maybe)
+import React.Basic (Component, JSX, StateUpdate(..), capture, createComponent, make)
import React.Basic as React
import React.Basic.DOM as R
-import React.Basic.DOM.Events (preventDefault, targetValue, timeStamp)
-import React.Basic.Events as Events
+import React.Basic.DOM.Events (targetValue, timeStamp)
+import React.Basic.Events (merge)
-component :: React.Component {}
-component = React.component { displayName: "ControlledInput", initialState, receiveProps, render }
- where
- initialState =
+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
+ , timestamp: Nothing
}
- receiveProps _ =
- pure unit
+ , update: \self -> case _ of
+ ValueChanged value timestamp ->
+ Update self.state
+ { value = value
+ , timestamp = Just timestamp
+ }
- render { state, setState } =
+ , render: \self ->
React.fragment
[ R.input
{ onChange:
- Events.handler
- (preventDefault >>> Events.merge { targetValue, timeStamp })
- \{ timeStamp, targetValue } ->
- setState _ { value = fromMaybe "" targetValue
- , timeStamp = Just timeStamp
- }
- , value: state.value
+ capture self (merge { targetValue, timeStamp })
+ \{ timeStamp, targetValue } -> ValueChanged (fromMaybe "" targetValue) timeStamp
+ , value: self.state.value
}
- , R.p_ [ R.text ("Current value = " <> show state.value) ]
- , R.p_ [ R.text ("Changed at = " <> maybe "never" show state.timeStamp) ]
+ , R.p_ [ R.text ("Current value = " <> show self.state.value) ]
+ , R.p_ [ R.text ("Changed at = " <> maybe "never" show self.state.timestamp) ]
]
+ }
diff --git a/examples/controlled-input/src/Main.purs b/examples/controlled-input/src/Main.purs
index 69cc4b8..8651738 100644
--- a/examples/controlled-input/src/Main.purs
+++ b/examples/controlled-input/src/Main.purs
@@ -2,11 +2,10 @@ module Main where
import Prelude
-import ControlledInput as ControlledInput
+import ControlledInput (controlledInput)
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)
@@ -19,5 +18,5 @@ main = do
case container of
Nothing -> throw "Container element not found."
Just c ->
- let app = element ControlledInput.component {}
+ let app = controlledInput unit
in render app c
diff --git a/examples/counter/package.json b/examples/counter/package.json
index 624a6f3..583c22b 100644
--- a/examples/counter/package.json
+++ b/examples/counter/package.json
@@ -1,9 +1,9 @@
{
"dependencies": {
- "react": "^16.4.2",
- "react-dom": "^16.4.2"
+ "react": "16.6.0",
+ "react-dom": "16.6.0"
},
"devDependencies": {
- "browserify": "^16.2.2"
+ "browserify": "16.2.3"
}
}
diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs
index cc49f94..07c0ba3 100644
--- a/examples/counter/src/Counter.purs
+++ b/examples/counter/src/Counter.purs
@@ -2,31 +2,30 @@ module Counter where
import Prelude
-import React.Basic as React
+import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make)
import React.Basic.DOM as R
-import React.Basic.Events as Events
--- The props for the component
+component :: Component Props
+component = createComponent "Counter"
+
type Props =
{ label :: String
}
--- Create a component by passing a record to the `react` function.
--- The `render` function takes the props and current state, as well as a
--- state update callback, and produces a document.
-component :: React.Component Props
-component = React.component { displayName: "Counter", initialState, receiveProps, render }
+data Action
+ = Increment
+
+counter :: Props -> JSX
+counter = make component { initialState, update, render }
where
- initialState =
- { counter: 0
- }
+ initialState = { counter: 0 }
- receiveProps _ =
- pure unit
+ update self = case _ of
+ Increment ->
+ Update self.state { counter = self.state.counter + 1 }
- render { props, state, setState } =
+ render self =
R.button
- { onClick: Events.handler_ do
- setState \s -> s { counter = s.counter + 1 }
- , children: [ R.text (props.label <> ": " <> show state.counter) ]
+ { onClick: capture_ self Increment
+ , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ]
}
diff --git a/examples/counter/src/Main.purs b/examples/counter/src/Main.purs
index 901a9d1..fa07fd4 100644
--- a/examples/counter/src/Main.purs
+++ b/examples/counter/src/Main.purs
@@ -2,11 +2,10 @@ module Main where
import Prelude
-import Counter as Counter
+import Counter (counter)
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)
@@ -19,5 +18,5 @@ main = do
case container of
Nothing -> throw "Container element not found."
Just c ->
- let app = element Counter.component { label: "Increment" }
+ let app = counter { label: "Increment" }
in render app c
diff --git a/examples/legacy-v2/.gitignore b/examples/legacy-v2/.gitignore
new file mode 100644
index 0000000..645684d
--- /dev/null
+++ b/examples/legacy-v2/.gitignore
@@ -0,0 +1,4 @@
+output
+html/index.js
+package-lock.json
+node_modules
diff --git a/examples/legacy-v2/Makefile b/examples/legacy-v2/Makefile
new file mode 100644
index 0000000..ecacfbe
--- /dev/null
+++ b/examples/legacy-v2/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/legacy-v2/README.md b/examples/legacy-v2/README.md
new file mode 100644
index 0000000..f2418c0
--- /dev/null
+++ b/examples/legacy-v2/README.md
@@ -0,0 +1,12 @@
+# Counter Example
+
+## Building
+
+```
+npm install
+make all
+```
+
+This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle.
+
+Then open `html/index.html` in your browser.
diff --git a/examples/legacy-v2/html/index.html b/examples/legacy-v2/html/index.html
new file mode 100644
index 0000000..6b93b7c
--- /dev/null
+++ b/examples/legacy-v2/html/index.html
@@ -0,0 +1,10 @@
+
+
+
+ react-basic example
+
+
+
+
+
+
diff --git a/examples/legacy-v2/package.json b/examples/legacy-v2/package.json
new file mode 100644
index 0000000..624a6f3
--- /dev/null
+++ b/examples/legacy-v2/package.json
@@ -0,0 +1,9 @@
+{
+ "dependencies": {
+ "react": "^16.4.2",
+ "react-dom": "^16.4.2"
+ },
+ "devDependencies": {
+ "browserify": "^16.2.2"
+ }
+}
diff --git a/examples/legacy-v2/src/LegacyCounter.purs b/examples/legacy-v2/src/LegacyCounter.purs
new file mode 100644
index 0000000..74d8e26
--- /dev/null
+++ b/examples/legacy-v2/src/LegacyCounter.purs
@@ -0,0 +1,36 @@
+module LegacyCounter where
+
+import Prelude
+
+import React.Basic.Compat (Component, component, element, stateless)
+import React.Basic.DOM as R
+import React.Basic.Events as Events
+
+type Props =
+ { label :: String
+ }
+
+-- | checks `component`
+legacyCounter :: Component Props
+legacyCounter = component { displayName: "LegacyCounter", initialState, receiveProps, render }
+ where
+ initialState =
+ { counter: 0
+ }
+
+ receiveProps self =
+ pure unit
+
+ render self =
+ R.button
+ { onClick: Events.handler_ do
+ self.setState \s -> s { counter = s.counter + 1 }
+ , children: [ element buttonLabel { label: self.props.label, counter: self.state.counter } ]
+ }
+
+-- | checks `stateless`
+buttonLabel :: Component { label :: String, counter :: Int }
+buttonLabel = stateless { displayName: "ButtonLabel", render }
+ where
+ render props =
+ R.text (props.label <> ": " <> show props.counter)
diff --git a/examples/legacy-v2/src/Main.purs b/examples/legacy-v2/src/Main.purs
new file mode 100644
index 0000000..bdb51c9
--- /dev/null
+++ b/examples/legacy-v2/src/Main.purs
@@ -0,0 +1,23 @@
+module Main where
+
+import Prelude
+
+import Data.Maybe (Maybe(..))
+import Effect (Effect)
+import Effect.Exception (throw)
+import LegacyCounter (legacyCounter)
+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 ->
+ let app = element legacyCounter { label: "Increment" }
+ in render app c
diff --git a/generated-docs/React/Basic.md b/generated-docs/React/Basic.md
index abab069..4632e99 100644
--- a/generated-docs/React/Basic.md
+++ b/generated-docs/React/Basic.md
@@ -1,80 +1,342 @@
## Module React.Basic
+#### `ComponentSpec`
+
+``` purescript
+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)
+```
+
+`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
+and 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 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. `make` will validate whether
+your component's internal 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`__
+
+#### `createComponent`
+
+``` purescript
+createComponent :: forall props. String -> Component props
+```
+
+Creates a `Component` 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 becomes possible for incompatible prop values to
+ be passed by React into lifecycle functions.__
+
+__*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`, `make`, `makeStateless`__
+
#### `Component`
``` purescript
-data Component :: Type -> Type
+data Component props
```
-A React component which can be used from JavaScript.
+A simplified alias for `ComponentSpec`. This type is usually used to represent
+the default component type returned from `createComponent`.
+Opaque component information for internal use.
+
+__*Note:* Never define a component with
+ a less specific type for `props` than its associated `ComponentSpec` and `make`
+ calls, as this can lead to unsafe interactions with React's lifecycle management.__
-#### `ComponentInstance`
+__*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`
``` purescript
-data ComponentInstance :: Type
+data StateUpdate props state action
+ = NoUpdate
+ | Update state
+ | SideEffects (Self props state action -> Effect Unit)
+ | UpdateAndSideEffects state (Self props state action -> Effect Unit)
```
-Represents the mounted component instance, or "this" in vanilla React.
+Used by the `update` function to describe the kind of state update and/or side
+effects desired.
-#### `JSX`
+__*See also:* `ComponentSpec`, `capture`__
+
+#### `Self`
``` purescript
-data JSX :: Type
+type Self props state action = { props :: props, state :: state, instance_ :: ReactComponentInstance }
```
-A virtual DOM element.
+`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`
-##### Instances
``` purescript
-Semigroup JSX
-Monoid JSX
+send :: forall props state action. Self props state action -> action -> Effect Unit
+```
+
+Dispatch an `action` into the component to be handled by `update`.
+
+__*See also:* `update`, `capture`__
+
+#### `sendAsync`
+
+``` purescript
+sendAsync :: forall props state action. Self props state action -> Aff action -> Effect Unit
```
-#### `component`
+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`
``` purescript
-component :: forall props state. { displayName :: String, initialState :: { | state }, receiveProps :: { isFirstMount :: Boolean, props :: { | props }, state :: { | state }, setState :: SetState state, setStateThen :: SetStateThen state, instance_ :: ComponentInstance } -> Effect Unit, render :: { props :: { | props }, state :: { | state }, setState :: SetState state, setStateThen :: SetStateThen state, instance_ :: ComponentInstance } -> JSX } -> Component { | props }
+capture :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler
```
-Create a React component from a _specification_ of that component.
+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`__
-A _specification_ consists of a state type, an initial value for that state,
-a function to apply incoming props to the internal state, and a rendering
-function which takes props, state and a state update function.
+__*See also:* `update`, `capture_`, `monitor`, `React.Basic.Events`__
-The rendering function should return a value of type `JSX`, which can be
-constructed using the helper functions provided by the `React.Basic.DOM`
-module.
+#### `capture_`
-Note: This function relies on `React.PureComponent` internally
+``` purescript
+capture_ :: forall props state action. Self props state action -> action -> EventHandler
+```
+
+Like `capture`, but for actions which don't need to extract information from the Event.
-#### `stateless`
+__*See also:* `update`, `capture`, `monitor_`__
+
+#### `monitor`
``` purescript
-stateless :: forall props. { displayName :: String, render :: { | props } -> JSX } -> Component { | props }
+monitor :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler
```
-Create a stateless React component.
+Like `capture`, but does not cancel the event.
-Removes a little bit of the `react` function's boilerplate when creating
-components which don't use state.
+__*See also:* `update`, `capture`, `monitor\_`__
-#### `element`
+#### `monitor_`
``` purescript
-element :: forall props. Component { | props } -> { | props } -> JSX
+monitor_ :: forall props state action. Self props state action -> action -> EventHandler
```
-Create a `JSX` node from a React component, by providing the props.
+Like `capture_`, but does not cancel the event.
-#### `elementKeyed`
+__*See also:* `update`, `monitor`, `capture_`, `React.Basic.Events`__
+
+#### `readProps`
+
+``` purescript
+readProps :: forall props state action. Self props state action -> Effect props
+```
+
+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`
+
+``` purescript
+readState :: forall props state action. Self props state action -> Effect state
+```
+
+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`
+
+``` purescript
+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
+```
+
+Turn a `Component` and `ComponentSpec` 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`__
+
+#### `makeStateless`
``` purescript
-elementKeyed :: forall props. Component { | props } -> { key :: String | props } -> JSX
+makeStateless :: forall props. Component props -> (props -> JSX) -> props -> JSX
+```
+
+Makes stateless component definition slightly less verbose:
+
+```purs
+component :: Component Props
+component = createComponent "Xyz"
+
+myComponent :: Props -> JSX
+myComponent = makeStateless component \props -> JSX
```
-Like `element`, plus a `key` for rendering components in a dynamic list.
-For more information see: https://reactjs.org/docs/reconciliation.html#keys
+__*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`__
+
+#### `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
+```
#### `empty`
@@ -82,10 +344,24 @@ For more information see: https://reactjs.org/docs/reconciliation.html#keys
empty :: JSX
```
-An empty node. This is often useful when you would like to conditionally
+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`__
+
+#### `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
@@ -94,15 +370,95 @@ fragment :: Array JSX -> JSX
Render an Array of children without a wrapping component.
-#### `fragmentKeyed`
+__*See also:* `JSX`__
+
+#### `element`
``` purescript
-fragmentKeyed :: String -> Array JSX -> JSX
+element :: forall props. ReactComponent { | props } -> { | props } -> JSX
```
-Render an Array of children without a wrapping component.
+Create a `JSX` node from a `ReactComponent`, by providing the props.
+
+This function is for non-React-Basic React components, such as those
+imported from FFI.
+
+__*See also:* `ReactComponent`, `elementKeyed`__
+
+#### `elementKeyed`
+
+``` purescript
+elementKeyed :: forall props. ReactComponent { | props } -> { 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.
+
+__*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__
+
+#### `displayNameFromComponent`
+
+``` purescript
+displayNameFromComponent :: forall props. Component props -> String
+```
+
+Retrieve the Display Name from a `ComponentSpec`. Useful for debugging and improving
+error messages in logs.
+
+__*See also:* `displayNameFromSelf`, `createComponent`__
+
+#### `displayNameFromSelf`
+
+``` purescript
+displayNameFromSelf :: forall props state action. Self props state action -> String
+```
+
+Retrieve the Display Name from a `Self`. Useful for debugging and improving
+error messages in logs.
+
+__*See also:* `displayNameFromComponent`, `createComponent`__
+
+#### `ReactComponent`
+
+``` purescript
+data ReactComponent props
+```
+
+Represents a traditional React component. Useful for JavaScript interop and
+FFI. For example:
+
+```purs
+foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String }
+```
+
+__*See also:* `element`, `toReactComponent`__
+
+#### `ReactComponentInstance`
+
+``` purescript
+data ReactComponentInstance
+```
+
+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.
+
+#### `toReactComponent`
+
+``` purescript
+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 }
+```
+
+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`.__
-Provide a key when dynamically rendering multiple fragments along side
-each other.
+__*See also:* `ReactComponent`__
diff --git a/generated-docs/React/Basic/Compat.md b/generated-docs/React/Basic/Compat.md
new file mode 100644
index 0000000..0137610
--- /dev/null
+++ b/generated-docs/React/Basic/Compat.md
@@ -0,0 +1,112 @@
+## Module React.Basic.Compat
+
+#### `Component`
+
+``` purescript
+type Component = ReactComponent
+```
+
+#### `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 } -> ReactComponent { | props }
+```
+
+Supports a common subset of the v2 API to ease the upgrade process
+
+#### `stateless`
+
+``` purescript
+stateless :: forall props. { displayName :: String, render :: { | props } -> JSX } -> ReactComponent { | props }
+```
+
+Supports a common subset of the v2 API to ease the upgrade process
+
+
+### Re-exported from React.Basic:
+
+#### `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
+```
+
+#### `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 props. ReactComponent { | props } -> { 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.
+
+__*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__
+
+#### `element`
+
+``` purescript
+element :: forall props. ReactComponent { | props } -> { | props } -> JSX
+```
+
+Create a `JSX` node from a `ReactComponent`, by providing the props.
+
+This function is for non-React-Basic React components, such as those
+imported from FFI.
+
+__*See also:* `ReactComponent`, `elementKeyed`__
+
diff --git a/generated-docs/React/Basic/DOM.md b/generated-docs/React/Basic/DOM.md
index b36435f..11123e9 100644
--- a/generated-docs/React/Basic/DOM.md
+++ b/generated-docs/React/Basic/DOM.md
@@ -3,9 +3,9 @@
This module defines helper functions for creating virtual DOM elements
safely.
-Note: DOM element props are provided as records, and checked using `Union`
-constraints. This means that we don't need to provide all props, but any we
-do provide must have the correct types.
+__*Note:* DOM element props are provided as records, and checked using `Union`
+ constraints. This means that we don't need to provide all props, but any we
+ do provide must have the correct types.__
#### `render`
@@ -16,7 +16,7 @@ render :: JSX -> Element -> Effect Unit
Render or update/re-render a component tree into
a DOM element.
-Note: Relies on `ReactDOM.render`
+__*Note:* Relies on `ReactDOM.render`__
#### `render'`
@@ -28,7 +28,7 @@ Render or update/re-render a component tree into
a DOM element. The given Effect is run once the
DOM update is complete.
-Note: Relies on `ReactDOM.render`
+__*Note:* Relies on `ReactDOM.render`__
#### `hydrate`
@@ -40,9 +40,9 @@ Render or update/re-render a component tree into
a DOM element, attempting to reuse the existing
DOM tree.
-Note: Relies on `ReactDOM.hydrate`, generally only
+__*Note:* Relies on `ReactDOM.hydrate`, generally only
used with `ReactDOMServer.renderToNodeStream` or
- `ReactDOMServer.renderToString`
+ `ReactDOMServer.renderToString`__
#### `hydrate'`
@@ -55,9 +55,9 @@ a DOM element, attempting to reuse the existing
DOM tree. The given Effect is run once the
DOM update is complete.
-Note: Relies on `ReactDOM.hydrate`, generally only
+__*Note:* Relies on `ReactDOM.hydrate`, generally only
used with `ReactDOMServer.renderToNodeStream` or
- `ReactDOMServer.renderToString`
+ `ReactDOMServer.renderToString`__
#### `unmount`
@@ -69,19 +69,22 @@ Attempt to unmount and clean up the React app
rendered into the given element. Returns `true`
if an app existed and was unmounted successfully.
-Note: Relies on `ReactDOM.unmountComponentAtNode`
+__*Note:* Relies on `ReactDOM.unmountComponentAtNode`__
#### `findDOMNode`
``` purescript
-findDOMNode :: ComponentInstance -> Effect (Either Error Node)
+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`
+__*Note:* This function can be *very* slow -- prefer
+`React.Basic.DOM.Components.Ref` where possible__
+
+__*Note:* Relies on `ReactDOM.findDOMNode`__
#### `createPortal`
@@ -110,7 +113,7 @@ css :: forall css. { | css } -> CSS
Create a value of type `CSS` (which can be provided to the `style` property)
from a plain record of CSS attributes.
-E.g.
+For example:
```
div { style: css { padding: "5px" } } [ text "This text is padded." ]
@@ -124,7 +127,7 @@ mergeStyles :: Array CSS -> CSS
Merge styles from right to left. Uses `Object.assign`.
-E.g.
+For example:
```
style: mergeCSS [ (css { padding: "5px" }), props.style ]
@@ -2194,6 +2197,6 @@ An abstract type representing records of CSS attributes.
#### `unsafeCreateDOMComponent`
``` purescript
-unsafeCreateDOMComponent :: forall props. String -> Component props
+unsafeCreateDOMComponent :: forall props. String -> ReactComponent props
```
diff --git a/generated-docs/React/Basic/DOM/Components/GlobalEvents.md b/generated-docs/React/Basic/DOM/Components/GlobalEvents.md
new file mode 100644
index 0000000..bb945fa
--- /dev/null
+++ b/generated-docs/React/Basic/DOM/Components/GlobalEvents.md
@@ -0,0 +1,64 @@
+## Module React.Basic.DOM.Components.GlobalEvents
+
+These helper components register and unregister event callbacks
+using React's the lifecycle callbacks. They're useful for
+declaratively defining global behavior which is associated with
+a particular component being mounted without having to wire
+all that lifecycle logic up manually.
+
+For example:
+
+```purs
+render self =
+ R.div
+ { className: "dropdown-wrapper"
+ , children:
+ [ dropdownButton
+ , guard showDropdown $
+ windowEvent
+ { eventType: EventType "click"
+ , options: defaultOptions
+ , handler: \_ -> send self CloseDropdown
+ }
+ dropdownMenu
+ ]
+ }
+```
+
+#### `EventHandlerOptions`
+
+``` purescript
+type EventHandlerOptions = { capture :: Boolean, once :: Boolean, passive :: Boolean }
+```
+
+#### `defaultOptions`
+
+``` purescript
+defaultOptions :: EventHandlerOptions
+```
+
+#### `globalEvent`
+
+``` purescript
+globalEvent :: EventTarget -> { eventType :: EventType, options :: EventHandlerOptions, handler :: Event -> Effect Unit } -> JSX -> JSX
+```
+
+#### `globalEvents`
+
+``` purescript
+globalEvents :: EventTarget -> Array { eventType :: EventType, options :: EventHandlerOptions, handler :: Event -> Effect Unit } -> JSX -> JSX
+```
+
+#### `windowEvent`
+
+``` purescript
+windowEvent :: { eventType :: EventType, options :: EventHandlerOptions, handler :: Event -> Effect Unit } -> JSX -> JSX
+```
+
+#### `windowEvents`
+
+``` purescript
+windowEvents :: Array { eventType :: EventType, options :: EventHandlerOptions, handler :: Event -> Effect Unit } -> JSX -> JSX
+```
+
+
diff --git a/generated-docs/React/Basic/DOM/Components/LogLifecycles.md b/generated-docs/React/Basic/DOM/Components/LogLifecycles.md
new file mode 100644
index 0000000..be1c32c
--- /dev/null
+++ b/generated-docs/React/Basic/DOM/Components/LogLifecycles.md
@@ -0,0 +1,9 @@
+## Module React.Basic.DOM.Components.LogLifecycles
+
+#### `logLifecycles`
+
+``` purescript
+logLifecycles :: Warn (Text "LogLifecycle is for debugging purposes only. Don't forget to remove it!") => JSX -> JSX
+```
+
+
diff --git a/generated-docs/React/Basic/DOM/Components/Ref.md b/generated-docs/React/Basic/DOM/Components/Ref.md
new file mode 100644
index 0000000..75b4bef
--- /dev/null
+++ b/generated-docs/React/Basic/DOM/Components/Ref.md
@@ -0,0 +1,45 @@
+## Module React.Basic.DOM.Components.Ref
+
+This module provides an efficient (no `ReactDOM.findDOMNode`) and
+declarative way to aquire a `Node` for an element in your render
+tree.
+
+For example:
+
+```purs
+render self =
+ ref \myRef ->
+ case myRef of
+ Nothing -> R.text "First DOM render in progress..."
+ Just _ -> R.text "First DOM render complete."
+```
+
+#### `ref`
+
+``` purescript
+ref :: (Maybe Node -> JSX) -> JSX
+```
+
+#### `selectorRef`
+
+``` purescript
+selectorRef :: QuerySelector -> (Maybe Node -> JSX) -> JSX
+```
+
+
+### Re-exported from Web.DOM.ParentNode:
+
+#### `QuerySelector`
+
+``` purescript
+newtype QuerySelector
+ = QuerySelector String
+```
+
+##### Instances
+``` purescript
+Eq QuerySelector
+Ord QuerySelector
+Newtype QuerySelector _
+```
+
diff --git a/generated-docs/React/Basic/DOM/Internal.md b/generated-docs/React/Basic/DOM/Internal.md
index b07c72d..8594101 100644
--- a/generated-docs/React/Basic/DOM/Internal.md
+++ b/generated-docs/React/Basic/DOM/Internal.md
@@ -19,7 +19,7 @@ Standard props which are shared by all DOM elements.
#### `unsafeCreateDOMComponent`
``` purescript
-unsafeCreateDOMComponent :: forall props. String -> Component props
+unsafeCreateDOMComponent :: forall props. String -> ReactComponent props
```
diff --git a/package-lock.json b/package-lock.json
index b969c5f..a609272 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1498,14 +1498,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1520,20 +1518,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -1650,8 +1645,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -1663,7 +1657,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -1678,7 +1671,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -1686,14 +1678,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -1712,7 +1702,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -1793,8 +1782,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -1806,7 +1794,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -1928,7 +1915,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -2794,9 +2780,9 @@
"dev": true
},
"nan": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
- "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz",
+ "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==",
"dev": true,
"optional": true
},
diff --git a/src/React/Basic.js b/src/React/Basic.js
index a2181c1..2ffd725 100644
--- a/src/React/Basic.js
+++ b/src/React/Basic.js
@@ -3,64 +3,161 @@
var React = require("react");
var Fragment = React.Fragment || "div";
-function setStateThen(instance) {
- return function(update, then) {
- return instance.setState(update, function() {
- then(this.state);
- });
- };
-}
+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
+ };
+ return self;
+ }
-exports.component_ = function(spec) {
- var Component = function constructor() {
- this.state = spec.initialState;
- this._setState = this.setState.bind(this);
- return this;
- };
+ function shouldComponentUpdate(nextProps, nextState) {
+ var shouldUpdate = this.$$spec.shouldUpdate;
+ return shouldUpdate === undefined
+ ? true
+ : shouldUpdate(this.toSelf())(nextProps.$$props)(
+ nextState === null ? null : nextState.$$state
+ );
+ }
- Component.prototype = Object.create(React.PureComponent.prototype);
+ function componentDidMount() {
+ var didMount = this.$$spec.didMount;
+ if (didMount !== undefined) {
+ didMount(this.toSelf())();
+ }
+ }
- Component.displayName = spec.displayName;
+ function componentDidUpdate() {
+ var didUpdate = this.$$spec.didUpdate;
+ if (didUpdate !== undefined) {
+ didUpdate(this.toSelf())();
+ }
+ }
- Component.prototype.componentDidMount = function componentDidMount() {
- spec.receiveProps({
- isFirstMount: true,
- props: this.props,
- state: this.state,
- setState: this._setState,
- setStateThen: setStateThen(this),
- instance_: this
- });
+ 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;
};
+})();
- Component.prototype.componentDidUpdate = function componentDidUpdate() {
- spec.receiveProps({
- isFirstMount: false,
- props: this.props,
- state: this.state,
- setState: this._setState,
- setStateThen: setStateThen(this),
- instance_: this
- });
+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())();
+ }
+ }
+ );
};
+};
- Component.prototype.render = function render() {
- return spec.render({
- props: this.props,
- state: this.state,
- setState: this._setState,
- setStateThen: setStateThen(this),
- instance_: this
- });
+exports.readProps = function(self) {
+ return self.instance_.props.$$props;
+};
+
+exports.readState = function(self) {
+ var state = self.instance_.state;
+ return state === null ? null : state.$$state;
+};
+
+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.empty = null;
- return Component;
+exports.keyed_ = function(key, child) {
+ return React.createElement(Fragment, { key: key }, child);
};
-exports.element_ = function(el, attrs) {
+exports.element_ = function(component, props) {
return React.createElement.apply(
null,
- [el, attrs].concat((attrs && attrs.children) || [])
+ [component, props].concat((props && props.children) || null)
);
};
@@ -70,9 +167,75 @@ exports.fragment = function(children) {
return React.createElement.apply(null, [Fragment, {}].concat(children));
};
-exports.fragmentKeyed_ = function(key, children) {
- return React.createElement.apply(
- null,
- [Fragment, { key: key }].concat(children)
+exports.displayNameFromComponent = function($$type) {
+ return $$type.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.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);
};
diff --git a/src/React/Basic.purs b/src/React/Basic.purs
index 2438ccd..8252eb8 100644
--- a/src/React/Basic.purs
+++ b/src/React/Basic.purs
@@ -1,24 +1,337 @@
module React.Basic
- ( Component
- , ComponentInstance
+ ( ComponentSpec
+ , createComponent
+ , Component
+ , StateUpdate(..)
+ , Self
+ , send
+ , sendAsync
+ , capture
+ , capture_
+ , monitor
+ , monitor_
+ , readProps
+ , readState
+ , make
+ , makeStateless
, JSX
- , component
- , stateless
- , element
- , elementKeyed
, empty
+ , keyed
, fragment
- , fragmentKeyed
+ , element
+ , elementKeyed
+ , displayNameFromComponent
+ , displayNameFromSelf
+ , ReactComponent
+ , ReactComponentInstance
+ , toReactComponent
) where
import Prelude
-import Data.Function.Uncurried (Fn1, Fn2, mkFn1, runFn2)
+import Data.Either (Either(..))
+import Data.Function.Uncurried (Fn1, Fn2, runFn1, runFn2)
+import Data.Nullable (Nullable, notNull, null)
import Effect (Effect)
-import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2)
-import Unsafe.Coerce (unsafeCoerce)
+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)
+
+-- | `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
+-- | and 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 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. `make` will validate whether
+-- | your component's internal 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 `Component` 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 becomes possible for incompatible prop values to
+-- | be passed by React into lifecycle functions.__
+-- |
+-- | __*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`, `make`, `makeStateless`__
+foreign import createComponent
+ :: forall props
+ . String
+ -> Component props
+
+-- | A simplified alias for `ComponentSpec`. This type is usually used to represent
+-- | the default component type returned from `createComponent`.
+-- type Component props = forall state action. ComponentSpec props state action
+
+-- | Opaque component information for internal use.
+-- |
+-- | __*Note:* Never define a component with
+-- | a less specific type for `props` than its associated `ComponentSpec` and `make`
+-- | calls, as this can lead to unsafe interactions with React's lifecycle management.__
+-- |
+-- | __*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
--- | A virtual DOM element.
+-- | Used by the `update` function to describe the kind of state update and/or side
+-- | effects desired.
+-- |
+-- | __*See also:* `ComponentSpec`, `capture`__
+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)
+
+-- | 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)
+
+-- | 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
+
+-- | 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)
+
+-- | 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
+
+-- | 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
+
+-- | 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
+
+-- | Turn a `Component` and `ComponentSpec` 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
+
+-- | 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 }
+
+-- | 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.__
foreign import data JSX :: Type
instance semigroupJSX :: Semigroup JSX where
@@ -27,155 +340,170 @@ instance semigroupJSX :: Semigroup JSX where
instance monoidJSX :: Monoid JSX where
mempty = empty
--- | A React component which can be used from JavaScript.
-foreign import data Component :: Type -> Type
-
--- | Create a React component from a _specification_ of that component.
--- |
--- | A _specification_ consists of a state type, an initial value for that state,
--- | a function to apply incoming props to the internal state, and a rendering
--- | function which takes props, state and a state update function.
--- |
--- | The rendering function should return a value of type `JSX`, which can be
--- | constructed using the helper functions provided by the `React.Basic.DOM`
--- | module.
--- |
--- | Note: This function relies on `React.PureComponent` internally
-component
- :: forall props state
- . { displayName :: String
- , initialState :: { | state }
- , receiveProps ::
- { isFirstMount :: Boolean
- , props :: { | props }
- , state :: { | state }
- , setState :: SetState state
- , setStateThen :: SetStateThen state
- , instance_ :: ComponentInstance
- }
- -> Effect Unit
- , render ::
- { props :: { | props }
- , state :: { | state }
- , setState :: SetState state
- , setStateThen :: SetStateThen state
- , instance_ :: ComponentInstance
- }
- -> JSX
- }
- -> Component { | props }
-component { displayName, initialState, receiveProps, render } =
- component_
- { displayName
- , initialState
- , receiveProps: mkEffectFn1 \this -> receiveProps
- { isFirstMount: this.isFirstMount
- , props: this.props
- , state: this.state
- , setState: runEffectFn1 this.setState
- , setStateThen: \update cb -> runEffectFn2 this.setStateThen update (mkEffectFn1 cb)
- , instance_: this.instance_
- }
- , render: mkFn1 \this -> render
- { props: this.props
- , state: this.state
- , setState: runEffectFn1 this.setState
- , setStateThen: \update cb -> runEffectFn2 this.setStateThen update (mkEffectFn1 cb)
- , instance_: this.instance_
- }
- }
-
--- | Create a stateless React component.
+-- | 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.
-- |
--- | Removes a little bit of the `react` function's boilerplate when creating
--- | components which don't use state.
-stateless
- :: forall props
- . { displayName :: String
- , render :: { | props } -> JSX
- }
- -> Component { | props }
-stateless { displayName, render } =
- component
- { displayName
- , initialState: {}
- , receiveProps: \_ -> pure unit
- , render: \this -> render this.props
- }
-
--- | SetState uses an update function to modify the current state.
-type SetState state = ({ | state } -> { | state }) -> Effect Unit
+-- | __*See also:* `JSX`, Monoid `guard`__
+foreign import empty :: JSX
--- | SetState uses an update function to modify the current state and a
--- | callback to invoke once the resulting rerender has been completely applied.
-type SetStateThen state = ({ | state } -> { | state }) -> ({ | state } -> Effect Unit) -> Effect Unit
+-- | 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__
+keyed :: String -> JSX -> JSX
+keyed = runFn2 keyed_
--- | Represents the mounted component instance, or "this" in vanilla React.
-foreign import data ComponentInstance :: Type
+-- | Render an Array of children without a wrapping component.
+-- |
+-- | __*See also:* `JSX`__
+foreign import fragment :: Array JSX -> JSX
--- | Create a `JSX` node from a React component, by providing the props.
+-- | Create a `JSX` node from a `ReactComponent`, by providing the props.
+-- |
+-- | This function is for non-React-Basic React components, such as those
+-- | imported from FFI.
+-- |
+-- | __*See also:* `ReactComponent`, `elementKeyed`__
element
:: forall props
- . Component { | props }
+ . ReactComponent { | props }
-> { | props }
-> JSX
element = runFn2 element_
--- | Like `element`, plus a `key` for rendering components in a dynamic list.
--- | For more information see: https://reactjs.org/docs/reconciliation.html#keys
+-- | 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.
+-- |
+-- | __*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__
elementKeyed
:: forall props
- . Component { | props }
+ . ReactComponent { | props }
-> { key :: String | props }
-> JSX
elementKeyed = runFn2 elementKeyed_
--- | An empty 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.
-empty :: JSX
-empty = unsafeCoerce false
+-- | 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
--- | Render an Array of children without a wrapping component.
-foreign import fragment :: Array JSX -> JSX
+-- | Retrieve the Display Name from a `Self`. Useful for debugging and improving
+-- | error messages in logs.
+-- |
+-- | __*See also:* `displayNameFromComponent`, `createComponent`__
+foreign import displayNameFromSelf
+ :: forall props state action
+ . Self props state action
+ -> String
--- | Render an Array of children without a wrapping component.
+-- | Represents a traditional React component. Useful for JavaScript interop and
+-- | FFI. For example:
+-- |
+-- | ```purs
+-- | foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String }
+-- | ```
-- |
--- | Provide a key when dynamically rendering multiple fragments along side
--- | each other.
-fragmentKeyed :: String -> Array JSX -> JSX
-fragmentKeyed = runFn2 fragmentKeyed_
-
--- | Private FFI
-
-foreign import component_
- :: forall props state
- . { displayName :: String
- , initialState :: { | state }
- , receiveProps ::
- EffectFn1
- { isFirstMount :: Boolean
- , props :: { | props }
- , state :: { | state }
- , setState :: EffectFn1 ({ | state } -> { | state }) Unit
- , setStateThen :: EffectFn2 ({ | state } -> { | state }) (EffectFn1 { | state } Unit) Unit
- , instance_ :: ComponentInstance
- }
- Unit
- , render ::
- Fn1
- { props :: { | props }
- , state :: { | state }
- , setState :: EffectFn1 ({ | state } -> { | state }) Unit
- , setStateThen :: EffectFn2 ({ | state } -> { | state }) (EffectFn1 { | state } Unit) Unit
- , instance_ :: ComponentInstance
- }
- JSX
+-- | __*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)
}
- -> Component { | props }
+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 elementKeyed_
+ :: forall props
+ . Fn2 (ReactComponent { | props }) { key :: String | props } JSX
-foreign import element_ :: forall props. Fn2 (Component { | props }) { | props } JSX
+foreign import warningDefaultUpdate
+ :: forall props state action
+ . EffectFn2
+ (Self props state action)
+ action
+ Unit
-foreign import elementKeyed_ :: forall props. Fn2 (Component { | props }) { key :: String | props } JSX
+foreign import warningUnmountedComponentAction
+ :: forall props state action
+ . EffectFn2
+ (Self props state action)
+ action
+ Unit
-foreign import fragmentKeyed_ :: Fn2 String (Array JSX) JSX
+foreign import warningFailedAsyncAction
+ :: forall props state action
+ . EffectFn2
+ (Self props state action)
+ Error
+ Unit
diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs
new file mode 100644
index 0000000..f450a45
--- /dev/null
+++ b/src/React/Basic/Compat.purs
@@ -0,0 +1,50 @@
+module React.Basic.Compat
+ ( Component
+ , component
+ , stateless
+ , module CompatibleTypes
+ ) where
+
+import Prelude
+
+import Effect (Effect)
+import React.Basic (ReactComponent, StateUpdate(..), createComponent, send, toReactComponent)
+import React.Basic (JSX, element, elementKeyed, empty, fragment, keyed) as CompatibleTypes
+
+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 } -> CompatibleTypes.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 } -> CompatibleTypes.JSX
+ }
+ -> ReactComponent { | props }
+stateless { displayName, render } =
+ toReactComponent identity (createComponent displayName)
+ { render: \self -> render self.props
+ }
diff --git a/src/React/Basic/DOM.purs b/src/React/Basic/DOM.purs
index 53fa16d..ca45370 100644
--- a/src/React/Basic/DOM.purs
+++ b/src/React/Basic/DOM.purs
@@ -1,9 +1,9 @@
-- | This module defines helper functions for creating virtual DOM elements
-- | safely.
-- |
--- | Note: DOM element props are provided as records, and checked using `Union`
--- | constraints. This means that we don't need to provide all props, but any we
--- | do provide must have the correct types.
+-- | __*Note:* DOM element props are provided as records, and checked using `Union`
+-- | constraints. This means that we don't need to provide all props, but any we
+-- | do provide must have the correct types.__
module React.Basic.DOM
( module Internal
@@ -29,7 +29,7 @@ import Data.Nullable (Nullable, toMaybe)
import Effect (Effect)
import Effect.Exception (Error, throw, try)
import Effect.Uncurried (EffectFn1, EffectFn3, runEffectFn1, runEffectFn3)
-import React.Basic (ComponentInstance, JSX)
+import React.Basic (ReactComponentInstance, 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)
@@ -38,7 +38,7 @@ import Web.DOM (Element, Node)
-- | Render or update/re-render a component tree into
-- | a DOM element.
-- |
--- | Note: Relies on `ReactDOM.render`
+-- | __*Note:* Relies on `ReactDOM.render`__
render :: JSX -> Element -> Effect Unit
render jsx node = render' jsx node (pure unit)
@@ -46,7 +46,7 @@ render jsx node = render' jsx node (pure unit)
-- | a DOM element. The given Effect is run once the
-- | DOM update is complete.
-- |
--- | Note: Relies on `ReactDOM.render`
+-- | __*Note:* Relies on `ReactDOM.render`__
render' :: JSX -> Element -> Effect Unit -> Effect Unit
render' = runEffectFn3 render_
@@ -56,9 +56,9 @@ foreign import render_ :: EffectFn3 JSX Element (Effect Unit) Unit
-- | a DOM element, attempting to reuse the existing
-- | DOM tree.
-- |
--- | Note: Relies on `ReactDOM.hydrate`, generally only
+-- | __*Note:* Relies on `ReactDOM.hydrate`, generally only
-- | used with `ReactDOMServer.renderToNodeStream` or
--- | `ReactDOMServer.renderToString`
+-- | `ReactDOMServer.renderToString`__
hydrate :: JSX -> Element -> Effect Unit
hydrate jsx node = hydrate' jsx node (pure unit)
@@ -67,9 +67,9 @@ hydrate jsx node = hydrate' jsx node (pure unit)
-- | DOM tree. The given Effect is run once the
-- | DOM update is complete.
-- |
--- | Note: Relies on `ReactDOM.hydrate`, generally only
+-- | __*Note:* Relies on `ReactDOM.hydrate`, generally only
-- | used with `ReactDOMServer.renderToNodeStream` or
--- | `ReactDOMServer.renderToString`
+-- | `ReactDOMServer.renderToString`__
hydrate' :: JSX -> Element -> Effect Unit -> Effect Unit
hydrate' = runEffectFn3 hydrate_
@@ -79,7 +79,7 @@ foreign import hydrate_ :: EffectFn3 JSX Element (Effect Unit) Unit
-- | rendered into the given element. Returns `true`
-- | if an app existed and was unmounted successfully.
-- |
--- | Note: Relies on `ReactDOM.unmountComponentAtNode`
+-- | __*Note:* Relies on `ReactDOM.unmountComponentAtNode`__
unmount :: Element -> Effect Boolean
unmount = runEffectFn1 unmountComponentAtNode_
@@ -89,8 +89,11 @@ foreign import unmountComponentAtNode_ :: EffectFn1 Element Boolean
-- | instance, or an Error if no node was found or the given
-- | instance was not mounted.
-- |
--- | Note: Relies on `ReactDOM.findDOMNode`
-findDOMNode :: ComponentInstance -> Effect (Either Error Node)
+-- | __*Note:* This function can be *very* slow -- prefer
+-- | `React.Basic.DOM.Components.Ref` where possible__
+-- |
+-- | __*Note:* Relies on `ReactDOM.findDOMNode`__
+findDOMNode :: ReactComponentInstance -> Effect (Either Error Node)
findDOMNode instance_ = try do
node <- runEffectFn1 findDOMNode_ instance_
case toMaybe node of
@@ -98,7 +101,7 @@ findDOMNode instance_ = try do
Just n -> pure n
-- | Warning: Relies on `ReactDOM.findDOMNode` which may throw exceptions
-foreign import findDOMNode_ :: EffectFn1 ComponentInstance (Nullable Node)
+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
@@ -115,7 +118,7 @@ text = unsafeCoerce
-- | Create a value of type `CSS` (which can be provided to the `style` property)
-- | from a plain record of CSS attributes.
-- |
--- | E.g.
+-- | For example:
-- |
-- | ```
-- | div { style: css { padding: "5px" } } [ text "This text is padded." ]
@@ -125,7 +128,7 @@ css = unsafeCoerce
-- | Merge styles from right to left. Uses `Object.assign`.
-- |
--- | E.g.
+-- | For example:
-- |
-- | ```
-- | style: mergeCSS [ (css { padding: "5px" }), props.style ]
diff --git a/src/React/Basic/DOM/Components/GlobalEvents.js b/src/React/Basic/DOM/Components/GlobalEvents.js
new file mode 100644
index 0000000..0e0d649
--- /dev/null
+++ b/src/React/Basic/DOM/Components/GlobalEvents.js
@@ -0,0 +1,90 @@
+"use strict";
+
+var React = require("react");
+
+exports._passiveSupported = null;
+
+function checkPassiveSupported() {
+ if (exports._passiveSupported == null) {
+ try {
+ window.addEventListener(
+ "test",
+ null,
+ Object.defineProperty({}, "passive", {
+ get: function() {
+ exports._passiveSupported = true;
+ }
+ })
+ );
+ } catch (err) {
+ exports._passiveSupported = false;
+ }
+ }
+ return exports._passiveSupported;
+}
+
+function createHandler(h) {
+ return function(e) {
+ return h(e)();
+ };
+}
+
+function up(target, eventType, handler, options) {
+ target.addEventListener(
+ eventType,
+ handler,
+ checkPassiveSupported() ? options : options.capture
+ );
+}
+
+function down(target, eventType, handler, options) {
+ target.removeEventListener(
+ eventType,
+ handler,
+ checkPassiveSupported() ? options : options.capture
+ );
+}
+
+var GlobalEvent = function() {
+ return this;
+};
+
+GlobalEvent.prototype = Object.create(React.Component.prototype);
+
+GlobalEvent.displayName = "GlobalEvent";
+
+GlobalEvent.prototype.componentDidMount = function() {
+ this._handler = createHandler(this.props.handler);
+ up(
+ this.props.target,
+ this.props.eventType,
+ this._handler,
+ this.props.options
+ );
+};
+
+GlobalEvent.prototype.componentDidUpdate = function(prevProps) {
+ down(prevProps.target, prevProps.eventType, this._handler, prevProps.options);
+ this._handler = createHandler(this.props.handler);
+ up(
+ this.props.target,
+ this.props.eventType,
+ this._handler,
+ this.props.options
+ );
+};
+
+GlobalEvent.prototype.componentWillUnmount = function() {
+ down(
+ this.props.target,
+ this.props.eventType,
+ this._handler,
+ this.props.options
+ );
+};
+
+GlobalEvent.prototype.render = function() {
+ return this.props.child;
+};
+
+exports.globalEvent_ = GlobalEvent;
diff --git a/src/React/Basic/DOM/Components/GlobalEvents.purs b/src/React/Basic/DOM/Components/GlobalEvents.purs
new file mode 100644
index 0000000..ea9081d
--- /dev/null
+++ b/src/React/Basic/DOM/Components/GlobalEvents.purs
@@ -0,0 +1,117 @@
+-- | These helper components register and unregister event callbacks
+-- | using React's the lifecycle callbacks. They're useful for
+-- | declaratively defining global behavior which is associated with
+-- | a particular component being mounted without having to wire
+-- | all that lifecycle logic up manually.
+-- |
+-- | For example:
+-- |
+-- | ```purs
+-- | render self =
+-- | R.div
+-- | { className: "dropdown-wrapper"
+-- | , children:
+-- | [ dropdownButton
+-- | , guard showDropdown $
+-- | windowEvent
+-- | { eventType: EventType "click"
+-- | , options: defaultOptions
+-- | , handler: \_ -> send self CloseDropdown
+-- | }
+-- | dropdownMenu
+-- | ]
+-- | }
+-- | ```
+module React.Basic.DOM.Components.GlobalEvents
+ ( EventHandlerOptions
+ , defaultOptions
+ , globalEvent
+ , globalEvents
+ , windowEvent
+ , windowEvents
+ ) where
+
+import Prelude
+
+import Data.Foldable (foldr)
+import Effect (Effect)
+import Effect.Unsafe (unsafePerformEffect)
+import React.Basic (JSX, ReactComponent, element)
+import Web.Event.Event (EventType)
+import Web.Event.Internal.Types (Event, EventTarget)
+import Web.HTML (window)
+import Web.HTML.Window as Window
+
+type EventHandlerOptions =
+ { capture :: Boolean
+ , once :: Boolean
+ , passive :: Boolean
+ }
+
+defaultOptions :: EventHandlerOptions
+defaultOptions =
+ { capture: false
+ , once: false
+ , passive: false
+ }
+
+foreign import globalEvent_
+ :: ReactComponent
+ { target :: EventTarget
+ , eventType :: EventType
+ , handler :: Event -> Effect Unit
+ , options :: EventHandlerOptions
+ , child :: JSX
+ }
+
+globalEvent
+ :: EventTarget
+ -> { eventType :: EventType
+ , options :: EventHandlerOptions
+ , handler :: Event -> Effect Unit
+ }
+ -> JSX
+ -> JSX
+globalEvent target { eventType, options, handler } child =
+ element globalEvent_
+ { target
+ , eventType
+ , handler
+ , options
+ , child
+ }
+
+globalEvents
+ :: EventTarget
+ -> Array
+ { eventType :: EventType
+ , options :: EventHandlerOptions
+ , handler :: Event -> Effect Unit
+ }
+ -> JSX
+ -> JSX
+globalEvents target = flip (foldr (globalEvent target))
+
+windowEvents
+ :: Array
+ { eventType :: EventType
+ , options :: EventHandlerOptions
+ , handler :: Event -> Effect Unit
+ }
+ -> JSX
+ -> JSX
+windowEvents = globalEvents $ unsafePerformEffect $ map Window.toEventTarget window
+
+windowEvent
+ :: { eventType :: EventType
+ , options :: EventHandlerOptions
+ , handler :: Event -> Effect Unit
+ }
+ -> JSX
+ -> JSX
+windowEvent = windowEvents <<< pure
+
+-- | Hide "unused ffi export" warning.
+-- | The export is required to prevent
+-- | PS' bundler from stripping it out.
+foreign import _passiveSupported :: Void
diff --git a/src/React/Basic/DOM/Components/LogLifecycles.js b/src/React/Basic/DOM/Components/LogLifecycles.js
new file mode 100644
index 0000000..714397d
--- /dev/null
+++ b/src/React/Basic/DOM/Components/LogLifecycles.js
@@ -0,0 +1,38 @@
+"use strict";
+
+var React = require("react");
+
+var LogLifecycles = function(_props) {
+ return this;
+};
+
+LogLifecycles.prototype = Object.create(React.Component.prototype);
+
+LogLifecycles.displayName = "LogLifecycles";
+
+LogLifecycles.prototype.componentDidMount = function() {
+ console.info(
+ this.props.child.type.displayName || this.props.child.type,
+ "[didMount]"
+ );
+};
+
+LogLifecycles.prototype.componentDidUpdate = function() {
+ console.info(
+ this.props.child.type.displayName || this.props.child.type,
+ "[didUpdate]"
+ );
+};
+
+LogLifecycles.prototype.componentWillUnmount = function() {
+ console.info(
+ this.props.child.type.displayName || this.props.child.type,
+ "[willUnmount]"
+ );
+};
+
+LogLifecycles.prototype.render = function() {
+ return this.props.child;
+};
+
+exports.logLifecycles_ = LogLifecycles;
diff --git a/src/React/Basic/DOM/Components/LogLifecycles.purs b/src/React/Basic/DOM/Components/LogLifecycles.purs
new file mode 100644
index 0000000..57a5fb7
--- /dev/null
+++ b/src/React/Basic/DOM/Components/LogLifecycles.purs
@@ -0,0 +1,11 @@
+module React.Basic.DOM.Components.LogLifecycles
+ ( logLifecycles
+ ) where
+
+import Prim.TypeError (class Warn, Text)
+import React.Basic (JSX, ReactComponent, element)
+
+foreign import logLifecycles_ :: ReactComponent { child :: JSX }
+
+logLifecycles :: Warn (Text "LogLifecycle is for debugging purposes only. Don't forget to remove it!") => JSX -> JSX
+logLifecycles child = element logLifecycles_ { child }
diff --git a/src/React/Basic/DOM/Components/Ref.js b/src/React/Basic/DOM/Components/Ref.js
new file mode 100644
index 0000000..5726c4c
--- /dev/null
+++ b/src/React/Basic/DOM/Components/Ref.js
@@ -0,0 +1,43 @@
+"use strict";
+
+var React = require("react");
+
+exports.mkRef = function(toMaybe) {
+ var Ref = function(_props) {
+ this.state = { node: null };
+ this.ref = React.createRef();
+ return this;
+ };
+
+ Ref.prototype = Object.create(React.Component.prototype);
+
+ Ref.displayName = "Ref";
+
+ Ref.prototype.syncRef = function() {
+ var selector = this.props.selector;
+ var current = this.ref.current;
+ var next =
+ selector === null ? current : current && current.querySelector(selector);
+ if (this.state.node !== next) {
+ this.setState({ node: next });
+ }
+ };
+
+ Ref.prototype.componentDidMount = function() {
+ this.syncRef();
+ };
+
+ Ref.prototype.componentDidUpdate = function() {
+ this.syncRef();
+ };
+
+ Ref.prototype.render = function() {
+ return React.createElement(
+ "react-basic-ref",
+ { ref: this.ref },
+ this.props.render(toMaybe(this.state.node))
+ );
+ };
+
+ return Ref;
+};
diff --git a/src/React/Basic/DOM/Components/Ref.purs b/src/React/Basic/DOM/Components/Ref.purs
new file mode 100644
index 0000000..c1b5a5f
--- /dev/null
+++ b/src/React/Basic/DOM/Components/Ref.purs
@@ -0,0 +1,39 @@
+-- | This module provides an efficient (no `ReactDOM.findDOMNode`) and
+-- | declarative way to aquire a `Node` for an element in your render
+-- | tree.
+-- |
+-- | For example:
+-- |
+-- | ```purs
+-- | render self =
+-- | ref \myRef ->
+-- | case myRef of
+-- | Nothing -> R.text "First DOM render in progress..."
+-- | Just _ -> R.text "First DOM render complete."
+-- | ```
+module React.Basic.DOM.Components.Ref
+ ( ref
+ , selectorRef
+ , module Web.DOM.ParentNode
+ ) where
+
+import Prelude
+
+import Data.Maybe (Maybe)
+import Data.Nullable (Nullable, notNull, null, toMaybe)
+import React.Basic (JSX, ReactComponent, element)
+import Web.DOM (Node)
+import Web.DOM.ParentNode (QuerySelector(..))
+
+foreign import mkRef :: (Nullable ~> Maybe) -> ReactComponent { render :: Maybe Node -> JSX, selector :: Nullable QuerySelector }
+
+ref_ :: ReactComponent { render :: Maybe Node -> JSX, selector :: Nullable QuerySelector }
+ref_ = mkRef toMaybe
+
+ref :: (Maybe Node -> JSX) -> JSX
+ref render = element ref_ { render, selector: null }
+
+selectorRef :: QuerySelector -> (Maybe Node -> JSX) -> JSX
+selectorRef qs render = element ref_ { render, selector: notNull qs }
+
+-- selectorAllRef ::
diff --git a/src/React/Basic/DOM/Internal.purs b/src/React/Basic/DOM/Internal.purs
index c3b6240..156dd44 100644
--- a/src/React/Basic/DOM/Internal.purs
+++ b/src/React/Basic/DOM/Internal.purs
@@ -1,6 +1,6 @@
module React.Basic.DOM.Internal where
-import React.Basic (Component)
+import React.Basic (ReactComponent)
import React.Basic.Events (EventHandler)
import Unsafe.Coerce (unsafeCoerce)
@@ -93,5 +93,5 @@ type SharedProps specific =
| specific
)
-unsafeCreateDOMComponent :: forall props. String -> Component props
+unsafeCreateDOMComponent :: forall props. String -> ReactComponent props
unsafeCreateDOMComponent = unsafeCoerce