├── .gitignore
├── README.md
├── bsconfig.json
├── package.json
├── src
├── interop
│ ├── README.md
│ ├── index.html
│ ├── interopRoot.js
│ ├── myBanner.js
│ ├── myBannerRe.re
│ └── pageReason.re
├── logo
│ ├── constants.re
│ ├── index.html
│ ├── lifecycleTester.re
│ ├── logo.re
│ └── logoRoot.re
├── simple
│ ├── index.html
│ ├── page.re
│ └── simpleRoot.re
├── tictactoe
│ ├── board.re
│ ├── index.html
│ ├── square.re
│ └── tictactoe.re
└── todomvc
│ ├── app.re
│ ├── bam.re
│ ├── index.html
│ ├── todoFooter.re
│ └── todoItem.re
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .merlin
3 | /node_modules/
4 | /lib/
5 | /bundledOutputs/
6 | npm-debug.log
7 | /_build/
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a repo with examples usages of [Reason-React](https://github.com/reasonml/reason-react), whose docs are [here](https://github.com/reasonml/reason-react/blob/master/documentation.md).
2 | Have something you don't understand? Join us on [Discord](https://discord.gg/reasonml)!
3 |
4 | ## System setup
5 |
6 | ```sh
7 | # On OSX, install opam via Homebrew:
8 | brew update
9 | brew install opam
10 | # On Linux, see here (you'll need opam >= 1.2.2): http://opam.ocaml.org/doc/Install.html
11 |
12 | opam init
13 |
14 | # **Note**: add the line below to your ~/.bashrc or ~/.zshrc too; it's needed at every shell startup
15 | eval $(opam config env)
16 | opam update
17 | opam switch 4.02.3
18 |
19 | opam install reason
20 | opam install merlin
21 | ```
22 |
23 | ## Install project and dependencies
24 |
25 | ```sh
26 | # Clone the repo
27 | git clone https://github.com/chenglou/reason-react-example.git
28 | cd reason-react-example
29 |
30 | npm install
31 | npm start
32 | # in another tab
33 | npm run build
34 | # open the html file directly in your browsers e.g. src/tictactoe/index.html
35 | ```
36 |
37 | After you see the webpack compilation succeed (the `npm run build` step), open up the nested html files in `src/*`! Then modify whichever file in `src` and refresh the page to see the changes.
38 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | /* This is the BuckleScript configuration file. Note that this is a comment;
2 | BuckleScript comes with a JSON parser that supports comments and trailing
3 | comma. If this screws with your editor highlighting, please tell us by filing
4 | an issue! */
5 | {
6 | "name" : "reason-react-example",
7 | "reason" : { "react-jsx" : true},
8 | "bs-dependencies": ["reason-react", "reason-js"],
9 | "sources": [
10 | {
11 | "dir": "src",
12 | "subdirs": ["interop", "logo", "simple", "todomvc", "tictactoe"],
13 | }
14 | ],
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reason-react-example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "bsb -make-world -w",
10 | "build": "webpack -w",
11 | "clean": "bsb -clean-world"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "director": "^1.2.0",
18 | "react-dom": "^15.4.2",
19 | "react": "^15.4.2",
20 | "reason-js": "0.3.0",
21 | "reason-react": "https://github.com/reasonml/reason-react.git",
22 | "todomvc-app-css": "^2.0.0",
23 | "todomvc-common": "^1.0.1"
24 | },
25 | "devDependencies": {
26 | "bs-platform": "^1.6.0",
27 | "webpack": "^1.14.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/interop/README.md:
--------------------------------------------------------------------------------
1 | ## Interoperate with Existing ReactJS Components
2 |
3 | This subdirectory demonstrate the Reason-React <-> ReactJS interop APIs.
4 |
5 | The entry point, `interopRoot.js`, illustrates ReactJS requiring a Reason-React component, `PageReason`.
6 |
7 | `PageReason` itself illustrates Reason-React requiring a ReactJS component, `myBanner.js`, through the Reason file `myBannerRe.re`.
8 |
--------------------------------------------------------------------------------
/src/interop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pure Reason Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/interop/interopRoot.js:
--------------------------------------------------------------------------------
1 | var ReactDOM = require('react-dom');
2 | var React = require('react');
3 |
4 | // Import a Reason-React component! `comp` is the exposed, underlying ReactJS class
5 | var PageReason = require('../../lib/js/src/interop/pageReason').comp;
6 |
7 | var App = React.createClass({
8 | render: function() {
9 | return React.createElement('div', null,
10 | React.createElement(PageReason, {message: 'Hello!'})
11 | );
12 | // didn't feel like dragging in Babel. Here's the equivalent JSX:
13 | //
14 | }
15 | });
16 |
17 | ReactDOM.render(React.createElement(App), document.getElementById('index'));
18 |
--------------------------------------------------------------------------------
/src/interop/myBanner.js:
--------------------------------------------------------------------------------
1 | var ReactDOM = require('react-dom');
2 | var React = require('react');
3 |
4 | var App = React.createClass({
5 | render: function() {
6 | if (this.props.show) {
7 | return React.createElement('div', null,
8 | this.props.message
9 | );
10 | } else {
11 | return null;
12 | }
13 | }
14 | });
15 |
16 | module.exports = App;
17 |
--------------------------------------------------------------------------------
/src/interop/myBannerRe.re:
--------------------------------------------------------------------------------
1 | /* Typing the myBanner.js component's output as a `reactClass`. */
2 | /* Note that this file's JS output is located at reason-react-example/lib/js/src/interop/myBannerRe.js; we're specifying the relative path to myBanner.js in the string below */
3 | external myBanner : ReactRe.reactClass = "../../../../src/interop/myBanner" [@@bs.module];
4 |
5 | let createElement ::show ::message =>
6 | ReactRe.wrapPropsShamelessly
7 | myBanner
8 | {
9 | "show": Js.Boolean.to_js_boolean show, /* ^ don't forget to convert an OCaml bool into a JS boolean! */
10 | "message": message /* OCaml string maps to JS string, no conversion needed here */
11 | };
12 |
--------------------------------------------------------------------------------
/src/interop/pageReason.re:
--------------------------------------------------------------------------------
1 | module PageReason = {
2 | include ReactRe.Component.JsProps;
3 | type props = {message: string, extraGreeting: option string};
4 | let name = "PageReason";
5 | let render {props} => {
6 | let greeting =
7 | switch props.extraGreeting {
8 | | None => "How are you?"
9 | | Some g => g
10 | };
11 |
12 | };
13 | /* extraGreeting is optional, which means the JS side might pass in a null or undefined */
14 | type jsProps = Js.t {. message : string, extraGreeting : Js.null_undefined string};
15 | let jsPropsToReasonProps =
16 | Some (
17 | fun jsProps => {
18 | message: jsProps##message,
19 | extraGreeting: Js.Null_undefined.to_opt jsProps##extraGreeting
20 | }
21 | );
22 | };
23 |
24 | include ReactRe.CreateComponent PageReason;
25 |
26 | let createElement ::message ::extraGreeting=? => wrapProps {message, extraGreeting};
27 |
--------------------------------------------------------------------------------
/src/logo/constants.re:
--------------------------------------------------------------------------------
1 | let mouseUpDrag = 0.978;
2 |
3 | let mouseDownDrag = 0.9;
4 |
5 | let maxVel = 11.0;
6 |
7 | let clickAccel = 3.0;
8 |
9 | let baseVel = 0.05;
10 |
11 | let border_path = "M3.00191459,4 C1.34400294,4 0,5.34785514 0,7.00550479 L0,220.994495 C0,222.65439 1.34239483,224 3.00191459,224 L276.998085,224 C278.655997,224 280,222.652145 280,220.994495 L280,7.00550479 C280,5.34561033 278.657605,4 276.998085,4 L3.00191459,4 Z M3.00191459,4";
12 |
13 | let bg_path = "M3.00191459,1 C1.34400294,1 0,2.34785514 0,4.00550479 L0,217.994495 C0,219.65439 1.34239483,221 3.00191459,221 L276.998085,221 C278.655997,221 280,219.652145 280,217.994495 L280,4.00550479 C280,2.34561033 278.657605,1 276.998085,1 L3.00191459,1 Z M3.00191459,1";
14 |
15 | let center_dot_path = "M84,105 C92.8365564,105 100,97.8365564 100,89 C100,80.1634436 92.8365564,73 84,73 C75.1634436,73 68,80.1634436 68,89 C68,97.8365564 75.1634436,105 84,105 Z M84,105";
16 |
17 | let ring_one_path = "M84,121 C130.391921,121 168,106.673113 168,89 C168,71.3268871 130.391921,57 84,57 C37.6080787,57 0,71.3268871 0,89 C0,106.673113 37.6080787,121 84,121 Z M84,121";
18 |
19 | let ring_two_path = "M84,121 C130.391921,121 168,106.673113 168,89 C168,71.3268871 130.391921,57 84,57 C37.6080787,57 0,71.3268871 0,89 C0,106.673113 37.6080787,121 84,121 Z M84,121";
20 |
21 | let ring_three_path = "M84,121 C130.391921,121 168,106.673113 168,89 C168,71.3268871 130.391921,57 84,57 C37.6080787,57 0,71.3268871 0,89 C0,106.673113 37.6080787,121 84,121 Z M84,121";
22 |
23 | let ring_two_rotate = "translate(84.000000, 89.000000) rotate(-240.000000) translate(-84.000000, -89.000000)";
24 |
25 | let ring_three_rotate = "translate(84.000000, 89.000000) rotate(-300.000000) translate(-84.000000, -89.000000)";
26 |
--------------------------------------------------------------------------------
/src/logo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pure Reason Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/logo/lifecycleTester.re:
--------------------------------------------------------------------------------
1 | module StandardTesterImpl = {
2 | include ReactRe.Component.Stateful;
3 | type props = {incrOnDidMount: bool, initialCount: int, incr: int};
4 | type state = {clicks: int};
5 | let name = "StandardTester";
6 | let getInitialState props => {clicks: props.initialCount};
7 | let handleClick {props, state} _ /* event */ => Some {clicks: state.clicks + props.incr};
8 | let componentDidMount {props, state} => {
9 | print_string "IN MOUNT";
10 | print_newline ();
11 | props.incrOnDidMount ? Some {clicks: state.clicks + props.incr} : None
12 | };
13 | let render {state, updater} =>
14 |
15 |
18 |
19 |
;
20 | };
21 |
22 | module StandardTester = {
23 | include ReactRe.CreateComponent StandardTesterImpl;
24 | let createElement ::incrOnDidMount=false ::incr=0 ::initialCount=0 =>
25 | wrapProps {incrOnDidMount, initialCount, incr};
26 | };
27 |
28 | module WithStatefulInstanceVarsImpl = {
29 | include ReactRe.Component.Stateful.InstanceVars;
30 | type props = {incrOnDidMount: bool, initialCount: int, incr: int};
31 | type state = {clicks: int};
32 | type instanceVars = (int, string);
33 | let name = "LifecycleTester";
34 | let getInitialState props => {clicks: props.initialCount};
35 | let getInstanceVars () => (10, "test");
36 | let handleClick {props, state} _ /* event */ => Some {clicks: state.clicks + props.incr};
37 | let componentDidMount {props, state} =>
38 | props.incrOnDidMount ? Some {clicks: state.clicks + props.incr} : None;
39 | let render {state, updater} =>
40 |
41 |
44 |
45 |
;
46 | };
47 |
48 | module WithStatefulInstanceVars = {
49 | include ReactRe.CreateComponent WithStatefulInstanceVarsImpl;
50 | let createElement ::incrOnDidMount=false ::incr=0 ::initialCount=0 =>
51 | wrapProps {incrOnDidMount, initialCount, incr};
52 | };
53 |
--------------------------------------------------------------------------------
/src/logo/logo.re:
--------------------------------------------------------------------------------
1 | external requestAnimationFrame : (unit => unit) => unit = "" [@@bs.val];
2 |
3 | open Constants;
4 |
5 | module Logo = {
6 |
7 | /**
8 | * Include Stateful Component Base!
9 | */
10 | include ReactRe.Component.Stateful;
11 | type state = {degrees: float, velocity: float, drag: float, lastMs: float};
12 | type props = {message: string};
13 | let getInitialState _ /* props */ => {
14 | drag: mouseUpDrag,
15 | degrees: 0.0,
16 | velocity: 0.1,
17 | lastMs: Js.Date.now ()
18 | };
19 | let componentDidMount {setState} => {
20 | let rec onAnimationFrame () => {
21 | let stateSetter {state} => {
22 | let now = Js.Date.now ();
23 | /* How many 16ms virtual frames elapsed, even if clock runs at 30hz */
24 | let idealFramesSinceLast = 1. +. (now -. state.lastMs) /. 16.;
25 | let nextDegrees = state.degrees +. (baseVel +. state.velocity) *. idealFramesSinceLast;
26 | let nextVelocity = state.velocity *. state.drag;
27 | {...state, degrees: nextDegrees, velocity: nextVelocity, lastMs: now}
28 | };
29 | setState stateSetter;
30 | requestAnimationFrame onAnimationFrame
31 | };
32 | requestAnimationFrame onAnimationFrame;
33 | None
34 | };
35 | let name = "Logo";
36 | let renderGraphic rotationStyle =>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | ;
53 |
54 | /**
55 | * On Mouse Up.
56 | */
57 | let handleMouseUp {state} _ /* event */ => {
58 | let withAccel = state.velocity +. clickAccel;
59 | let nextVelocity = withAccel < maxVel ? withAccel : maxVel;
60 | Some {...state, velocity: nextVelocity, drag: mouseUpDrag}
61 | };
62 |
63 | /**
64 | * On Mouse Down.
65 | */
66 | let handleMouseDown {state} _ /* event */ => Some {...state, drag: mouseDownDrag};
67 | let render {props, state, updater} => {
68 | let transform = "rotate(" ^ string_of_float state.degrees ^ "deg)";
69 | /* To create JS Objects in Reason, */
70 | let rotationStyle = ReactDOMRe.Style.make transformOrigin::"50% 50%" ::transform ();
71 |
82 | (ReactRe.stringToElement props.message)
83 |
93 |
94 | };
95 | };
96 |
97 | include ReactRe.CreateComponent Logo;
98 |
99 | let createElement ::message => wrapProps {message: message};
100 |
--------------------------------------------------------------------------------
/src/logo/logoRoot.re:
--------------------------------------------------------------------------------
1 | ReactDOMRe.renderToElementWithId "index";
2 |
--------------------------------------------------------------------------------
/src/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pure Reason Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/simple/page.re:
--------------------------------------------------------------------------------
1 | module Page = {
2 | include ReactRe.Component;
3 | type props = {message: string};
4 | let name = "Page";
5 | let handleClick _ _ => {
6 | Js.log "clicked!";
7 | None
8 | };
9 | let render {props, updater} =>
10 | (ReactRe.stringToElement props.message)
;
11 | };
12 |
13 | include ReactRe.CreateComponent Page;
14 |
15 | let createElement ::message => wrapProps {message: message};
16 |
--------------------------------------------------------------------------------
/src/simple/simpleRoot.re:
--------------------------------------------------------------------------------
1 | ReactDOMRe.renderToElementWithId "index";
2 |
--------------------------------------------------------------------------------
/src/tictactoe/board.re:
--------------------------------------------------------------------------------
1 | module Board = {
2 | include ReactRe.Component;
3 | type props = {squares: list Square.squareState, handleSquareClick: int => unit};
4 | let name = "Board";
5 | let handleClick i {props} _ => {
6 | props.handleSquareClick i;
7 | None
8 | };
9 | let breakOnThree i => i mod 3 == 0 ?
: ;
10 | let render {props, updater} => {
11 | let foo =
12 | props.squares |>
13 | List.mapi (
14 | fun i s =>
15 | (breakOnThree i)
16 | );
17 | (ReactRe.arrayToElement (Array.of_list foo))
18 | };
19 | };
20 |
21 | include ReactRe.CreateComponent Board;
22 |
23 | let createElement ::squares ::handleSquareClick => wrapProps {squares, handleSquareClick};
24 |
--------------------------------------------------------------------------------
/src/tictactoe/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reason-React • TicTacToe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/tictactoe/square.re:
--------------------------------------------------------------------------------
1 | type squareState =
2 | | Cross
3 | | Circle
4 | | Empty;
5 |
6 | let emptySvg = "M571 589q-10-25-34-35t-49 0q-108 44-191 127t-127 191q-10 25 0 49t35 34q13 5 24 5 42 0 60-40 34-84 98.5-148.5t148.5-98.5q25-11 35-35t0-49zm942-356l46 46-244 243 68 68q19 19 19 45.5t-19 45.5l-64 64q89 161 89 343 0 143-55.5 273.5t-150 225-225 150-273.5 55.5-273.5-55.5-225-150-150-225-55.5-273.5 55.5-273.5 150-225 225-150 273.5-55.5q182 0 343 89l64-64q19-19 45.5-19t45.5 19l68 68zm8-56q-10 10-22 10-13 0-23-10l-91-90q-9-10-9-23t9-23q10-9 23-9t23 9l90 91q10 9 10 22.5t-10 22.5zm230 230q-11 9-23 9t-23-9l-90-91q-10-9-10-22.5t10-22.5q9-10 22.5-10t22.5 10l91 90q9 10 9 23t-9 23zm41-183q0 14-9 23t-23 9h-96q-14 0-23-9t-9-23 9-23 23-9h96q14 0 23 9t9 23zm-192-192v96q0 14-9 23t-23 9-23-9-9-23v-96q0-14 9-23t23-9 23 9 9 23zm151 55l-91 90q-10 10-22 10-13 0-23-10-10-9-10-22.5t10-22.5l90-91q10-9 23-9t23 9q9 10 9 23t-9 23z";
7 |
8 | let crossSvg = "M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z";
9 |
10 | let circleSvg = "M896 352q-148 0-273 73t-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273-73-273-198-198-273-73zm768 544q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z";
11 |
12 | module Square = {
13 | include ReactRe.Component;
14 | type props = {value: squareState, handleClick: ReactEventRe.Mouse.t => unit};
15 | let name = "Square";
16 | let squareStyle =
17 | ReactDOMRe.Style.make width::"25px" backgroundColor::"blue" fontSize::"100pt" ();
18 | let render {props} =>
19 |
20 | (
21 | switch props.value {
22 | | Cross =>
23 |
26 | | Circle =>
27 |
30 | | Empty =>
31 |
34 | }
35 | )
36 | ;
37 | };
38 |
39 | include ReactRe.CreateComponent Square;
40 |
41 | let createElement ::value ::handleClick => wrapProps {value, handleClick};
42 |
--------------------------------------------------------------------------------
/src/tictactoe/tictactoe.re:
--------------------------------------------------------------------------------
1 | let namespace = "reason-react-tictactoe";
2 |
3 | type playerState =
4 | | Circle
5 | | Cross;
6 |
7 | module Top = {
8 | module TicTacToe = {
9 | include ReactRe.Component.Stateful;
10 | let name = "TicTacToe";
11 | type props = unit;
12 | type state = {squares: list Square.squareState, player: playerState};
13 | let getInitialState _ => {
14 | squares: [Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty],
15 | player: Circle
16 | };
17 | let switchPlayer (current: playerState) =>
18 | switch current {
19 | | Circle => Cross
20 | | Cross => Circle
21 | };
22 | let isEmptySquare (square: Square.squareState) =>
23 | switch square {
24 | | Empty => true
25 | | _ => false
26 | };
27 | let eventuallySetSquare squares selection player =>
28 | List.mapi
29 | (
30 | fun i square =>
31 | if (i == selection && isEmptySquare square) {
32 | switch player {
33 | | Circle => Square.Circle
34 | | Cross => Square.Cross
35 | }
36 | } else {
37 | square
38 | }
39 | )
40 | squares;
41 | let playTurn {state} (selection: int) => {
42 | let {squares, player} = state;
43 | Some {
44 | squares: eventuallySetSquare squares selection player,
45 | player: switchPlayer state.player
46 | }
47 | };
48 | let render {state, updater} =>
49 | ;
55 | };
56 | include ReactRe.CreateComponent TicTacToe;
57 | let createElement = wrapProps ();
58 | };
59 |
60 | ReactDOMRe.renderToElementWithClassName "tictactoe";
61 |
--------------------------------------------------------------------------------
/src/todomvc/app.re:
--------------------------------------------------------------------------------
1 | type router = Js.t {. init : (string => unit) [@bs.meth]};
2 |
3 | external routerMake : Js.t {..} => router = "Router" [@@bs.module "director"] [@@bs.new];
4 |
5 | external unsafeJsonParse : string => 'a = "JSON.parse" [@@bs.val];
6 |
7 | let namespace = "reason-react-todos";
8 |
9 | let saveLocally todos =>
10 | switch (Js.Json.stringifyAny todos) {
11 | | None => ()
12 | | Some stringifiedTodos =>
13 | ReasonJs.Storage.setItem namespace stringifiedTodos ReasonJs.Storage.localStorage
14 | };
15 |
16 | module Top = {
17 | module TodoApp = {
18 | include ReactRe.Component.Stateful;
19 | let name = "TodoAppRe";
20 | type props = unit;
21 | type state = {
22 | nowShowing: TodoFooter.TodoFooter.showingState,
23 | editing: option string,
24 | newTodo: string,
25 | todos: list TodoItem.todo
26 | };
27 | let getInitialState _ /* props */ => {
28 | let todos =
29 | switch (ReasonJs.Storage.getItem namespace ReasonJs.Storage.localStorage) {
30 | | None => []
31 | | Some todos => unsafeJsonParse todos
32 | };
33 | {nowShowing: AllTodos, editing: None, newTodo: "", todos}
34 | };
35 | let componentDidMount {updater} => {
36 | let f1 {state} () => Some {...state, nowShowing: AllTodos};
37 | let f2 {state} () => Some {...state, nowShowing: ActiveTodos};
38 | let f3 {state} () => Some {...state, nowShowing: CompletedTodos};
39 | let router = routerMake {"/": updater f1, "/active": updater f2, "/completed": updater f3};
40 | router##init "/";
41 | None
42 | };
43 | let handleChange {state} event =>
44 | Some {
45 | ...state,
46 | newTodo: (ReactDOMRe.domElementToObj (ReactEventRe.Form.target event))##value
47 | };
48 | let handleNewTodoKeyDown {state} event =>
49 | if (ReactEventRe.Keyboard.keyCode event === 13 /* enter key */) {
50 | ReactEventRe.Keyboard.preventDefault event;
51 | switch (String.trim state.newTodo) {
52 | | "" => None
53 | | nonEmptyValue =>
54 | let todos =
55 | state.todos @ [
56 | {id: string_of_float (Js.Date.now ()), title: nonEmptyValue, completed: false}
57 | ];
58 | saveLocally todos;
59 | Some {...state, newTodo: "", todos}
60 | }
61 | } else {
62 | None
63 | };
64 | let toggleAll {state} event => {
65 | let checked = (ReactDOMRe.domElementToObj (ReactEventRe.Form.target event))##checked;
66 | let todos =
67 | List.map (fun todo => {...todo, TodoItem.completed: Js.to_bool checked}) state.todos;
68 | saveLocally todos;
69 | Some {...state, todos}
70 | };
71 | let toggle todoToToggle {state} _ => {
72 | let todos =
73 | List.map
74 | (
75 | fun todo =>
76 | todo == todoToToggle ?
77 | {...todo, TodoItem.completed: not TodoItem.(todo.completed)} : todo
78 | )
79 | state.todos;
80 | saveLocally todos;
81 | Some {...state, todos}
82 | };
83 | let destroy todo {state} _ => {
84 | let todos = List.filter (fun candidate => candidate !== todo) state.todos;
85 | saveLocally todos;
86 | Some {...state, todos}
87 | };
88 | let edit todo {state} _ => Some {...state, editing: Some TodoItem.(todo.id)};
89 | let save todoToSave {state} text => {
90 | let todos =
91 | List.map
92 | (fun todo => todo == todoToSave ? {...todo, TodoItem.title: text} : todo) state.todos;
93 | Some {...state, editing: None, todos}
94 | };
95 | let cancel {state} _ => Some {...state, editing: None};
96 | let clearCompleted {state} _ => {
97 | let todos = List.filter (fun todo => not TodoItem.(todo.completed)) state.todos;
98 | saveLocally todos;
99 | Some {...state, todos}
100 | };
101 | let render {state, updater} => {
102 | let {todos, editing} = state;
103 | let todoItems =
104 | todos |>
105 | List.filter (
106 | fun todo =>
107 | TodoItem.(
108 | switch state.nowShowing {
109 | | ActiveTodos => not todo.completed
110 | | CompletedTodos => todo.completed
111 | | AllTodos => true
112 | }
113 | )
114 | ) |>
115 | List.map (
116 | fun todo => {
117 | let editing =
118 | switch editing {
119 | | None => false
120 | | Some editing => editing === TodoItem.(todo.id)
121 | };
122 |
132 | }
133 | );
134 | let todosLength = List.length todos;
135 |
136 | let completedCount =
137 | todos |> List.filter (fun todo => TodoItem.(todo.completed)) |> List.length;
138 | let activeTodoCount = todosLength - completedCount;
139 | let bam =
140 | switch (activeTodoCount, completedCount) {
141 | | (0, 0) => ReactRe.nullElement
142 | | _ =>
143 | ;
147 | };
148 | let footer =
149 | switch (activeTodoCount, completedCount) {
150 | | (0, 0) => ReactRe.nullElement
151 | | _ =>
152 |
158 | };
159 | let main =
160 | todosLength === 0 ?
161 | ReactRe.nullElement :
162 | ;
171 |
172 |
183 | main
184 | bam
185 | footer
186 |
187 | };
188 | };
189 | include ReactRe.CreateComponent TodoApp;
190 | let createElement = wrapProps ();
191 | };
192 |
193 | ReactDOMRe.renderToElementWithClassName "todoapp";
194 |
--------------------------------------------------------------------------------
/src/todomvc/bam.re:
--------------------------------------------------------------------------------
1 | type foo = string;
2 |
3 | module Bam = {
4 | include ReactRe.Component;
5 |
6 | let name = "Bam";
7 | type props = {
8 | title: string,
9 | description: string
10 | };
11 |
12 | let render {props} => {
13 |
14 |
(ReactRe.stringToElement props.title)
15 | (ReactRe.stringToElement props.description)
16 |
17 | };
18 | };
19 |
20 | include ReactRe.CreateComponent Bam;
21 |
22 | let createElement ::title ::description => wrapProps {title, description};
23 |
--------------------------------------------------------------------------------
/src/todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reason-React • TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/todomvc/todoFooter.re:
--------------------------------------------------------------------------------
1 | module TodoFooter = {
2 | include ReactRe.Component;
3 | let name = "TodoFooterRe";
4 | type showingState =
5 | | AllTodos
6 | | ActiveTodos
7 | | CompletedTodos;
8 | type props = {
9 | count: int,
10 | completedCount: int,
11 | nowShowing: showingState,
12 | onClearCompleted: ReactEventRe.Mouse.t => unit
13 | };
14 | let render {props} => {
15 | let activeTodoWord = props.count === 1 ? "item" : "items";
16 | let clearButton =
17 | props.completedCount > 0 ?
18 | :
21 | ReactRe.nullElement;
22 | let (all, active, completed) =
23 | switch props.nowShowing {
24 | | AllTodos => ("selected", "", "")
25 | | ActiveTodos => ("", "selected", "")
26 | | CompletedTodos => ("", "", "selected")
27 | };
28 |
45 | };
46 | };
47 |
48 | include ReactRe.CreateComponent TodoFooter;
49 |
50 | let createElement ::count ::completedCount ::nowShowing ::onClearCompleted =>
51 | wrapProps {count, completedCount, nowShowing, onClearCompleted};
52 |
--------------------------------------------------------------------------------
/src/todomvc/todoItem.re:
--------------------------------------------------------------------------------
1 | let escapeKey = 27;
2 |
3 | let enterKey = 13;
4 |
5 | type todo = {id: string, title: string, completed: bool};
6 |
7 | module TodoItem = {
8 | include ReactRe.Component.Stateful.InstanceVars;
9 | let name = "TodoItemRe";
10 | type props = {
11 | todo,
12 | editing: bool,
13 | onDestroy: unit => unit,
14 | onSave: string => unit,
15 | onEdit: unit => unit,
16 | onToggle: unit => unit,
17 | onCancel: unit => unit
18 | };
19 | type state = {editText: string};
20 | type instanceVars = {mutable editFieldRef: option Dom.element};
21 | let getInstanceVars () => {editFieldRef: None};
22 | let getInitialState props => {editText: props.todo.title};
23 | let handleSubmit {props, state} _ =>
24 | switch (String.trim state.editText) {
25 | | "" =>
26 | props.onDestroy ();
27 | None
28 | | nonEmptyValue =>
29 | props.onSave nonEmptyValue;
30 | Some {editText: nonEmptyValue}
31 | };
32 | let handleEdit {props} _ /* event */ => {
33 | props.onEdit ();
34 | Some {editText: props.todo.title}
35 | };
36 | let handleKeyDown ({props} as componentBag) event =>
37 | if (ReactEventRe.Keyboard.which event === escapeKey) {
38 | props.onCancel ();
39 | Some {editText: props.todo.title}
40 | } else if (
41 | ReactEventRe.Keyboard.which event === enterKey
42 | ) {
43 | handleSubmit componentBag ()
44 | } else {
45 | None
46 | };
47 | let handleChange {props} event =>
48 | props.editing ?
49 | Some {editText: (ReactDOMRe.domElementToObj (ReactEventRe.Form.target event))##value} : None;
50 | let setEditFieldRef {instanceVars} r => instanceVars.editFieldRef = Some r;
51 |
52 | /**
53 | * Safely manipulate the DOM after updating the state when invoking
54 | * `props.onEdit()` in the `handleEdit` method above.
55 | * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
56 | * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
57 | */
58 | let componentDidUpdate ::prevProps prevState::_ {props, instanceVars} =>
59 | switch (prevProps.editing, props.editing, instanceVars.editFieldRef) {
60 | | (false, true, Some field) =>
61 | let node = ReactDOMRe.domElementToObj field;
62 | ignore (node##focus ());
63 | ignore (node##setSelectionRange node##value##length node##value##length);
64 | None
65 | | _ => None
66 | };
67 | let render {props, state, updater, handler} => {
68 | let {todo, editing, onDestroy, onToggle} = props;
69 | let className =
70 | [todo.completed ? "completed" : "", editing ? "editing" : ""] |> String.concat " ";
71 |
72 |
73 | onToggle ())
78 | />
79 |
80 |
82 |
90 |
91 | };
92 | };
93 |
94 | include ReactRe.CreateComponent TodoItem;
95 |
96 | let createElement ::todo ::editing ::onDestroy ::onSave ::onEdit ::onToggle ::onCancel =>
97 | wrapProps {todo, editing, onDestroy, onSave, onEdit, onToggle, onCancel};
98 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: {
5 | simple: './lib/js/src/simple/simpleRoot.js',
6 | logo: './lib/js/src/logo/logoRoot.js',
7 | todomvc: './lib/js/src/todomvc/app.js',
8 | interop: './src/interop/interopRoot.js',
9 | tictactoe: './lib/js/src/tictactoe/tictactoe.js',
10 | },
11 | output: {
12 | path: path.join(__dirname, 'bundledOutputs'),
13 | filename: '[name].js',
14 | },
15 | };
16 |
--------------------------------------------------------------------------------