├── .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 | 91 | (renderGraphic rotationStyle) 92 | 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 | 24 | 25 | 26 | | Circle => 27 | 28 | 29 | 30 | | Empty => 31 | 32 | 33 | 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 |
50 |
51 | 52 |
53 |
54 |
; 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 |
163 | 169 |
    (ReactRe.arrayToElement (Array.of_list todoItems))
170 |
; 171 |
172 |
173 |

(ReactRe.stringToElement "todos")

174 | 182 |
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 |
12 |

Double-click to edit a todo

13 |

Created by chenglou

14 |

Part of TodoMVC

15 |
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 | --------------------------------------------------------------------------------