Skip to content

Commit 90ccad9

Browse files
authored
v4 API (#58)
* api experiment * more reason-react-like api experiment * Add Compat module; fix stateless; build docs * Split the spec component type from ReactComponent; fix toReactComponent * Fix statelessComponent * Update types, distinguish initialState from state until make is called * Fix shouldUpdate ffi * Restore package-lock.json * Formatting * Provide self-bound event handling functions * Clean up capture/monitor * A few more performance-related tweaks * Move displayName functions to FFI * More performance tweaks * Still more perf :) * Remove Compat module (moved to legacy example) * Capture forall bug * Refactor Self, plus more FFI perf improvements * Reorganize for docs * Add docs * Make it more difficult to run into the incompatible props bug * Use Union instead of Record update * Fix displayNameFromSelf * Export Compat module * Remove unused state in counter example * Docs * selectorRef * Doc updates
1 parent 3290344 commit 90ccad9

38 files changed

+1972
-430
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
/.psc*
77
/.purs*
88
/.psa*
9+
/.vscode/

README.md

Lines changed: 11 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
[![Build Status](https://travis-ci.org/lumihq/purescript-react-basic.svg?branch=master)](https://travis-ci.org/lumihq/purescript-react-basic)
44

5-
This package implements an opinionated set of bindings to the React library, optimizing for the most basic use cases.
5+
This package implements an opinionated set of bindings over [React](https://reactjs.org), optimizing for correctness and simplifying basic use cases.
66

77
## Features
88

99
- All React DOM elements and attributes are supported (soon, events are a work in progress).
10-
- An intuitive API for specifying props - no arrays of key value pairs, just records.
10+
- An intuitive API for specifying props - simple records, no arrays of key value pairs.
1111
- Attributes are optional, but type-checked. It is a type error to specify `href` as an integer, for example.
12+
- An action/update pattern for local component state, inspired by [ReasonReact](https://reasonml.github.io/reason-react/).
13+
- React lifecycles are available, but not in your way when you don't need them.
14+
- Typeclasses, like `Eq props`, can be used in component definitions.
1215

1316
## Getting Started
1417

@@ -18,49 +21,12 @@ You can install this package using Bower:
1821
bower install git@github.com:lumihq/purescript-react-basic.git
1922
```
2023

21-
Here is an example component which renders a label read from props along with a counter:
24+
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:
2225

23-
```purescript
24-
module Counter where
26+
- [A Counter](./examples/counter/src/Counter.purs)
27+
- [A controlled input](./examples/controlled-input/src/ControlledInput.purs)
28+
- [Components](./examples/component/src/ToggleButton.purs) in [components](./examples/component/src/Container.purs)
2529

26-
import Prelude
30+
## Migrating to v4 from v2 or v3
2731

28-
import React.Basic as React
29-
import React.Basic.DOM as R
30-
import React.Basic.Events as Events
31-
32-
-- The props for the component
33-
type Props =
34-
{ label :: String
35-
}
36-
37-
-- Create a component by passing a record to the `react` function.
38-
-- The `render` function takes the props and current state, as well as a
39-
-- state update callback, and produces a document.
40-
component :: React.Component Props
41-
component = React.component { displayName: "Counter", initialState, receiveProps, render }
42-
where
43-
initialState =
44-
{ counter: 0
45-
}
46-
47-
receiveProps _ =
48-
pure unit
49-
50-
render { props, state, setState } =
51-
R.button
52-
{ onClick: Events.handler_ do
53-
setState \s -> s { counter = s.counter + 1 }
54-
, children: [ R.text (props.label <> ": " <> show state.counter) ]
55-
}
56-
```
57-
58-
This component can be used directly from JavaScript. For example, if you are using `purs-loader`:
59-
60-
```jsx
61-
import {example as Example} from 'React.Basic.Example.purs';
62-
63-
const myComponent = () => (
64-
<Example label='Increment' />
65-
);
66-
```
32+
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.

bower.json

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
{
22
"name": "purescript-react-basic",
33
"license": "Apache-2.0",
4-
"ignore": [
5-
"**/.*",
6-
"node_modules",
7-
"bower_components",
8-
"output"
9-
],
4+
"ignore": ["**/.*", "node_modules", "bower_components", "output"],
105
"repository": {
116
"type": "git",
127
"url": "git://github.com/lumihq/purescript-react-basic.git"
138
},
149
"dependencies": {
10+
"purescript-aff": "^5.0.2",
11+
"purescript-console": "^4.1.0",
12+
"purescript-effect": "^2.0.0",
13+
"purescript-exceptions": "^4.0.0",
1514
"purescript-functions": "^4.0.0",
16-
"purescript-unsafe-coerce": "^4.0.0",
17-
"purescript-nullable": "^4.0.0",
18-
"purescript-typelevel-prelude": "^3.0.0",
15+
"purescript-nullable": "^4.1.0",
1916
"purescript-record": "^1.0.0",
20-
"purescript-effect": "^2.0.0",
21-
"purescript-web-events": "^1.0.0",
17+
"purescript-typelevel-prelude": "^3.0.0",
18+
"purescript-unsafe-coerce": "^4.0.0",
2219
"purescript-web-dom": "^1.0.0",
23-
"purescript-exceptions": "^4.0.0"
20+
"purescript-web-events": "^1.0.0"
2421
},
2522
"devDependencies": {
26-
"purescript-web-html": "^1.0.0",
27-
"purescript-console": "^4.1.0"
23+
"purescript-web-html": "^1.0.0"
2824
}
2925
}

codegen/index.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const fs = require('fs');
2-
const { props, voids, types, reserved } = require('./consts');
1+
const fs = require("fs");
2+
const { props, voids, types, reserved } = require("./consts");
33
const genFile = "../src/React/Basic/DOM/Generated.purs";
44

55
const header = `-- | ----------------------------------------
@@ -15,14 +15,15 @@ import React.Basic.Events (EventHandler)
1515
1616
`;
1717

18-
const printRecord = (elProps) => elProps.length ? `
19-
( ${ elProps.map((p) =>
20-
`${p} :: ${types[p] || "String"}`).join("\n , ")
21-
}
22-
)` : "()"
18+
const printRecord = elProps =>
19+
elProps.length
20+
? `
21+
( ${elProps.map(p => `${p} :: ${types[p] || "String"}`).join("\n , ")}
22+
)`
23+
: "()";
2324

2425
const domTypes = props.elements.html
25-
.map((e) => {
26+
.map(e => {
2627
const noChildren = voids.includes(e);
2728
const symbol = reserved.includes(e) ? `${e}'` : e;
2829
return `
@@ -36,13 +37,17 @@ const domTypes = props.elements.html
3637
=> Record attrs
3738
-> JSX
3839
${symbol} = element (unsafeCreateDOMComponent "${e}")${
39-
noChildren ? "" : `
40+
noChildren
41+
? ""
42+
: `
4043
4144
${e}_ :: Array JSX -> JSX
4245
${e}_ children = ${symbol} { children }`
4346
}
4447
`;
45-
}).map((x) => x.replace(/^\n\ {4}/, "").replace(/\n\ {4}/g, "\n")).join("\n");
48+
})
49+
.map(x => x.replace(/^\n\ {4}/, "").replace(/\n\ {4}/g, "\n"))
50+
.join("\n");
4651

4752
console.log(`Writing "${genFile}" ...`);
4853
fs.writeFileSync(genFile, header + domTypes);

examples/component/src/Container.purs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
module Container where
22

3-
import React.Basic as React
3+
import Prelude
4+
5+
import React.Basic (Component, JSX, createComponent, makeStateless)
46
import React.Basic.DOM as R
5-
import ToggleButton as ToggleButton
7+
import ToggleButton (toggleButton)
8+
9+
component :: Component Unit
10+
component = createComponent "Container"
611

7-
component :: React.Component {}
8-
component = React.stateless { displayName: "Container", render }
9-
where
10-
render _ =
11-
R.div
12-
{ children:
13-
[ React.element ToggleButton.component { label: "A" }
14-
, React.element ToggleButton.component { label: "B" }
15-
]
16-
}
12+
toggleButtonContainer :: JSX
13+
toggleButtonContainer = unit # makeStateless component \_ ->
14+
R.div
15+
{ children:
16+
[ toggleButton { label: "A" }
17+
, toggleButton { label: "B" }
18+
]
19+
}

examples/component/src/Main.purs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ module Main where
22

33
import Prelude
44

5-
import Container as Container
5+
import Container (toggleButtonContainer)
66
import Data.Maybe (Maybe(..))
77
import Effect (Effect)
88
import Effect.Exception (throw)
9-
import React.Basic (element)
109
import React.Basic.DOM (render)
1110
import Web.DOM.NonElementParentNode (getElementById)
1211
import Web.HTML (window)
@@ -19,5 +18,5 @@ main = do
1918
case container of
2019
Nothing -> throw "Container element not found."
2120
Just c ->
22-
let app = element Container.component {}
21+
let app = toggleButtonContainer
2322
in render app c

examples/component/src/ToggleButton.purs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,40 @@ module ToggleButton where
33
import Prelude
44

55
import Effect.Console (log)
6-
import React.Basic as React
6+
import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make)
77
import React.Basic.DOM as R
8-
import React.Basic.Events as Events
8+
9+
component :: Component Props
10+
component = createComponent "ToggleButton"
911

1012
type Props =
1113
{ label :: String
1214
}
1315

14-
component :: React.Component Props
15-
component = React.component { displayName: "ToggleButton", initialState, receiveProps, render }
16-
where
17-
initialState =
16+
data Action
17+
= Toggle
18+
19+
toggleButton :: Props -> JSX
20+
toggleButton = make component
21+
{ initialState:
1822
{ on: false
1923
}
2024

21-
receiveProps _ =
22-
pure unit
25+
, update: \self -> case _ of
26+
Toggle ->
27+
UpdateAndSideEffects
28+
self.state { on = not self.state.on }
29+
\nextSelf -> do
30+
log $ "next state: " <> show nextSelf.state
2331

24-
render { props, state, setStateThen } =
32+
, render: \self ->
2533
R.button
26-
{ onClick: Events.handler_ do
27-
setStateThen (\s -> s { on = not s.on }) \nextState -> do
28-
log $ "nextState: " <> show nextState
34+
{ onClick: capture_ self Toggle
2935
, children:
30-
[ R.text props.label
31-
, R.text if state.on
32-
then " On"
33-
else " Off"
36+
[ R.text self.props.label
37+
, R.text if self.state.on
38+
then " On"
39+
else " Off"
3440
]
3541
}
42+
}

examples/controlled-input/src/ControlledInput.purs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,43 @@ module ControlledInput where
33
import Prelude
44

55
import Data.Maybe (Maybe(..), fromMaybe, maybe)
6+
import React.Basic (Component, JSX, StateUpdate(..), capture, createComponent, make)
67
import React.Basic as React
78
import React.Basic.DOM as R
8-
import React.Basic.DOM.Events (preventDefault, targetValue, timeStamp)
9-
import React.Basic.Events as Events
9+
import React.Basic.DOM.Events (targetValue, timeStamp)
10+
import React.Basic.Events (merge)
1011

11-
component :: React.Component {}
12-
component = React.component { displayName: "ControlledInput", initialState, receiveProps, render }
13-
where
14-
initialState =
12+
component :: Component Props
13+
component = createComponent "ControlledInput"
14+
15+
type Props = Unit
16+
17+
data Action
18+
= ValueChanged String Number
19+
20+
controlledInput :: Props -> JSX
21+
controlledInput = make component
22+
{ initialState:
1523
{ value: "hello world"
16-
, timeStamp: Nothing
24+
, timestamp: Nothing
1725
}
1826

19-
receiveProps _ =
20-
pure unit
27+
, update: \self -> case _ of
28+
ValueChanged value timestamp ->
29+
Update self.state
30+
{ value = value
31+
, timestamp = Just timestamp
32+
}
2133

22-
render { state, setState } =
34+
, render: \self ->
2335
React.fragment
2436
[ R.input
2537
{ onChange:
26-
Events.handler
27-
(preventDefault >>> Events.merge { targetValue, timeStamp })
28-
\{ timeStamp, targetValue } ->
29-
setState _ { value = fromMaybe "" targetValue
30-
, timeStamp = Just timeStamp
31-
}
32-
, value: state.value
38+
capture self (merge { targetValue, timeStamp })
39+
\{ timeStamp, targetValue } -> ValueChanged (fromMaybe "" targetValue) timeStamp
40+
, value: self.state.value
3341
}
34-
, R.p_ [ R.text ("Current value = " <> show state.value) ]
35-
, R.p_ [ R.text ("Changed at = " <> maybe "never" show state.timeStamp) ]
42+
, R.p_ [ R.text ("Current value = " <> show self.state.value) ]
43+
, R.p_ [ R.text ("Changed at = " <> maybe "never" show self.state.timestamp) ]
3644
]
45+
}

examples/controlled-input/src/Main.purs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ module Main where
22

33
import Prelude
44

5-
import ControlledInput as ControlledInput
5+
import ControlledInput (controlledInput)
66
import Data.Maybe (Maybe(..))
77
import Effect (Effect)
88
import Effect.Exception (throw)
9-
import React.Basic (element)
109
import React.Basic.DOM (render)
1110
import Web.DOM.NonElementParentNode (getElementById)
1211
import Web.HTML (window)
@@ -19,5 +18,5 @@ main = do
1918
case container of
2019
Nothing -> throw "Container element not found."
2120
Just c ->
22-
let app = element ControlledInput.component {}
21+
let app = controlledInput unit
2322
in render app c

examples/counter/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"dependencies": {
3-
"react": "^16.4.2",
4-
"react-dom": "^16.4.2"
3+
"react": "16.6.0",
4+
"react-dom": "16.6.0"
55
},
66
"devDependencies": {
7-
"browserify": "^16.2.2"
7+
"browserify": "16.2.3"
88
}
99
}

0 commit comments

Comments
 (0)