├── .gitignore ├── README.md ├── bsconfig.json ├── ocaml-nyc-2019-02.key ├── ocaml-nyc-2019-02.pdf ├── package-lock.json ├── package.json ├── ppx ├── reactjs_jsx_ppx_v3.exe ├── reactjs_jsx_ppx_v3.ml └── reactjs_jsx_ppx_v3.mli ├── src ├── animation │ ├── 0.jpg │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── Animation.bs.js │ ├── Animation.re │ ├── Animation.rei │ ├── AnimationRoot.bs.js │ ├── AnimationRoot.re │ ├── Demo.bs.js │ ├── Demo.re │ ├── Reanimate.bs.js │ ├── Reanimate.re │ ├── RemoteAction.bs.js │ ├── RemoteAction.re │ ├── RemoteAction.rei │ ├── Spring.bs.js │ ├── Spring.re │ ├── SpringAnimation.bs.js │ ├── SpringAnimation.re │ ├── SpringAnimation.rei │ ├── head0.jpg │ ├── head1.jpg │ ├── head2.jpg │ ├── head3.jpg │ ├── head4.jpg │ ├── head5.jpg │ ├── index.html │ └── style.css ├── async │ ├── Counter.bs.js │ ├── Counter.re │ ├── CounterRoot.bs.js │ ├── CounterRoot.re │ └── index.html ├── fetch │ ├── FetchExample.bs.js │ ├── FetchExample.re │ ├── FetchExampleRoot.bs.js │ ├── FetchExampleRoot.re │ └── index.html ├── hooks-animation │ ├── 0.jpg │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── HooksAnimation.bs.js │ ├── HooksAnimation.re │ ├── HooksAnimation.rei │ ├── HooksAnimationRoot.bs.js │ ├── HooksAnimationRoot.re │ ├── HooksDemo.bs.js │ ├── HooksDemo.re │ ├── HooksReanimate.bs.js │ ├── HooksReanimate.re │ ├── HooksRemoteAction.bs.js │ ├── HooksRemoteAction.re │ ├── HooksRemoteAction.rei │ ├── HooksSpring.bs.js │ ├── HooksSpring.re │ ├── HooksSpringAnimation.bs.js │ ├── HooksSpringAnimation.re │ ├── HooksSpringAnimation.rei │ ├── head0.jpg │ ├── head1.jpg │ ├── head2.jpg │ ├── head3.jpg │ ├── head4.jpg │ ├── head5.jpg │ ├── index.html │ └── style.css ├── hooks │ ├── HooksPage.bs.js │ ├── HooksPage.re │ ├── HooksRoot.bs.js │ ├── HooksRoot.re │ └── index.html ├── index.html ├── interop │ ├── GreetingRe.bs.js │ ├── GreetingRe.re │ ├── InteropRoot.js │ ├── MyBanner.js │ ├── MyBannerRe.bs.js │ ├── MyBannerRe.re │ ├── README.md │ └── index.html ├── retainedProps │ ├── RetainedPropsExample.bs.js │ ├── RetainedPropsExample.re │ ├── RetainedPropsRoot.bs.js │ ├── RetainedPropsRoot.re │ └── index.html ├── simple │ ├── Page.bs.js │ ├── Page.re │ ├── SimpleRoot.bs.js │ ├── SimpleRoot.re │ └── index.html └── todomvc │ ├── App.bs.js │ ├── App.re │ ├── TodoFooter.bs.js │ ├── TodoFooter.re │ ├── TodoItem.bs.js │ ├── TodoItem.re │ └── index.html ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .merlin 2 | /node_modules/ 3 | /lib/* 4 | !/lib/js/* 5 | /bundledOutputs/ 6 | npm-debug.log 7 | .DS_Store 8 | .bsb.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains a fork of [reason-react-example](https://github.com/reasonml-community/reason-react-example) with code snippets of hooks as well as my slides from the OCaml NYC Meetup in Feb. Feel free to play around and let me know if you run into any issues! The React and ppx PRs will be made soon to make it easier to try in your own projects! 2 | 3 | Code changes: 4 | src/hooks 5 | src/hooks-animation/HooksReanimate.re 6 | 7 | ------------------------ 8 | 9 | This is a repo with examples usages of [ReasonReact](https://github.com/reasonml/reason-react), whose docs are [here](https://reasonml.github.io/reason-react/). 10 | Have something you don't understand? Join us on [Discord](https://discord.gg/reasonml)! 11 | 12 | ```sh 13 | git clone https://github.com/chenglou/reason-react-example.git 14 | cd reason-react-example 15 | npm install 16 | npm run build 17 | npm run webpack 18 | ``` 19 | 20 | Then open `src/index.html` to see the links to the examples (**no server needed!**). 21 | 22 | ## Watch File Changes 23 | 24 | The above commands works for a one-time build. To continuously build when a file changes, do: 25 | 26 | ```sh 27 | npm start 28 | ``` 29 | 30 | Then in another tab, do: 31 | 32 | ```sh 33 | npm run webpack 34 | ``` 35 | 36 | You can then modify whichever file in `src` and refresh the html page to see the changes. 37 | 38 | ## Build for Production 39 | 40 | ```sh 41 | npm run build 42 | npm run webpack:production 43 | ``` 44 | 45 | This will replace the development JS artifact for an optimized version. 46 | 47 | **To enable dead code elimination**, change `bsconfig.json`'s `package-specs` `module` from `"commonjs"` to `"es6"`. Then re-run the above 2 commands. This will allow Webpack to remove unused code. 48 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-example", 3 | "bsc-flags": ["-bs-no-version-header", "-bs-super-errors"], 4 | "ppx-flags": [ 5 | "./ppx/reactjs_jsx_ppx_v3.exe"], 6 | "namespace": true, 7 | "bs-dependencies": [ 8 | "reason-react", 9 | "bs-fetch", 10 | "@glennsl/bs-json" 11 | ], 12 | "sources": [ 13 | { 14 | "dir": "src", 15 | "subdirs": true 16 | } 17 | ], 18 | "package-specs": { 19 | "module": "commonjs", 20 | "in-source": true 21 | }, 22 | "suffix": ".bs.js", 23 | "refmt": 3 24 | } 25 | -------------------------------------------------------------------------------- /ocaml-nyc-2019-02.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/ocaml-nyc-2019-02.key -------------------------------------------------------------------------------- /ocaml-nyc-2019-02.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/ocaml-nyc-2019-02.pdf -------------------------------------------------------------------------------- /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 | "build": "bsb -make-world", 9 | "start": "bsb -make-world -w", 10 | "clean": "bsb -clean-world", 11 | "test": "exit 0", 12 | "webpack": "webpack -w", 13 | "webpack:production": "NODE_ENV=production webpack" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@glennsl/bs-json": "^3.0.0", 20 | "bs-fetch": "^0.3.0", 21 | "react": "16.8.1", 22 | "react-dom": "^16.8.1", 23 | "reason-react": "git://github.com/rickyvetter/reason-react.git#master", 24 | "todomvc-app-css": "^2.0.0", 25 | "todomvc-common": "^1.0.1" 26 | }, 27 | "devDependencies": { 28 | "bs-platform": "^4.0.3", 29 | "concurrently": "^3.5.0", 30 | "webpack": "^4.0.1", 31 | "webpack-cli": "^3.1.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ppx/reactjs_jsx_ppx_v3.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/ppx/reactjs_jsx_ppx_v3.exe -------------------------------------------------------------------------------- /ppx/reactjs_jsx_ppx_v3.mli: -------------------------------------------------------------------------------- 1 | (* 2 | This file's shared between the Reason repo and the BuckleScript repo. In 3 | Reason, it's in src. In BuckleScript, it's in vendor/reason We periodically 4 | copy this file from Reason (the source of truth) to BuckleScript, then 5 | uncomment the #if #else #end cppo macros you see in the file. That's because 6 | BuckleScript's on OCaml 4.02 while Reason's on 4.04; so the #if macros 7 | surround the pieces of code that are different between the two compilers. 8 | *) 9 | (* #if undefined BS_NO_COMPILER_PATCH then *) 10 | (* val ast_mapper : Ast_mapper.mapper *) 11 | (* #end *) 12 | -------------------------------------------------------------------------------- /src/animation/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/0.jpg -------------------------------------------------------------------------------- /src/animation/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/1.jpg -------------------------------------------------------------------------------- /src/animation/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/2.jpg -------------------------------------------------------------------------------- /src/animation/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/3.jpg -------------------------------------------------------------------------------- /src/animation/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/4.jpg -------------------------------------------------------------------------------- /src/animation/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/5.jpg -------------------------------------------------------------------------------- /src/animation/Animation.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 5 | 6 | function defaultCallback() { 7 | return /* Stop */[undefined]; 8 | } 9 | 10 | function create() { 11 | return /* record */[ 12 | /* id */undefined, 13 | /* callback */defaultCallback 14 | ]; 15 | } 16 | 17 | function onAnimationFrame(animation, _) { 18 | if (animation[/* id */0] !== undefined) { 19 | var match = animation[/* callback */1](); 20 | if (match) { 21 | var match$1 = match[0]; 22 | if (match$1 !== undefined) { 23 | animation[/* id */0] = undefined; 24 | return Curry._1(match$1, /* () */0); 25 | } else { 26 | animation[/* id */0] = undefined; 27 | return /* () */0; 28 | } 29 | } else { 30 | animation[/* id */0] = Js_primitive.some(requestAnimationFrame((function (param) { 31 | return onAnimationFrame(animation, param); 32 | }))); 33 | return /* () */0; 34 | } 35 | } else { 36 | return 0; 37 | } 38 | } 39 | 40 | function start(animation) { 41 | animation[/* id */0] = Js_primitive.some(requestAnimationFrame((function (param) { 42 | return onAnimationFrame(animation, param); 43 | }))); 44 | return /* () */0; 45 | } 46 | 47 | function stop(animation) { 48 | var match = animation[/* id */0]; 49 | if (match !== undefined) { 50 | cancelAnimationFrame(Js_primitive.valFromOption(match)); 51 | animation[/* id */0] = undefined; 52 | return /* () */0; 53 | } else { 54 | return /* () */0; 55 | } 56 | } 57 | 58 | function setCallback(animation, callback) { 59 | stop(animation); 60 | animation[/* callback */1] = callback; 61 | return /* () */0; 62 | } 63 | 64 | function isActive(animation) { 65 | return animation[/* id */0] !== undefined; 66 | } 67 | 68 | exports.create = create; 69 | exports.isActive = isActive; 70 | exports.setCallback = setCallback; 71 | exports.start = start; 72 | exports.stop = stop; 73 | /* No side effect */ 74 | -------------------------------------------------------------------------------- /src/animation/Animation.re: -------------------------------------------------------------------------------- 1 | type animationFrameID; 2 | 3 | [@bs.val] 4 | external requestAnimationFrame: (unit => unit) => animationFrameID = ""; 5 | 6 | [@bs.val] external cancelAnimationFrame: animationFrameID => unit = ""; 7 | 8 | type onStop = option(unit => unit); 9 | 10 | type ctrl = 11 | | Stop(onStop) 12 | | Continue; 13 | 14 | type callback = (. unit) => ctrl; 15 | 16 | type t = { 17 | mutable id: option(animationFrameID), 18 | mutable callback, 19 | }; 20 | 21 | let defaultCallback = (.) => Stop(None); 22 | 23 | let create = () => {id: None, callback: defaultCallback}; 24 | 25 | let rec onAnimationFrame = (animation, ()) => 26 | if (animation.id != None) { 27 | switch (animation.callback(.)) { 28 | | Stop(None) => animation.id = None 29 | | Stop(Some(onStop)) => 30 | animation.id = None; 31 | onStop(); 32 | | Continue => 33 | animation.id = 34 | Some(requestAnimationFrame(onAnimationFrame(animation))) 35 | }; 36 | }; 37 | 38 | let start = animation => 39 | animation.id = Some(requestAnimationFrame(onAnimationFrame(animation))); 40 | 41 | let stop = animation => 42 | switch (animation.id) { 43 | | Some(id) => 44 | cancelAnimationFrame(id); 45 | animation.id = None; 46 | | None => () 47 | }; 48 | 49 | let setCallback = (animation, ~callback) => { 50 | stop(animation); 51 | animation.callback = callback; 52 | }; 53 | 54 | let isActive = animation => animation.id != None; 55 | -------------------------------------------------------------------------------- /src/animation/Animation.rei: -------------------------------------------------------------------------------- 1 | type t; 2 | 3 | type onStop = option(unit => unit); 4 | 5 | type ctrl = 6 | | Stop(onStop) 7 | | Continue; 8 | 9 | type callback = (. unit) => ctrl; 10 | 11 | let create: unit => t; 12 | 13 | let isActive: t => bool; 14 | 15 | let setCallback: (t, ~callback: callback) => unit; 16 | 17 | let start: t => unit; 18 | 19 | let stop: t => unit; -------------------------------------------------------------------------------- /src/animation/AnimationRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var Reanimate$ReasonReactExample = require("./Reanimate.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, Reanimate$ReasonReactExample.ComponentGallery[/* make */1](/* array */[])), "index"); 8 | 9 | /* Not a pure module */ 10 | -------------------------------------------------------------------------------- /src/animation/AnimationRoot.re: -------------------------------------------------------------------------------- 1 | ReactDOMRe.renderToElementWithId( 2 | ReasonReact.element(Reanimate.ComponentGallery.make([||])), 3 | "index", 4 | ); 5 | -------------------------------------------------------------------------------- /src/animation/Demo.re: -------------------------------------------------------------------------------- 1 | module GlobalState = { 2 | type state = { 3 | count1: int, 4 | count2: int, 5 | toggle: bool, 6 | }; 7 | let initial = {count1: 0, count2: 0, toggle: false}; 8 | }; 9 | 10 | module Counter1 = { 11 | open GlobalState; 12 | let component = ReasonReact.statelessComponent("Counter1"); 13 | let make = (~state, ~update, _children) => { 14 | ...component, 15 | render: _ => 16 |
17 | 21 | 25 | (ReasonReact.string(" counter:" ++ string_of_int(state.count1))) 26 |
, 27 | }; 28 | }; 29 | 30 | module Counter2 = { 31 | open GlobalState; 32 | let component = ReasonReact.statelessComponent("Counter2"); 33 | let make = (~state, ~update, _children) => { 34 | ...component, 35 | render: _ => 36 |
37 | 41 | 45 | (ReasonReact.string(" counter:" ++ string_of_int(state.count2))) 46 |
, 47 | }; 48 | }; 49 | 50 | module Toggle = { 51 | open GlobalState; 52 | let component = ReasonReact.statelessComponent("Toggle"); 53 | let make = (~state, ~update, _children) => { 54 | ...component, 55 | render: _ => 56 |
57 | 61 | (ReasonReact.string(" toggle:" ++ string_of_bool(state.toggle))) 62 |
, 63 | }; 64 | }; 65 | 66 | module GlobalStateExample = { 67 | let component = ReasonReact.reducerComponent("GlobalStateExample"); 68 | let make = _children => { 69 | ...component, 70 | initialState: () => GlobalState.initial, 71 | reducer: (fn, state) => Update(fn(state)), 72 | render: ({state, send}) => { 73 | let update = foo => send(foo); 74 |
75 | 76 | 77 | 78 | 79 |
; 80 | }, 81 | }; 82 | }; 83 | 84 | module LocalCounter = { 85 | type state = int; 86 | type action = 87 | | Incr 88 | | Decr; 89 | let component = ReasonReact.reducerComponent("LocalCounter"); 90 | let make = _children => { 91 | ...component, 92 | initialState: () => 0, 93 | reducer: (action, state) => 94 | switch (action) { 95 | | Incr => Update(state + 1) 96 | | Decr => Update(state - 1) 97 | }, 98 | render: ({state, send}) => 99 |
100 | 103 | 106 | (ReasonReact.string(" counter:" ++ string_of_int(state))) 107 |
, 108 | }; 109 | }; 110 | 111 | module LocalToggle = { 112 | type state = bool; 113 | type action = 114 | | Toggle; 115 | let component = ReasonReact.reducerComponent("LocalToggle"); 116 | let make = _children => { 117 | ...component, 118 | initialState: () => false, 119 | reducer: (action, state) => 120 | switch (action) { 121 | | Toggle => Update(!state) 122 | }, 123 | render: ({state, send}) => 124 |
125 | 128 | (ReasonReact.string(" toggle:" ++ string_of_bool(state))) 129 |
, 130 | }; 131 | }; 132 | 133 | module LocalStateExample = { 134 | let component = ReasonReact.statelessComponent("LocalStateExample"); 135 | let make = _children => { 136 | ...component, 137 | render: _ => 138 |
139 | 140 | 141 | 142 | 143 |
, 144 | }; 145 | }; 146 | 147 | module TextInput = { 148 | type state = string; 149 | type action = 150 | | Text(string); 151 | let component = ReasonReact.reducerComponent("TextInput"); 152 | let textOfEvent = e => ReactEvent.Form.target(e)##value; 153 | let make = (~onChange=_ => (), ~showText=x => x, ~initial="", _children) => { 154 | ...component, 155 | initialState: () => initial, 156 | reducer: (action, _state) => 157 | switch (action) { 158 | | Text(text) => Update(text) 159 | }, 160 | render: ({state, send}) => 161 | { 165 | let text = textOfEvent(event); 166 | send(Text(text)); 167 | onChange(text); 168 | } 169 | ) 170 | />, 171 | }; 172 | }; 173 | 174 | module Spring = { 175 | type state = { 176 | animation: SpringAnimation.t, 177 | value: float, 178 | target: float, 179 | }; 180 | type action = 181 | | Click 182 | | Value(float); 183 | let component = ReasonReact.reducerComponent("Spring"); 184 | let make = (~renderValue, _children) => { 185 | ...component, 186 | initialState: () => { 187 | animation: SpringAnimation.create(0.0), 188 | value: 0.0, 189 | target: 1.0, 190 | }, 191 | didMount: ({state, send, onUnmount}) => { 192 | state.animation 193 | |> SpringAnimation.setOnChange( 194 | ~onChange=value => send(Value(value)), 195 | ~finalValue=state.target, 196 | ); 197 | onUnmount(() => SpringAnimation.stop(state.animation)); 198 | }, 199 | reducer: (action, state) => 200 | switch (action) { 201 | | Click => 202 | let target = state.target == 0.0 ? 1.0 : 0.0; 203 | UpdateWithSideEffects( 204 | {...state, target}, 205 | (_ => state.animation |> SpringAnimation.setFinalValue(target)), 206 | ); 207 | | Value(value) => Update({...state, value}) 208 | }, 209 | render: ({state, send}) => 210 |
211 | 214 |
(renderValue(state.value))
215 |
, 216 | }; 217 | }; 218 | 219 | module SimpleSpring = { 220 | let renderValue = value => 221 | ReasonReact.string(Printf.sprintf("value: %.3f", value)); 222 | let component = ReasonReact.statelessComponent("SimpleSpring"); 223 | let make = _children => {...component, render: _ => }; 224 | }; 225 | 226 | module AnimatedTextInput = { 227 | let shrinkText = (~text, ~value) => 228 | value >= 1.0 ? 229 | text : 230 | { 231 | let len = Js.Math.round(value *. float_of_int(String.length(text))); 232 | String.sub(text, 0, int_of_float(len)); 233 | }; 234 | let renderValue = value => 235 | shrinkText(~text, ~value)) 237 | initial="edit this or click target" 238 | />; 239 | let component = ReasonReact.statelessComponent("AnimatedTextInput"); 240 | let make = _children => {...component, render: _ => }; 241 | }; 242 | 243 | module TextInputRemote = { 244 | type state = string; 245 | type action = 246 | | Text(string) 247 | | Reset; 248 | let component = ReasonReact.reducerComponent("TextInputRemote"); 249 | let textOfEvent = e => ReactEvent.Form.target(e)##value; 250 | let make = 251 | ( 252 | ~remoteAction, 253 | ~onChange=_ => (), 254 | ~showText=x => x, 255 | ~initial="", 256 | _children, 257 | ) => { 258 | ...component, 259 | initialState: () => initial, 260 | didMount: ({send, onUnmount}) => { 261 | let token = RemoteAction.subscribe(~send, remoteAction); 262 | let cleanup = () => 263 | switch (token) { 264 | | Some(token) => RemoteAction.unsubscribe(token) 265 | | None => () 266 | }; 267 | onUnmount(cleanup); 268 | }, 269 | reducer: (action, _state) => 270 | switch (action) { 271 | | Text(text) => Update(text) 272 | | Reset => Update("the text has been reset") 273 | }, 274 | render: ({state, send}) => 275 | { 279 | let text = textOfEvent(event); 280 | send(Text(text)); 281 | onChange(text); 282 | } 283 | ) 284 | />, 285 | }; 286 | }; 287 | 288 | module AnimatedTextInputRemote = { 289 | let shrinkText = (~text, ~value) => 290 | value >= 1.0 ? 291 | text : 292 | { 293 | let len = Js.Math.round(value *. float_of_int(String.length(text))); 294 | String.sub(text, 0, int_of_float(len)); 295 | }; 296 | let remoteAction = RemoteAction.create(); 297 | let renderValue = value => 298 | shrinkText(~text, ~value)) 301 | initial="edit this or click target" 302 | />; 303 | let component = ReasonReact.statelessComponent("AnimatedTextInput"); 304 | let make = _children => { 305 | ...component, 306 | render: _ => 307 |
308 | 315 |
(ReasonReact.string("-----"))
316 | 317 |
, 318 | }; 319 | }; 320 | 321 | module GrandChild = { 322 | type action = 323 | | Incr 324 | | Decr; 325 | let component = ReasonReact.reducerComponent("GrandChild"); 326 | let make = (~remoteAction, _) => { 327 | ...component, 328 | initialState: () => 0, 329 | didMount: ({send, onUnmount}) => { 330 | let token = RemoteAction.subscribe(~send, remoteAction); 331 | let cleanup = () => 332 | switch (token) { 333 | | Some(token) => RemoteAction.unsubscribe(token) 334 | | None => () 335 | }; 336 | onUnmount(cleanup); 337 | }, 338 | reducer: (action, state) => 339 | switch (action) { 340 | | Incr => Update(state + 1) 341 | | Decr => Update(state - 1) 342 | }, 343 | render: ({state}) => 344 |
345 | (ReasonReact.string("in grandchild state: " ++ string_of_int(state))) 346 |
, 347 | }; 348 | }; 349 | 350 | module Child = { 351 | let component = ReasonReact.statelessComponent("Child"); 352 | let make = (~remoteAction, _) => { 353 | ...component, 354 | render: _ => 355 |
356 | (ReasonReact.string("in child")) 357 | 358 |
, 359 | }; 360 | }; 361 | 362 | module Parent = { 363 | let component = ReasonReact.reducerComponent("Parent"); 364 | let make = _ => { 365 | ...component, 366 | initialState: () => RemoteAction.create(), 367 | reducer: ((), _) => NoUpdate, 368 | render: ({state}) => 369 |
370 | 374 | 375 |
, 376 | }; 377 | }; 378 | -------------------------------------------------------------------------------- /src/animation/Reanimate.re: -------------------------------------------------------------------------------- 1 | let pxI = i => string_of_int(i) ++ "px"; 2 | 3 | let pxF = v => pxI(int_of_float(v)); 4 | 5 | module Key = { 6 | let counter = ref(0); 7 | let gen = () => { 8 | incr(counter); 9 | string_of_int(counter^); 10 | }; 11 | }; 12 | 13 | module ImageTransition: { 14 | /*** 15 | * Render function for a transition between two images. 16 | * phase is a value between 0.0 (first image) and 1.0 (second image). 17 | **/ 18 | let render: (~phase: float, int, int) => ReasonReact.reactElement; 19 | let displayHeight: int; 20 | } = { 21 | let numImages = 6; 22 | let displayHeight = 200; 23 | let displayHeightString = pxI(displayHeight); 24 | let sizes = [| 25 | (500, 350), 26 | (800, 600), 27 | (800, 400), 28 | (700, 500), 29 | (200, 650), 30 | (600, 600), 31 | |]; 32 | let displayWidths = 33 | Belt.Array.map(sizes, ((w, h)) => w * displayHeight / h); 34 | let getWidth = i => displayWidths[((i + numImages) mod numImages)]; 35 | 36 | /*** 37 | * Interpolate width and left for 2 images, phase is between 0.0 and 1.0. 38 | **/ 39 | let interpolate = (~width1, ~width2, phase) => { 40 | let width1 = float_of_int(width1); 41 | let width2 = float_of_int(width2); 42 | let width = width1 *. (1. -. phase) +. width2 *. phase; 43 | let left1 = -. (width1 *. phase); 44 | let left2 = left1 +. width1; 45 | (pxF(width), pxF(left1), pxF(left2)); 46 | }; 47 | let renderImage = (~left, i) => 48 | ; 53 | let render = (~phase, image1, image2) => { 54 | let width1 = getWidth(image1); 55 | let width2 = getWidth(image2); 56 | let (width, left1, left2) = interpolate(~width1, ~width2, phase); 57 |
58 |
61 | (renderImage(~left=left1, image1)) 62 | (renderImage(~left=left2, image2)) 63 |
64 |
; 65 | }; 66 | }; 67 | 68 | module ImageGalleryAnimation = { 69 | type action = 70 | | Click 71 | | SetCursor(float); 72 | type state = { 73 | animation: SpringAnimation.t, 74 | /* cursor value 3.5 means half way between image 3 and image 4 */ 75 | cursor: float, 76 | targetImage: int, 77 | }; 78 | let component = ReasonReact.reducerComponent("ImagesExample"); 79 | let make = (~initialImage=0, ~animateMount=true, _children) => { 80 | ...component, 81 | initialState: () => { 82 | animation: SpringAnimation.create(float_of_int(initialImage)), 83 | cursor: float_of_int(initialImage), 84 | targetImage: initialImage, 85 | }, 86 | didMount: ({state: {animation}, send}) => { 87 | animation 88 | |> SpringAnimation.setOnChange(~precision=0.05, ~onChange=cursor => 89 | send(SetCursor(cursor)) 90 | ); 91 | if (animateMount) { 92 | send(Click); 93 | }; 94 | }, 95 | willUnmount: ({state: {animation}}) => SpringAnimation.stop(animation), 96 | reducer: (action, state) => 97 | switch (action) { 98 | | Click => 99 | UpdateWithSideEffects( 100 | {...state, targetImage: state.targetImage + 1}, 101 | ( 102 | ({state: {animation, targetImage}}) => 103 | animation 104 | |> SpringAnimation.setFinalValue(float_of_int(targetImage)) 105 | ), 106 | ) 107 | | SetCursor(cursor) => Update({...state, cursor}) 108 | }, 109 | render: ({state: {cursor}, send}) => { 110 | let image = int_of_float(cursor); 111 | let phase = cursor -. float_of_int(image); 112 |
send(Click))> 113 | (ImageTransition.render(~phase, image, image + 1)) 114 |
; 115 | }, 116 | }; 117 | }; 118 | 119 | module AnimatedButton = { 120 | module Text = { 121 | let component = ReasonReact.statelessComponent("Text"); 122 | let make = (~text, _children) => { 123 | ...component, 124 | render: _ => , 125 | }; 126 | }; 127 | type size = 128 | | Small 129 | | Large; 130 | let targetHeight = 30.; 131 | let closeWidth = 50.; 132 | let smallWidth = 250.; 133 | let largeWidth = 450.; 134 | type state = { 135 | animation: SpringAnimation.t, 136 | width: int, 137 | size, 138 | clickCount: int, 139 | actionCount: int, 140 | }; 141 | type action = 142 | | Click 143 | | Reset 144 | | Unclick 145 | /* Width action triggered during animation. */ 146 | | Width(int) 147 | /* Toggle the size between small and large, and animate the width. */ 148 | | ToggleSize 149 | /* Close the button by animating the width to shrink. */ 150 | | Close; 151 | let component = ReasonReact.reducerComponent("ButtonAnimation"); 152 | let make = 153 | (~text="Button", ~rAction, ~animateMount=true, ~onClose=?, _children) => { 154 | ...component, 155 | initialState: () => { 156 | animation: SpringAnimation.create(smallWidth), 157 | width: int_of_float(smallWidth), 158 | size: Small, 159 | clickCount: 0, 160 | actionCount: 0, 161 | }, 162 | didMount: ({send}) => { 163 | RemoteAction.subscribe(~send, rAction) |> ignore; 164 | if (animateMount) { 165 | send(ToggleSize); 166 | }; 167 | }, 168 | willUnmount: ({state: {animation}}) => SpringAnimation.stop(animation), 169 | reducer: (action, state) => 170 | switch (action) { 171 | | Click => 172 | UpdateWithSideEffects( 173 | { 174 | ...state, 175 | clickCount: state.clickCount + 1, 176 | actionCount: state.actionCount + 1, 177 | }, 178 | (({send}) => send(ToggleSize)), 179 | ) 180 | | Reset => 181 | Update({...state, clickCount: 0, actionCount: state.actionCount + 1}) 182 | | Unclick => 183 | Update({ 184 | ...state, 185 | clickCount: state.clickCount - 1, 186 | actionCount: state.actionCount + 1, 187 | }) 188 | | Width(width) => Update({...state, width}) 189 | | ToggleSize => 190 | UpdateWithSideEffects( 191 | {...state, size: state.size === Small ? Large : Small}, 192 | ( 193 | ({state: {animation, size}, send}) => 194 | animation 195 | |> SpringAnimation.setOnChange( 196 | ~finalValue=size === Small ? smallWidth : largeWidth, 197 | ~precision=10., 198 | ~onChange=w => 199 | send(Width(int_of_float(w))) 200 | ) 201 | ), 202 | ) 203 | | Close => 204 | SideEffects( 205 | ( 206 | ({state: {animation}, send}) => 207 | animation 208 | |> SpringAnimation.setOnChange( 209 | ~finalValue=closeWidth, 210 | ~speedup=0.3, 211 | ~precision=10., 212 | ~onStop=onClose, 213 | ~onChange=w => 214 | send(Width(int_of_float(w))) 215 | ) 216 | ), 217 | ) 218 | }, 219 | render: ({state: {width} as state, send}) => { 220 | let buttonLabel = state => 221 | text 222 | ++ " clicks:" 223 | ++ string_of_int(state.clickCount) 224 | ++ " actions:" 225 | ++ string_of_int(state.actionCount); 226 |
send(Click)) 229 | style=(ReactDOMRe.Style.make(~width=pxI(width), ()))> 230 | 231 |
; 232 | }, 233 | }; 234 | }; 235 | 236 | module AnimateHeight = { 237 | /* When the closing animation begins */ 238 | type onBeginClosing = Animation.onStop; 239 | type action = 240 | | Open(Animation.onStop) 241 | | BeginClosing(onBeginClosing, Animation.onStop) 242 | | Close(Animation.onStop) 243 | | Animate(float, Animation.onStop) 244 | | Height(float); 245 | type state = { 246 | height: float, 247 | animation: SpringAnimation.t, 248 | }; 249 | let component = ReasonReact.reducerComponent("HeightAnim"); 250 | let make = (~rAction, ~targetHeight, children) => { 251 | ...component, 252 | initialState: () => {height: 0., animation: SpringAnimation.create(0.)}, 253 | didMount: ({send}) => { 254 | RemoteAction.subscribe(~send, rAction) |> ignore; 255 | send(Animate(targetHeight, None)); 256 | }, 257 | reducer: (action, state) => 258 | switch (action) { 259 | | Height(v) => Update({...state, height: v}) 260 | | Animate(finalValue, onStop) => 261 | SideEffects( 262 | ( 263 | ({send}) => 264 | state.animation 265 | |> SpringAnimation.setOnChange( 266 | ~finalValue, ~precision=10., ~onStop, ~onChange=h => 267 | send(Height(h)) 268 | ) 269 | ), 270 | ) 271 | | Close(onClose) => 272 | SideEffects((({send}) => send(Animate(0., onClose)))) 273 | | BeginClosing(onBeginClosing, onClose) => 274 | SideEffects( 275 | ( 276 | ({send}) => { 277 | switch (onBeginClosing) { 278 | | None => () 279 | | Some(f) => f() 280 | }; 281 | send(Animate(0., onClose)); 282 | } 283 | ), 284 | ) 285 | | Open(onOpen) => 286 | SideEffects((({send}) => send(Animate(targetHeight, onOpen)))) 287 | }, 288 | willUnmount: ({state}) => SpringAnimation.stop(state.animation), 289 | render: ({state}) => 290 |
298 | (ReasonReact.array(children)) 299 |
, 300 | }; 301 | }; 302 | 303 | module ReducerAnimationExample = { 304 | type action = 305 | | SetAct(action => unit) 306 | | AddSelf 307 | | AddButton(bool) 308 | | AddButtonFirst(bool) 309 | | AddImage(bool) 310 | | DecrementAllButtons 311 | /* Remove from the list the button uniquely identified by its height RemoteAction */ 312 | | FilterOutItem(RemoteAction.t(AnimateHeight.action)) 313 | | IncrementAllButtons 314 | | CloseAllButtons 315 | | RemoveItem 316 | | ResetAllButtons 317 | | ReverseItemsAnimation 318 | | CloseHeight(Animation.onStop) /* Used by ReverseAnim */ 319 | | ReverseWithSideEffects(unit => unit) /* Used by ReverseAnim */ 320 | | OpenHeight(Animation.onStop) /* Used by ReverseAnim */ 321 | | ToggleRandomAnimation; 322 | type item = { 323 | element: ReasonReact.reactElement, 324 | rActionButton: RemoteAction.t(AnimatedButton.action), 325 | rActionHeight: RemoteAction.t(AnimateHeight.action), 326 | /* used while removing items, to find the first item not already closing */ 327 | mutable closing: bool, 328 | }; 329 | module State: { 330 | type t = { 331 | act: action => unit, 332 | randomAnimation: Animation.t, 333 | items: list(item), 334 | }; 335 | let createButton: 336 | ( 337 | ~removeFromList: RemoteAction.t(AnimateHeight.action) => unit, 338 | ~animateMount: bool=?, 339 | int 340 | ) => 341 | item; 342 | let createImage: (~animateMount: bool=?, int) => item; 343 | let getElements: t => array(ReasonReact.reactElement); 344 | let initial: unit => t; 345 | } = { 346 | type t = { 347 | act: action => unit, 348 | randomAnimation: Animation.t, 349 | items: list(item), 350 | }; 351 | let initial = () => { 352 | act: _action => (), 353 | randomAnimation: Animation.create(), 354 | items: [], 355 | }; 356 | let getElements = ({items}) => 357 | Belt.List.toArray(Belt.List.mapReverse(items, x => x.element)); 358 | let createButton = (~removeFromList, ~animateMount=?, number) => { 359 | let rActionButton = RemoteAction.create(); 360 | let rActionHeight = RemoteAction.create(); 361 | let key = Key.gen(); 362 | let onClose = () => 363 | RemoteAction.send( 364 | rActionHeight, 365 | ~action= 366 | AnimateHeight.Close(Some(() => removeFromList(rActionHeight))), 367 | ); 368 | let element: ReasonReact.reactElement = 369 | 371 | 378 | ; 379 | {element, rActionButton, rActionHeight, closing: false}; 380 | }; 381 | let createImage = (~animateMount=?, number) => { 382 | let key = Key.gen(); 383 | let rActionButton = RemoteAction.create(); 384 | let imageGalleryAnimation = 385 | ; 390 | let rActionHeight = RemoteAction.create(); 391 | let element = 392 | 396 | imageGalleryAnimation 397 | ; 398 | {element, rActionButton, rActionHeight, closing: false}; 399 | }; 400 | }; 401 | let runAll = action => { 402 | let performSideEffects = ({ReasonReact.state: {State.items}}) => 403 | Belt.List.forEach(items, ({rActionButton}) => 404 | RemoteAction.send(rActionButton, ~action) 405 | ); 406 | ReasonReact.SideEffects(performSideEffects); 407 | }; 408 | let component = ReasonReact.reducerComponent("ReducerAnimationExample"); 409 | let rec make = (~showAllButtons, _children) => { 410 | ...component, 411 | initialState: () => State.initial(), 412 | didMount: ({state: {State.randomAnimation: animation}, send}) => { 413 | let callback = 414 | (.) => { 415 | let randomAction = 416 | switch (Random.int(6)) { 417 | | 0 => AddButton(true) 418 | | 1 => AddImage(true) 419 | | 2 => RemoveItem 420 | | 3 => RemoveItem 421 | | 4 => DecrementAllButtons 422 | | 5 => IncrementAllButtons 423 | | _ => assert(false) 424 | }; 425 | send(randomAction); 426 | Animation.Continue; 427 | }; 428 | send(SetAct(send)); 429 | Animation.setCallback(animation, ~callback); 430 | }, 431 | willUnmount: ({state: {randomAnimation}}) => 432 | Animation.stop(randomAnimation), 433 | reducer: (action, {act, items, randomAnimation} as state) => 434 | switch (action) { 435 | | SetAct(act) => Update({...state, act}) 436 | | AddSelf => 437 | module Self = { 438 | let make = make(~showAllButtons); 439 | }; 440 | let key = Key.gen(); 441 | let rActionButton = RemoteAction.create(); 442 | let rActionHeight = RemoteAction.create(); 443 | let element = 444 | 445 | 446 | ; 447 | let item = {element, rActionButton, rActionHeight, closing: false}; 448 | Update({...state, items: [item, ...items]}); 449 | | AddButton(animateMount) => 450 | let removeFromList = rActionHeight => 451 | act(FilterOutItem(rActionHeight)); 452 | Update({ 453 | ...state, 454 | items: [ 455 | State.createButton( 456 | ~removeFromList, 457 | ~animateMount, 458 | Belt.List.length(items), 459 | ), 460 | ...items, 461 | ], 462 | }); 463 | | AddButtonFirst(animateMount) => 464 | let removeFromList = rActionHeight => 465 | act(FilterOutItem(rActionHeight)); 466 | Update({ 467 | ...state, 468 | items: 469 | items 470 | @ [ 471 | State.createButton( 472 | ~removeFromList, 473 | ~animateMount, 474 | Belt.List.length(items), 475 | ), 476 | ], 477 | }); 478 | | AddImage(animateMount) => 479 | Update({ 480 | ...state, 481 | items: [ 482 | State.createImage(~animateMount, Belt.List.length(items)), 483 | ...items, 484 | ], 485 | }) 486 | | FilterOutItem(rAction) => 487 | let filter = item => item.rActionHeight !== rAction; 488 | Update({...state, items: Belt.List.keep(items, filter)}); 489 | | DecrementAllButtons => runAll(Unclick) 490 | | IncrementAllButtons => runAll(Click) 491 | | CloseAllButtons => runAll(Close) 492 | | RemoveItem => 493 | switch (Belt.List.getBy(items, item => item.closing === false)) { 494 | | Some(firstItemNotClosing) => 495 | let onBeginClosing = 496 | Some((() => firstItemNotClosing.closing = true)); 497 | let onClose = 498 | Some( 499 | (() => act(FilterOutItem(firstItemNotClosing.rActionHeight))), 500 | ); 501 | SideEffects( 502 | ( 503 | _ => 504 | RemoteAction.send( 505 | firstItemNotClosing.rActionHeight, 506 | ~action=BeginClosing(onBeginClosing, onClose), 507 | ) 508 | ), 509 | ); 510 | | None => NoUpdate 511 | } 512 | | ResetAllButtons => runAll(Reset) 513 | | CloseHeight(onStop) => 514 | let len = Belt.List.length(items); 515 | let count = ref(len); 516 | let onClose = () => { 517 | decr(count); 518 | if (count^ === 0) { 519 | switch (onStop) { 520 | | None => () 521 | | Some(f) => f() 522 | }; 523 | }; 524 | }; 525 | let iter = _ => 526 | Belt.List.forEach(items, item => 527 | RemoteAction.send( 528 | item.rActionHeight, 529 | ~action=Close(Some(onClose)), 530 | ) 531 | ); 532 | SideEffects(iter); 533 | | OpenHeight(onStop) => 534 | let len = Belt.List.length(items); 535 | let count = ref(len); 536 | let onClose = () => { 537 | decr(count); 538 | if (count^ === 0) { 539 | switch (onStop) { 540 | | None => () 541 | | Some(f) => f() 542 | }; 543 | }; 544 | }; 545 | let iter = _ => 546 | Belt.List.forEach(items, item => 547 | RemoteAction.send( 548 | item.rActionHeight, 549 | ~action=Open(Some(onClose)), 550 | ) 551 | ); 552 | SideEffects(iter); 553 | | ReverseWithSideEffects(performSideEffects) => 554 | UpdateWithSideEffects( 555 | {...state, items: Belt.List.reverse(items)}, 556 | (_ => performSideEffects()), 557 | ) 558 | | ReverseItemsAnimation => 559 | let onStopClose = () => 560 | act(ReverseWithSideEffects(() => act(OpenHeight(None)))); 561 | SideEffects((_ => act(CloseHeight(Some(onStopClose))))); 562 | | ToggleRandomAnimation => 563 | SideEffects( 564 | ( 565 | _ => 566 | Animation.isActive(randomAnimation) ? 567 | Animation.stop(randomAnimation) : 568 | Animation.start(randomAnimation) 569 | ), 570 | ) 571 | }, 572 | render: ({state}) => { 573 | let button = (~repeat=1, ~hide=false, txt, action) => 574 | hide ? 575 | ReasonReact.null : 576 |
581 | for (_ in 1 to repeat) { 582 | state.act(action); 583 | } 584 | )> 585 | (ReasonReact.string(txt)) 586 |
; 587 | let hide = !showAllButtons; 588 |
589 |
590 | (ReasonReact.string("Control:")) 591 | (button("Add Button", AddButton(true))) 592 | (button("Add Image", AddImage(true))) 593 | (button("Add Button On Top", AddButtonFirst(true))) 594 | (button("Remove Item", RemoveItem)) 595 | ( 596 | button( 597 | ~hide, 598 | ~repeat=100, 599 | "Add 100 Buttons On Top", 600 | AddButtonFirst(false), 601 | ) 602 | ) 603 | (button(~hide, ~repeat=100, "Add 100 Images", AddImage(false))) 604 | (button("Click all the Buttons", IncrementAllButtons)) 605 | (button(~hide, "Unclick all the Buttons", DecrementAllButtons)) 606 | (button("Close all the Buttons", CloseAllButtons)) 607 | ( 608 | button( 609 | ~hide, 610 | ~repeat=10, 611 | "Click all the Buttons 10 times", 612 | IncrementAllButtons, 613 | ) 614 | ) 615 | (button(~hide, "Reset all the Buttons' states", ResetAllButtons)) 616 | (button("Reverse Items", ReverseItemsAnimation)) 617 | ( 618 | button( 619 | "Random Animation " 620 | ++ (Animation.isActive(state.randomAnimation) ? "ON" : "OFF"), 621 | ToggleRandomAnimation, 622 | ) 623 | ) 624 | (button("Add Self", AddSelf)) 625 |
626 |
629 |
630 | ( 631 | ReasonReact.string( 632 | "Items:" ++ string_of_int(Belt.List.length(state.items)), 633 | ) 634 | ) 635 |
636 | (ReasonReact.array(State.getElements(state))) 637 |
638 |
; 639 | }, 640 | }; 641 | }; 642 | 643 | module ChatHead = { 644 | type action = 645 | | MoveX(float) 646 | | MoveY(float); 647 | type state = { 648 | x: float, 649 | y: float, 650 | }; 651 | let component = ReasonReact.reducerComponent("ChatHead"); 652 | let make = (~rAction, ~headNum, ~imageGallery, _children) => { 653 | ...component, 654 | initialState: () => {x: 0., y: 0.}, 655 | didMount: ({send}) => RemoteAction.subscribe(~send, rAction) |> ignore, 656 | reducer: (action, state: state) => 657 | switch (action) { 658 | | MoveX(x) => Update({...state, x}) 659 | | MoveY(y) => Update({...state, y}) 660 | }, 661 | render: ({state: {x, y}}) => { 662 | let left = pxF(x -. 25.); 663 | let top = pxF(y -. 25.); 664 | imageGallery ? 665 |
675 | 676 |
: 677 |
; 688 | }, 689 | }; 690 | }; 691 | 692 | module ChatHeadsExample = { 693 | [@bs.val] 694 | external addEventListener: (string, Js.t({..}) => unit) => unit = 695 | "window.addEventListener"; 696 | let numHeads = 6; 697 | type control = { 698 | rAction: RemoteAction.t(ChatHead.action), 699 | animX: SpringAnimation.t, 700 | animY: SpringAnimation.t, 701 | }; 702 | type state = { 703 | controls: array(control), 704 | chatHeads: array(ReasonReact.reactElement), 705 | }; 706 | let createControl = () => { 707 | rAction: RemoteAction.create(), 708 | animX: SpringAnimation.create(0.), 709 | animY: SpringAnimation.create(0.), 710 | }; 711 | let component = ReasonReact.reducerComponent("ChatHeadsExample"); 712 | let make = (~imageGallery, _children) => { 713 | ...component, 714 | initialState: () => { 715 | let controls = Belt.Array.makeBy(numHeads, _ => createControl()); 716 | let chatHeads = 717 | Belt.Array.makeBy(numHeads, i => 718 | 724 | ); 725 | {controls, chatHeads}; 726 | }, 727 | didMount: ({state: {controls}}) => { 728 | let setupAnimation = headNum => { 729 | let setOnChange = (~isX, afterChange) => { 730 | let control = controls[headNum]; 731 | let animation = isX ? control.animX : control.animY; 732 | animation 733 | |> SpringAnimation.setOnChange( 734 | ~preset=Spring.gentle, 735 | ~speedup=2., 736 | ~onChange=v => { 737 | RemoteAction.send( 738 | control.rAction, 739 | ~action=isX ? MoveX(v) : MoveY(v), 740 | ); 741 | afterChange(v); 742 | }, 743 | ); 744 | }; 745 | let isLastHead = headNum == numHeads - 1; 746 | let afterChangeX = x => 747 | isLastHead ? 748 | () : 749 | controls[(headNum + 1)].animX |> SpringAnimation.setFinalValue(x); 750 | let afterChangeY = y => 751 | isLastHead ? 752 | () : 753 | controls[(headNum + 1)].animY |> SpringAnimation.setFinalValue(y); 754 | setOnChange(~isX=true, afterChangeX); 755 | setOnChange(~isX=false, afterChangeY); 756 | }; 757 | Belt.Array.forEachWithIndex(controls, (i, _) => setupAnimation(i)); 758 | let onMove = e => { 759 | let x = e##pageX; 760 | let y = e##pageY; 761 | controls[0].animX |> SpringAnimation.setFinalValue(x); 762 | controls[0].animY |> SpringAnimation.setFinalValue(y); 763 | }; 764 | addEventListener("mousemove", onMove); 765 | addEventListener("touchmove", onMove); 766 | }, 767 | willUnmount: ({state: {controls}}) => 768 | Belt.Array.forEach( 769 | controls, 770 | ({animX, animY}) => { 771 | SpringAnimation.stop(animX); 772 | SpringAnimation.stop(animY); 773 | }, 774 | ), 775 | reducer: ((), _) => NoUpdate, 776 | render: ({state: {chatHeads}}) => 777 |
(ReasonReact.array(chatHeads))
, 778 | }; 779 | }; 780 | 781 | module ChatHeadsExampleStarter = { 782 | type state = 783 | | StartMessage 784 | | ChatHeads 785 | | ImageGalleryHeads; 786 | let component = ReasonReact.reducerComponent("ChatHeadsExampleStarter"); 787 | let make = _children => { 788 | ...component, 789 | initialState: () => StartMessage, 790 | reducer: (actionIsState, _) => Update(actionIsState), 791 | render: ({state, send}) => 792 | switch (state) { 793 | | StartMessage => 794 |
795 |
796 | 799 |
800 | 803 |
804 | | ChatHeads => 805 | | ImageGalleryHeads => 806 | }, 807 | }; 808 | }; 809 | 810 | module GalleryItem = { 811 | let component = ReasonReact.statelessComponent("GalleryItem"); 812 | let make = (~title="Untitled", ~description="no description", child) => { 813 | let title =
(ReasonReact.string(title))
; 814 | let description = 815 |
; 819 | let leftRight = 820 |
821 |
child
822 |
; 823 | { 824 | ...component, 825 | render: _self => 826 |
title description leftRight
, 827 | }; 828 | }; 829 | }; 830 | 831 | module GalleryContainer = { 832 | let component = ReasonReact.statelessComponent("GalleryContainer"); 833 | let megaHeaderTitle = "Animating With Reason React Reducers"; 834 | let megaHeaderSubtext = {| 835 | Examples With Animations. 836 | |}; 837 | let megaHeaderSubtextDetails = {| 838 | Explore animation with ReasonReact and reducers. 839 | 840 | |}; 841 | let make = children => { 842 | ...component, 843 | render: _self => 844 |
847 |
848 | (ReasonReact.string(megaHeaderTitle)) 849 |
850 |
851 | (ReasonReact.string(megaHeaderSubtext)) 852 |
853 |
854 | (ReasonReact.string(megaHeaderSubtextDetails)) 855 |
856 | ( 857 | ReasonReact.array( 858 | Array.map(c =>
c
, children), 859 | ) 860 | ) 861 |
, 862 | }; 863 | }; 864 | 865 | module ComponentGallery = { 866 | let component = ReasonReact.statelessComponent("ComponentGallery"); 867 | let make = _children => { 868 | let globalStateExample = 869 | 870 | ... 871 | ; 872 | let localStateExample = 873 | 874 | ... 875 | ; 876 | let simpleTextInput = 877 | 879 | ... Js.log2("onChange:", text)) /> 880 | ; 881 | let simpleSpring = 882 | 884 | ... 885 | ; 886 | let animatedTextInput = 887 | 890 | ... 891 | ; 892 | let animatedTextInputRemote = 893 | 896 | ... 897 | ; 898 | let callActionsOnGrandChild = 899 | 901 | ... 902 | ; 903 | let chatHeads = 904 | 905 | ... 906 | ; 907 | let imageGallery = 908 | 911 | ... 912 | ; 913 | let reducerAnimation = 914 | 915 | ... 916 | ; 917 | { 918 | ...component, 919 | render: _self => 920 | 921 | globalStateExample 922 | localStateExample 923 | simpleTextInput 924 | simpleSpring 925 | animatedTextInput 926 | animatedTextInputRemote 927 | callActionsOnGrandChild 928 | chatHeads 929 | imageGallery 930 | reducerAnimation 931 | , 932 | }; 933 | }; 934 | }; 935 | -------------------------------------------------------------------------------- /src/animation/RemoteAction.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | 5 | function sendDefault() { 6 | return /* () */0; 7 | } 8 | 9 | function create() { 10 | return /* record */[/* send */sendDefault]; 11 | } 12 | 13 | function subscribe(send, x) { 14 | if (x[/* send */0] === sendDefault) { 15 | x[/* send */0] = send; 16 | return x; 17 | } 18 | 19 | } 20 | 21 | function unsubscribe(x) { 22 | x[/* send */0] = sendDefault; 23 | return /* () */0; 24 | } 25 | 26 | function send(x, action) { 27 | return Curry._1(x[/* send */0], action); 28 | } 29 | 30 | exports.create = create; 31 | exports.subscribe = subscribe; 32 | exports.unsubscribe = unsubscribe; 33 | exports.send = send; 34 | /* No side effect */ 35 | -------------------------------------------------------------------------------- /src/animation/RemoteAction.re: -------------------------------------------------------------------------------- 1 | type t('action) = {mutable send: 'action => unit}; 2 | 3 | type token('action) = t('action); 4 | 5 | let sendDefault = _action => (); 6 | 7 | let create = () => {send: sendDefault}; 8 | 9 | /*** 10 | * The return type of subscribe is constrained as a token 11 | * by the interface file. This means that only the caller of 12 | * a given RemoteAction has the ability to unsubscribe. 13 | */ 14 | let subscribe = (~send, x) => 15 | if (x.send === sendDefault) { 16 | x.send = send; 17 | Some(x); 18 | } else { 19 | None; 20 | }; 21 | 22 | let unsubscribe = x => x.send = sendDefault; 23 | 24 | let send = (x, ~action) => x.send(action); 25 | -------------------------------------------------------------------------------- /src/animation/RemoteAction.rei: -------------------------------------------------------------------------------- 1 | /*** 2 | * RemoteAction provides a way to send actions to a remote component. 3 | * The sender creates a fresh RemoteAction and passes it down. 4 | * The recepient component calls subscribe in the didMount method. 5 | * The caller can then send actions to the recipient components via send. 6 | */ 7 | type t('action); 8 | 9 | type token('action); 10 | 11 | 12 | /*** Create a new remote action, to which one component will subscribe. */ 13 | let create: unit => t('action); 14 | 15 | 16 | /*** 17 | * Subscribe to the remote action, via the component's `send` function. 18 | * Returns an unsubscribe token which can be used to end the connection 19 | * to this particular send function. Will only return a token if the remote 20 | * action passed does not already have an active subscription. 21 | */ 22 | let subscribe: 23 | (~send: 'action => unit, t('action)) => option(token('action)); 24 | 25 | 26 | /*** Unsubscribe from a subscription */ 27 | let unsubscribe: token('action) => unit; 28 | 29 | 30 | /*** Perform an action on the subscribed component. */ 31 | let send: (t('action), ~action: 'action) => unit; -------------------------------------------------------------------------------- /src/animation/Spring.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var defaultSecondsPerFrame = 1 / 60; 5 | 6 | var noWobble = /* record */[ 7 | /* stiffness */170, 8 | /* damping */26 9 | ]; 10 | 11 | function createState(value) { 12 | return /* record */[ 13 | /* value */value, 14 | /* velocity */0, 15 | /* finalValue */value 16 | ]; 17 | } 18 | 19 | function stepper($staropt$star, speedup, $staropt$star$1, $staropt$star$2, state) { 20 | var finalValue = state[/* finalValue */2]; 21 | var velocity = state[/* velocity */1]; 22 | var value = state[/* value */0]; 23 | var secondsPerFrame = $staropt$star !== undefined ? $staropt$star : defaultSecondsPerFrame; 24 | var precision = $staropt$star$1 !== undefined ? $staropt$star$1 : 0.01; 25 | var preset = $staropt$star$2 !== undefined ? $staropt$star$2 : noWobble; 26 | var secondsPerFrame$1 = speedup !== undefined ? secondsPerFrame * speedup : secondsPerFrame; 27 | var forceSpring = -preset[/* stiffness */0] * (value - finalValue); 28 | var forceDamper = -preset[/* damping */1] * velocity; 29 | var acceleration = forceSpring + forceDamper; 30 | var newVelocity = velocity + acceleration * secondsPerFrame$1; 31 | var newValue = value + newVelocity * secondsPerFrame$1; 32 | var match = Math.abs(newVelocity) < precision && Math.abs(newValue - finalValue) < precision; 33 | if (match) { 34 | return /* record */[ 35 | /* value */finalValue, 36 | /* velocity */0.0, 37 | /* finalValue */state[/* finalValue */2] 38 | ]; 39 | } else { 40 | return /* record */[ 41 | /* value */newValue, 42 | /* velocity */newVelocity, 43 | /* finalValue */state[/* finalValue */2] 44 | ]; 45 | } 46 | } 47 | 48 | function isFinished(param) { 49 | if (param[/* value */0] === param[/* finalValue */2]) { 50 | return param[/* velocity */1] === 0; 51 | } else { 52 | return false; 53 | } 54 | } 55 | 56 | function test() { 57 | var _state = /* record */[ 58 | /* value */0.0, 59 | /* velocity */0.0, 60 | /* finalValue */1.0 61 | ]; 62 | while(true) { 63 | var state = _state; 64 | console.log(state); 65 | if (isFinished(state)) { 66 | return 0; 67 | } else { 68 | _state = stepper(undefined, undefined, undefined, undefined, state); 69 | continue ; 70 | } 71 | }; 72 | } 73 | 74 | var defaultPrecision = 0.01; 75 | 76 | var gentle = /* record */[ 77 | /* stiffness */120, 78 | /* damping */14 79 | ]; 80 | 81 | var wobbly = /* record */[ 82 | /* stiffness */180, 83 | /* damping */12 84 | ]; 85 | 86 | var stiff = /* record */[ 87 | /* stiffness */210, 88 | /* damping */20 89 | ]; 90 | 91 | var defaultPreset = noWobble; 92 | 93 | exports.defaultSecondsPerFrame = defaultSecondsPerFrame; 94 | exports.defaultPrecision = defaultPrecision; 95 | exports.noWobble = noWobble; 96 | exports.gentle = gentle; 97 | exports.wobbly = wobbly; 98 | exports.stiff = stiff; 99 | exports.defaultPreset = defaultPreset; 100 | exports.createState = createState; 101 | exports.stepper = stepper; 102 | exports.isFinished = isFinished; 103 | exports.test = test; 104 | /* No side effect */ 105 | -------------------------------------------------------------------------------- /src/animation/Spring.re: -------------------------------------------------------------------------------- 1 | let defaultSecondsPerFrame = 1. /. 60.; 2 | 3 | let defaultPrecision = 0.01; 4 | 5 | type preset = { 6 | stiffness: float, 7 | damping: float, 8 | }; 9 | 10 | let noWobble = {stiffness: 170., damping: 26.}; 11 | 12 | let gentle = {stiffness: 120., damping: 14.}; 13 | 14 | let wobbly = {stiffness: 180., damping: 12.}; 15 | 16 | let stiff = {stiffness: 210., damping: 20.}; 17 | 18 | let defaultPreset = noWobble; 19 | 20 | type state = { 21 | value: float, 22 | velocity: float, 23 | finalValue: float, 24 | }; 25 | 26 | let createState = value => {value, velocity: 0., finalValue: value}; 27 | 28 | let stepper = 29 | ( 30 | ~secondsPerFrame=defaultSecondsPerFrame, 31 | ~speedup=?, 32 | ~precision=defaultPrecision, 33 | ~preset=defaultPreset, 34 | {value, velocity, finalValue} as state, 35 | ) => { 36 | let secondsPerFrame = 37 | switch (speedup) { 38 | | None => secondsPerFrame 39 | | Some(x) => secondsPerFrame *. x 40 | }; 41 | let forceSpring = -. preset.stiffness *. (value -. finalValue); 42 | let forceDamper = -. preset.damping *. velocity; 43 | let acceleration = forceSpring +. forceDamper; 44 | let newVelocity = velocity +. acceleration *. secondsPerFrame; 45 | let newValue = value +. newVelocity *. secondsPerFrame; 46 | abs_float(newVelocity) < precision 47 | && abs_float(newValue -. finalValue) < precision ? 48 | {...state, value: finalValue, velocity: 0.0} : 49 | {...state, value: newValue, velocity: newVelocity}; 50 | }; 51 | 52 | let isFinished = ({value, velocity, finalValue}) => 53 | value == finalValue && velocity == 0.; 54 | 55 | let test = () => { 56 | let rec iterate = state => { 57 | Js.log(state); 58 | if (!isFinished(state)) { 59 | iterate(stepper(state)); 60 | }; 61 | }; 62 | iterate({value: 0.0, velocity: 0.0, finalValue: 1.0}); 63 | }; 64 | -------------------------------------------------------------------------------- /src/animation/SpringAnimation.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 5 | var Spring$ReasonReactExample = require("./Spring.bs.js"); 6 | var Animation$ReasonReactExample = require("./Animation.bs.js"); 7 | 8 | function create(initialValue) { 9 | var animation = Animation$ReasonReactExample.create(/* () */0); 10 | var state = Spring$ReasonReactExample.createState(initialValue); 11 | return /* record */[ 12 | /* animation */animation, 13 | /* state */state 14 | ]; 15 | } 16 | 17 | function setOnChange(preset, speedup, precision, $staropt$star, onChange, finalValue, a) { 18 | var onStop = $staropt$star !== undefined ? Js_primitive.valFromOption($staropt$star) : undefined; 19 | var callback = function () { 20 | a[/* state */1] = Spring$ReasonReactExample.stepper(undefined, speedup, precision, preset, a[/* state */1]); 21 | var isFinished = Spring$ReasonReactExample.isFinished(a[/* state */1]); 22 | Curry._1(onChange, a[/* state */1][/* value */0]); 23 | if (isFinished) { 24 | return /* Stop */[onStop]; 25 | } else { 26 | return /* Continue */0; 27 | } 28 | }; 29 | Animation$ReasonReactExample.stop(a[/* animation */0]); 30 | ((function (param) { 31 | return Animation$ReasonReactExample.setCallback(param, callback); 32 | })(a[/* animation */0])); 33 | if (finalValue !== undefined) { 34 | var init = a[/* state */1]; 35 | a[/* state */1] = /* record */[ 36 | /* value */init[/* value */0], 37 | /* velocity */init[/* velocity */1], 38 | /* finalValue */finalValue 39 | ]; 40 | return Animation$ReasonReactExample.start(a[/* animation */0]); 41 | } else { 42 | return /* () */0; 43 | } 44 | } 45 | 46 | function setFinalValue(finalValue, a) { 47 | Animation$ReasonReactExample.stop(a[/* animation */0]); 48 | var init = a[/* state */1]; 49 | a[/* state */1] = /* record */[ 50 | /* value */init[/* value */0], 51 | /* velocity */init[/* velocity */1], 52 | /* finalValue */finalValue 53 | ]; 54 | return Animation$ReasonReactExample.start(a[/* animation */0]); 55 | } 56 | 57 | function stop(a) { 58 | return Animation$ReasonReactExample.stop(a[/* animation */0]); 59 | } 60 | 61 | exports.create = create; 62 | exports.setOnChange = setOnChange; 63 | exports.setFinalValue = setFinalValue; 64 | exports.stop = stop; 65 | /* No side effect */ 66 | -------------------------------------------------------------------------------- /src/animation/SpringAnimation.re: -------------------------------------------------------------------------------- 1 | type t = { 2 | animation: Animation.t, 3 | mutable state: Spring.state, 4 | }; 5 | 6 | let create = initialValue => { 7 | let animation = Animation.create(); 8 | let state = Spring.createState(initialValue); 9 | {animation, state}; 10 | }; 11 | 12 | type onChange = float => unit; 13 | 14 | let setOnChange = 15 | ( 16 | ~preset=?, 17 | ~speedup=?, 18 | ~precision=?, 19 | ~onStop=None, 20 | ~onChange, 21 | ~finalValue=?, 22 | a, 23 | ) => { 24 | let callback = 25 | (.) => { 26 | a.state = Spring.stepper(~preset?, ~speedup?, ~precision?, a.state); 27 | let isFinished = Spring.isFinished(a.state); 28 | onChange(a.state.value); 29 | isFinished ? Animation.Stop(onStop) : Continue; 30 | }; 31 | a.animation |> Animation.stop; 32 | a.animation |> Animation.setCallback(~callback); 33 | switch (finalValue) { 34 | | None => () 35 | | Some(finalValue) => 36 | a.state = {...a.state, finalValue}; 37 | a.animation |> Animation.start; 38 | }; 39 | }; 40 | 41 | let setFinalValue = (finalValue, a) => { 42 | a.animation |> Animation.stop; 43 | a.state = {...a.state, finalValue}; 44 | a.animation |> Animation.start; 45 | }; 46 | 47 | let stop = a => a.animation |> Animation.stop; 48 | -------------------------------------------------------------------------------- /src/animation/SpringAnimation.rei: -------------------------------------------------------------------------------- 1 | type t; 2 | 3 | let create: float => t; 4 | 5 | type onChange = float => unit; 6 | 7 | /** 8 | * Set the onChange function and other parameters of a spring animation. 9 | * The animation is stopped, and only re-started if finalValue is supplied. 10 | */ 11 | let setOnChange: 12 | ( 13 | ~preset: Spring.preset=?, 14 | ~speedup: float=?, 15 | ~precision: float=?, 16 | ~onStop: Animation.onStop=?, 17 | ~onChange: onChange, 18 | ~finalValue: float=?, 19 | t 20 | ) => 21 | unit; 22 | 23 | /** 24 | * Update the final value of the animation, and start it if it was stopped. 25 | */ 26 | let setFinalValue: (float, t) => unit; 27 | 28 | let stop: t => unit; -------------------------------------------------------------------------------- /src/animation/head0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head0.jpg -------------------------------------------------------------------------------- /src/animation/head1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head1.jpg -------------------------------------------------------------------------------- /src/animation/head2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head2.jpg -------------------------------------------------------------------------------- /src/animation/head3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head3.jpg -------------------------------------------------------------------------------- /src/animation/head4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head4.jpg -------------------------------------------------------------------------------- /src/animation/head5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/animation/head5.jpg -------------------------------------------------------------------------------- /src/animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Animating With Reducers 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/animation/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | html, body { 5 | height: 100%; 6 | } 7 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | outline: 0; 12 | font-weight: inherit; 13 | font-style: inherit; 14 | font-family: inherit; 15 | font-size: 100%; 16 | vertical-align: baseline; 17 | } 18 | code, xmp, plaintext, listing { 19 | font-family: monospace; 20 | white-space: pre; 21 | margin: 1em 0px; 22 | line-height: normal; 23 | color: hsla(221, 0%, 31%, 1); 24 | background-color: hsla(221, 0%, 93%, 1); 25 | /* border: 1px solid hsla(221, 48%, 51%, 1); */ 26 | border-radius: 1px; 27 | padding-left: 4px; 28 | padding-right: 4px; 29 | } 30 | pre { 31 | display: block; 32 | font-family: monospace; 33 | white-space: pre; 34 | margin: 1em 0px; 35 | line-height: normal; 36 | } 37 | /* Color scheme for code */ 38 | .Function { color: #4078f2; } 39 | .Conditional { color: #a626a4; } 40 | .Macro { color: #a626a4; } 41 | .Keyword { color: #e45649; } 42 | .StorageClass { color: #c18401; } 43 | .Type { color: #c18401; } 44 | .Comment { color: #a0a1a7; font-style: italic; } 45 | .Constant { color: #50a14f; } 46 | .String { color: #50a14f; } 47 | .Number { color: #986801; } 48 | .Special { color: #4078f2; } 49 | code, pre { 50 | font-family: "Menlo"; 51 | font-size: 12px; 52 | } 53 | pre { 54 | color: #494b53; 55 | } 56 | 57 | body { 58 | -webkit-font-smoothing: antialiased; 59 | text-rendering: optimizeLegibility; 60 | color: #555; 61 | background-color: #fafafa; 62 | } 63 | body, td, textarea, input { 64 | /* overflow-x: hidden; */ 65 | font-family: Helvetica Neue, Open Sans, sans-serif; 66 | line-height: 1.6; 67 | font-size: 13px; 68 | color: #505050; 69 | } 70 | .componentBox { 71 | display: flex; 72 | -webkit-user-select: none; /* Chrome all / Safari all */ 73 | -moz-user-select: none; /* Firefox all */ 74 | -ms-user-select: none; /* IE 10+ */ 75 | user-select: none; /* Likely future */ 76 | border-top: 18px solid hsla(31, 0%, 80%, 0.2); 77 | text-shadow: 0px 1px rgba(250, 250, 250, 0.05); 78 | background-color:hsla(25, 0%, 86%, 0.20); 79 | color: hsla(25, 0%, 53%, 1); 80 | padding: 14px; 81 | } 82 | .componentColumn { 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: flex-start; 86 | align-items: center; 87 | 88 | } 89 | .componentBox pre { 90 | text-shadow: none; 91 | } 92 | .exampleButton { 93 | display: flex; 94 | flex-direction: column; 95 | justify-content: center; 96 | overflow: hidden; 97 | white-space: nowrap; 98 | cursor: pointer; 99 | -webkit-user-select: none; /* Chrome all / Safari all */ 100 | -moz-user-select: none; /* Firefox all */ 101 | -ms-user-select: none; /* IE 10+ */ 102 | user-select: none; /* Likely future */ 103 | background-color: rgba(0,0,0, 0.05); 104 | text-align: center; 105 | } 106 | .exampleButton:hover { 107 | background-color: rgba(0,0,0, 0.1); 108 | } 109 | .exampleButton:active { 110 | background-color: rgba(0,0,0, 0.12); 111 | } 112 | .exampleButton.small { 113 | font-size: 12px; 114 | } 115 | .exampleButton.medium { 116 | font-size: 13px; 117 | } 118 | .exampleButton.large { 119 | font-size: 15px; 120 | } 121 | 122 | .block { 123 | display: block; 124 | } 125 | .megaHeader { 126 | max-width: 600px; 127 | font-weight: bold; 128 | padding: 10px 0; 129 | text-transform: uppercase; 130 | font-size: 2.8em; 131 | letter-spacing: 1px; 132 | } 133 | .megaHeaderSubtext { 134 | max-width: 600px; 135 | font-size: 1.3em; 136 | font-family: Open Sans, sans-serif; 137 | font-weight: 300; 138 | margin-bottom: 40px; 139 | } 140 | .megaHeaderSubtextDetails { 141 | max-width: 600px; 142 | margin-bottom: 40px; 143 | } 144 | .header { 145 | max-width: 600px; 146 | border-left: 4px solid hsl(31, 0%, 79%); 147 | padding-left: 14px; 148 | font-weight: 500; 149 | text-transform: uppercase; 150 | padding-bottom: 20px; 151 | font-size: 1.4em; 152 | letter-spacing: 1px; 153 | } 154 | .headerSubtext { 155 | max-width: 600px; 156 | border-left: 4px solid hsl(31, 0%, 79%); 157 | padding-left: 14px; 158 | padding-bottom: 8px; 159 | margin-bottom: 30px; 160 | } 161 | .headerSubtext p { 162 | margin-bottom: 1em; 163 | } 164 | .mainGallery { 165 | padding: 50px; 166 | } 167 | .stateLogger { 168 | font-family: Monospace; 169 | font-family: "Menlo"; 170 | font-size: 12px; 171 | padding: 8px; 172 | margin: 8px; 173 | font-weight: bold; 174 | } 175 | .galleryItem { 176 | margin-bottom: 60px; 177 | } 178 | .galleryItemDemo { 179 | margin-bottom: 60px; 180 | margin-top: 14px; 181 | } 182 | .leftRightContainer { 183 | display: flex; 184 | flex-wrap: wrap; 185 | } 186 | .left { 187 | flex: 1; 188 | /* For when it breaks */ 189 | margin-bottom: 18px; 190 | } 191 | .right { 192 | flex: 1; 193 | margin-left: 18px; 194 | margin-bottom: 18px; 195 | } 196 | .sourceContainer { 197 | border-top: 18px solid hsla(31, 0%, 80%, 0.2); 198 | /* Header left border + padding-left */ 199 | margin-left: 18px; 200 | background-color:hsla(25, 0%, 86%, 0.20); 201 | padding: 14px; 202 | } 203 | .fileName { 204 | position: absolute; 205 | font-size: .9em; 206 | top: -32px; 207 | left: -8px; 208 | color: #bbb; 209 | } 210 | .interactionContainer { 211 | /* Header left border + padding-left */ 212 | margin-left: 18px; 213 | } 214 | .photo-outer { 215 | overflow: hidden; 216 | position: relative; 217 | margin: auto; 218 | } 219 | .photo-inner { 220 | position: absolute; 221 | } 222 | .chat-head { 223 | border-radius: 99px; 224 | background-color: white; 225 | width: 50px; 226 | height: 50px; 227 | border: 3px solid white; 228 | position: absolute; 229 | background-size: 50px; 230 | } 231 | .chat-head-image-gallery { 232 | position: absolute; 233 | transform: translate(-50px, -50px) scale(0.7) ; 234 | } 235 | 236 | .chat-head-0 { 237 | background-image: url(head0.jpg); 238 | } 239 | .chat-head-1 { 240 | background-image: url(head1.jpg); 241 | } 242 | .chat-head-2 { 243 | background-image: url(head2.jpg); 244 | } 245 | .chat-head-3 { 246 | background-image: url(head3.jpg); 247 | } 248 | .chat-head-4 { 249 | background-image: url(head4.jpg); 250 | } 251 | .chat-head-5 { 252 | background-image: url(head5.jpg); 253 | } 254 | 255 | -------------------------------------------------------------------------------- /src/async/Counter.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Block = require("bs-platform/lib/js/block.js"); 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var React = require("react"); 6 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 7 | 8 | var component = ReasonReact.reducerComponent("Counter"); 9 | 10 | function make() { 11 | return /* record */[ 12 | /* debugName */component[/* debugName */0], 13 | /* reactClassInternal */component[/* reactClassInternal */1], 14 | /* handedOffState */component[/* handedOffState */2], 15 | /* willReceiveProps */component[/* willReceiveProps */3], 16 | /* didMount */(function (self) { 17 | var intervalId = setInterval((function () { 18 | return Curry._1(self[/* send */3], /* Tick */0); 19 | }), 1000); 20 | return Curry._1(self[/* onUnmount */4], (function () { 21 | clearInterval(intervalId); 22 | return /* () */0; 23 | })); 24 | }), 25 | /* didUpdate */component[/* didUpdate */5], 26 | /* willUnmount */component[/* willUnmount */6], 27 | /* willUpdate */component[/* willUpdate */7], 28 | /* shouldUpdate */component[/* shouldUpdate */8], 29 | /* render */(function (param) { 30 | return React.createElement("div", undefined, String(param[/* state */1][/* count */0])); 31 | }), 32 | /* initialState */(function () { 33 | return /* record */[/* count */0]; 34 | }), 35 | /* retainedProps */component[/* retainedProps */11], 36 | /* reducer */(function (_, state) { 37 | return /* Update */Block.__(0, [/* record */[/* count */state[/* count */0] + 1 | 0]]); 38 | }), 39 | /* jsElementWrapped */component[/* jsElementWrapped */13] 40 | ]; 41 | } 42 | 43 | exports.component = component; 44 | exports.make = make; 45 | /* component Not a pure module */ 46 | -------------------------------------------------------------------------------- /src/async/Counter.re: -------------------------------------------------------------------------------- 1 | /* See https://reasonml.github.io/reason-react/docs/en/counter.html for another possible way of doing this */ 2 | /* This is a stateful component. In ReasonReact, we call them reducer components */ 3 | /* A list of state transitions, to be used in self.send and reducer */ 4 | type action = 5 | | Tick; 6 | 7 | /* The component's state type. It can be anything, including, commonly, being a record type */ 8 | type state = {count: int}; 9 | 10 | let component = ReasonReact.reducerComponent("Counter"); 11 | 12 | let make = _children => { 13 | ...component, 14 | initialState: () => {count: 0}, 15 | reducer: (action, state) => 16 | switch (action) { 17 | | Tick => ReasonReact.Update({count: state.count + 1}) 18 | }, 19 | didMount: self => { 20 | let intervalId = Js.Global.setInterval(() => self.send(Tick), 1000); 21 | self.onUnmount(() => Js.Global.clearInterval(intervalId)); 22 | }, 23 | render: ({state}) => 24 |
(ReasonReact.string(string_of_int(state.count)))
, 25 | }; 26 | -------------------------------------------------------------------------------- /src/async/CounterRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var Counter$ReasonReactExample = require("./Counter.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, Counter$ReasonReactExample.make(/* array */[])), "index"); 8 | 9 | /* Not a pure module */ 10 | -------------------------------------------------------------------------------- /src/async/CounterRoot.re: -------------------------------------------------------------------------------- 1 | ReactDOMRe.renderToElementWithId(, "index"); 2 | -------------------------------------------------------------------------------- /src/async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/fetch/FetchExample.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Block = require("bs-platform/lib/js/block.js"); 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var React = require("react"); 6 | var Belt_Array = require("bs-platform/lib/js/belt_Array.js"); 7 | var Json_decode = require("@glennsl/bs-json/src/Json_decode.bs.js"); 8 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 9 | 10 | function dogs(json) { 11 | var __x = Json_decode.field("message", (function (param) { 12 | return Json_decode.array(Json_decode.string, param); 13 | }), json); 14 | return Belt_Array.map(__x, (function (dog) { 15 | return dog; 16 | })); 17 | } 18 | 19 | var Decode = /* module */[/* dogs */dogs]; 20 | 21 | var component = ReasonReact.reducerComponent("FetchExample"); 22 | 23 | function make() { 24 | return /* record */[ 25 | /* debugName */component[/* debugName */0], 26 | /* reactClassInternal */component[/* reactClassInternal */1], 27 | /* handedOffState */component[/* handedOffState */2], 28 | /* willReceiveProps */component[/* willReceiveProps */3], 29 | /* didMount */(function (self) { 30 | return Curry._1(self[/* send */3], /* DogsFetch */0); 31 | }), 32 | /* didUpdate */component[/* didUpdate */5], 33 | /* willUnmount */component[/* willUnmount */6], 34 | /* willUpdate */component[/* willUpdate */7], 35 | /* shouldUpdate */component[/* shouldUpdate */8], 36 | /* render */(function (self) { 37 | var match = self[/* state */1]; 38 | if (typeof match === "number") { 39 | if (match !== 0) { 40 | return React.createElement("div", undefined, "An error occurred!"); 41 | } else { 42 | return React.createElement("div", undefined, "Loading..."); 43 | } 44 | } else { 45 | return React.createElement("div", undefined, React.createElement("h1", undefined, "Dogs"), React.createElement("p", undefined, "Source: "), React.createElement("a", { 46 | href: "https://dog.ceo" 47 | }, "https://dog.ceo"), React.createElement("ul", undefined, Belt_Array.map(match[0], (function (dog) { 48 | return React.createElement("li", { 49 | key: dog 50 | }, dog); 51 | })))); 52 | } 53 | }), 54 | /* initialState */(function () { 55 | return /* Loading */0; 56 | }), 57 | /* retainedProps */component[/* retainedProps */11], 58 | /* reducer */(function (action, _) { 59 | if (typeof action === "number") { 60 | if (action !== 0) { 61 | return /* Update */Block.__(0, [/* Error */1]); 62 | } else { 63 | return /* UpdateWithSideEffects */Block.__(2, [ 64 | /* Loading */0, 65 | (function (self) { 66 | fetch("https://dog.ceo/api/breeds/list").then((function (prim) { 67 | return prim.json(); 68 | })).then((function (json) { 69 | var dogs$1 = dogs(json); 70 | return Promise.resolve(Curry._1(self[/* send */3], /* DogsFetched */[dogs$1])); 71 | })).catch((function () { 72 | return Promise.resolve(Curry._1(self[/* send */3], /* DogsFailedToFetch */1)); 73 | })); 74 | return /* () */0; 75 | }) 76 | ]); 77 | } 78 | } else { 79 | return /* Update */Block.__(0, [/* Loaded */[action[0]]]); 80 | } 81 | }), 82 | /* jsElementWrapped */component[/* jsElementWrapped */13] 83 | ]; 84 | } 85 | 86 | exports.Decode = Decode; 87 | exports.component = component; 88 | exports.make = make; 89 | /* component Not a pure module */ 90 | -------------------------------------------------------------------------------- /src/fetch/FetchExample.re: -------------------------------------------------------------------------------- 1 | /* The new stdlib additions */ 2 | open Belt; 3 | 4 | type dog = string; 5 | 6 | type state = 7 | | Loading 8 | | Error 9 | | Loaded(array(dog)); 10 | 11 | type action = 12 | | DogsFetch 13 | | DogsFetched(array(dog)) 14 | | DogsFailedToFetch; 15 | 16 | module Decode = { 17 | let dogs = json: array(dog) => 18 | Json.Decode.( 19 | json |> field("message", array(string)) |> Array.map(_, dog => dog) 20 | ); 21 | }; 22 | 23 | let component = ReasonReact.reducerComponent("FetchExample"); 24 | 25 | let make = _children => { 26 | ...component, 27 | initialState: _state => Loading, 28 | reducer: (action, _state) => 29 | switch (action) { 30 | | DogsFetch => 31 | ReasonReact.UpdateWithSideEffects( 32 | Loading, 33 | ( 34 | self => 35 | Js.Promise.( 36 | Fetch.fetch("https://dog.ceo/api/breeds/list") 37 | |> then_(Fetch.Response.json) 38 | |> then_(json => 39 | json 40 | |> Decode.dogs 41 | |> (dogs => self.send(DogsFetched(dogs))) 42 | |> resolve 43 | ) 44 | |> catch(_err => 45 | Js.Promise.resolve(self.send(DogsFailedToFetch)) 46 | ) 47 | |> ignore 48 | ) 49 | ), 50 | ) 51 | | DogsFetched(dogs) => ReasonReact.Update(Loaded(dogs)) 52 | | DogsFailedToFetch => ReasonReact.Update(Error) 53 | }, 54 | didMount: self => self.send(DogsFetch), 55 | render: self => 56 | switch (self.state) { 57 | | Error =>
(ReasonReact.string("An error occurred!"))
58 | | Loading =>
(ReasonReact.string("Loading..."))
59 | | Loaded(dogs) => 60 |
61 |

(ReasonReact.string("Dogs"))

62 |

(ReasonReact.string("Source: "))

63 | 64 | (ReasonReact.string("https://dog.ceo")) 65 | 66 |
    67 | ( 68 | Array.map(dogs, dog => 69 |
  • (ReasonReact.string(dog))
  • 70 | ) 71 | |> ReasonReact.array 72 | ) 73 |
74 |
75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/fetch/FetchExampleRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var FetchExample$ReasonReactExample = require("./FetchExample.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, FetchExample$ReasonReactExample.make(/* array */[])), "index"); 8 | 9 | /* Not a pure module */ 10 | -------------------------------------------------------------------------------- /src/fetch/FetchExampleRoot.re: -------------------------------------------------------------------------------- 1 | ReactDOMRe.renderToElementWithId(, "index"); 2 | -------------------------------------------------------------------------------- /src/fetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fetch 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/hooks-animation/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/0.jpg -------------------------------------------------------------------------------- /src/hooks-animation/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/1.jpg -------------------------------------------------------------------------------- /src/hooks-animation/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/2.jpg -------------------------------------------------------------------------------- /src/hooks-animation/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/3.jpg -------------------------------------------------------------------------------- /src/hooks-animation/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/4.jpg -------------------------------------------------------------------------------- /src/hooks-animation/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/5.jpg -------------------------------------------------------------------------------- /src/hooks-animation/HooksAnimation.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 5 | 6 | function defaultCallback() { 7 | return /* Stop */[undefined]; 8 | } 9 | 10 | function create() { 11 | return /* record */[ 12 | /* id */undefined, 13 | /* callback */defaultCallback 14 | ]; 15 | } 16 | 17 | function onAnimationFrame(animation, _) { 18 | if (animation[/* id */0] !== undefined) { 19 | var match = animation[/* callback */1](); 20 | if (match) { 21 | var match$1 = match[0]; 22 | if (match$1 !== undefined) { 23 | animation[/* id */0] = undefined; 24 | return Curry._1(match$1, /* () */0); 25 | } else { 26 | animation[/* id */0] = undefined; 27 | return /* () */0; 28 | } 29 | } else { 30 | animation[/* id */0] = Js_primitive.some(requestAnimationFrame((function (param) { 31 | return onAnimationFrame(animation, param); 32 | }))); 33 | return /* () */0; 34 | } 35 | } else { 36 | return 0; 37 | } 38 | } 39 | 40 | function start(animation) { 41 | animation[/* id */0] = Js_primitive.some(requestAnimationFrame((function (param) { 42 | return onAnimationFrame(animation, param); 43 | }))); 44 | return /* () */0; 45 | } 46 | 47 | function stop(animation) { 48 | var match = animation[/* id */0]; 49 | if (match !== undefined) { 50 | cancelAnimationFrame(Js_primitive.valFromOption(match)); 51 | animation[/* id */0] = undefined; 52 | return /* () */0; 53 | } else { 54 | return /* () */0; 55 | } 56 | } 57 | 58 | function setCallback(animation, callback) { 59 | stop(animation); 60 | animation[/* callback */1] = callback; 61 | return /* () */0; 62 | } 63 | 64 | function isActive(animation) { 65 | return animation[/* id */0] !== undefined; 66 | } 67 | 68 | exports.create = create; 69 | exports.isActive = isActive; 70 | exports.setCallback = setCallback; 71 | exports.start = start; 72 | exports.stop = stop; 73 | /* No side effect */ 74 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksAnimation.re: -------------------------------------------------------------------------------- 1 | type animationFrameID; 2 | 3 | [@bs.val] 4 | external requestAnimationFrame: (unit => unit) => animationFrameID = ""; 5 | 6 | [@bs.val] external cancelAnimationFrame: animationFrameID => unit = ""; 7 | 8 | type onStop = option(unit => unit); 9 | 10 | type ctrl = 11 | | Stop(onStop) 12 | | Continue; 13 | 14 | type callback = (. unit) => ctrl; 15 | 16 | type t = { 17 | mutable id: option(animationFrameID), 18 | mutable callback, 19 | }; 20 | 21 | let defaultCallback = (.) => Stop(None); 22 | 23 | let create = () => {id: None, callback: defaultCallback}; 24 | 25 | let rec onAnimationFrame = (animation, ()) => 26 | if (animation.id != None) { 27 | switch (animation.callback(.)) { 28 | | Stop(None) => animation.id = None 29 | | Stop(Some(onStop)) => 30 | animation.id = None; 31 | onStop(); 32 | | Continue => 33 | animation.id = 34 | Some(requestAnimationFrame(onAnimationFrame(animation))) 35 | }; 36 | }; 37 | 38 | let start = animation => 39 | animation.id = Some(requestAnimationFrame(onAnimationFrame(animation))); 40 | 41 | let stop = animation => 42 | switch (animation.id) { 43 | | Some(id) => 44 | cancelAnimationFrame(id); 45 | animation.id = None; 46 | | None => () 47 | }; 48 | 49 | let setCallback = (animation, ~callback) => { 50 | stop(animation); 51 | animation.callback = callback; 52 | }; 53 | 54 | let isActive = animation => animation.id != None; 55 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksAnimation.rei: -------------------------------------------------------------------------------- 1 | type t; 2 | 3 | type onStop = option(unit => unit); 4 | 5 | type ctrl = 6 | | Stop(onStop) 7 | | Continue; 8 | 9 | type callback = (. unit) => ctrl; 10 | 11 | let create: unit => t; 12 | 13 | let isActive: t => bool; 14 | 15 | let setCallback: (t, ~callback: callback) => unit; 16 | 17 | let start: t => unit; 18 | 19 | let stop: t => unit; -------------------------------------------------------------------------------- /src/hooks-animation/HooksAnimationRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var HooksReanimate$ReasonReactExample = require("./HooksReanimate.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, HooksReanimate$ReasonReactExample.ComponentGallery[/* make */1](/* array */[])), "index"); 8 | 9 | /* Not a pure module */ 10 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksAnimationRoot.re: -------------------------------------------------------------------------------- 1 | ReactDOMRe.renderToElementWithId( 2 | ReasonReact.element(HooksReanimate.ComponentGallery.make([||])), 3 | "index", 4 | ); 5 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksDemo.re: -------------------------------------------------------------------------------- 1 | module GlobalState = { 2 | type state = { 3 | count1: int, 4 | count2: int, 5 | toggle: bool, 6 | }; 7 | let initial = {count1: 0, count2: 0, toggle: false}; 8 | }; 9 | 10 | module Counter1 = { 11 | open GlobalState; 12 | let component = ReasonReact.statelessComponent("Counter1"); 13 | let make = (~state, ~update, _children) => { 14 | ...component, 15 | render: _ => 16 |
17 | 21 | 25 | (ReasonReact.string(" counter:" ++ string_of_int(state.count1))) 26 |
, 27 | }; 28 | }; 29 | 30 | module Counter2 = { 31 | open GlobalState; 32 | let component = ReasonReact.statelessComponent("Counter2"); 33 | let make = (~state, ~update, _children) => { 34 | ...component, 35 | render: _ => 36 |
37 | 41 | 45 | (ReasonReact.string(" counter:" ++ string_of_int(state.count2))) 46 |
, 47 | }; 48 | }; 49 | 50 | module Toggle = { 51 | open GlobalState; 52 | let component = ReasonReact.statelessComponent("Toggle"); 53 | let make = (~state, ~update, _children) => { 54 | ...component, 55 | render: _ => 56 |
57 | 61 | (ReasonReact.string(" toggle:" ++ string_of_bool(state.toggle))) 62 |
, 63 | }; 64 | }; 65 | 66 | module GlobalStateExample = { 67 | let component = ReasonReact.reducerComponent("GlobalStateExample"); 68 | let make = _children => { 69 | ...component, 70 | initialState: () => GlobalState.initial, 71 | reducer: (fn, state) => Update(fn(state)), 72 | render: ({state, send}) => { 73 | let update = foo => send(foo); 74 |
75 | 76 | 77 | 78 | 79 |
; 80 | }, 81 | }; 82 | }; 83 | 84 | module LocalCounter = { 85 | type state = int; 86 | type action = 87 | | Incr 88 | | Decr; 89 | let component = ReasonReact.reducerComponent("LocalCounter"); 90 | let make = _children => { 91 | ...component, 92 | initialState: () => 0, 93 | reducer: (action, state) => 94 | switch (action) { 95 | | Incr => Update(state + 1) 96 | | Decr => Update(state - 1) 97 | }, 98 | render: ({state, send}) => 99 |
100 | 103 | 106 | (ReasonReact.string(" counter:" ++ string_of_int(state))) 107 |
, 108 | }; 109 | }; 110 | 111 | module LocalToggle = { 112 | type state = bool; 113 | type action = 114 | | Toggle; 115 | let component = ReasonReact.reducerComponent("LocalToggle"); 116 | let make = _children => { 117 | ...component, 118 | initialState: () => false, 119 | reducer: (action, state) => 120 | switch (action) { 121 | | Toggle => Update(!state) 122 | }, 123 | render: ({state, send}) => 124 |
125 | 128 | (ReasonReact.string(" toggle:" ++ string_of_bool(state))) 129 |
, 130 | }; 131 | }; 132 | 133 | module LocalStateExample = { 134 | let component = ReasonReact.statelessComponent("LocalStateExample"); 135 | let make = _children => { 136 | ...component, 137 | render: _ => 138 |
139 | 140 | 141 | 142 | 143 |
, 144 | }; 145 | }; 146 | 147 | module TextInput = { 148 | type state = string; 149 | type action = 150 | | Text(string); 151 | let component = ReasonReact.reducerComponent("TextInput"); 152 | let textOfEvent = e => ReactEvent.Form.target(e)##value; 153 | let make = (~onChange=_ => (), ~showText=x => x, ~initial="", _children) => { 154 | ...component, 155 | initialState: () => initial, 156 | reducer: (action, _state) => 157 | switch (action) { 158 | | Text(text) => Update(text) 159 | }, 160 | render: ({state, send}) => 161 | { 165 | let text = textOfEvent(event); 166 | send(Text(text)); 167 | onChange(text); 168 | } 169 | ) 170 | />, 171 | }; 172 | }; 173 | 174 | module Spring = { 175 | type state = { 176 | animation: SpringAnimation.t, 177 | value: float, 178 | target: float, 179 | }; 180 | type action = 181 | | Click 182 | | Value(float); 183 | let component = ReasonReact.reducerComponent("Spring"); 184 | let make = (~renderValue, _children) => { 185 | ...component, 186 | initialState: () => { 187 | animation: SpringAnimation.create(0.0), 188 | value: 0.0, 189 | target: 1.0, 190 | }, 191 | didMount: ({state, send, onUnmount}) => { 192 | state.animation 193 | |> SpringAnimation.setOnChange( 194 | ~onChange=value => send(Value(value)), 195 | ~finalValue=state.target, 196 | ); 197 | onUnmount(() => SpringAnimation.stop(state.animation)); 198 | }, 199 | reducer: (action, state) => 200 | switch (action) { 201 | | Click => 202 | let target = state.target == 0.0 ? 1.0 : 0.0; 203 | UpdateWithSideEffects( 204 | {...state, target}, 205 | (_ => state.animation |> SpringAnimation.setFinalValue(target)), 206 | ); 207 | | Value(value) => Update({...state, value}) 208 | }, 209 | render: ({state, send}) => 210 |
211 | 214 |
(renderValue(state.value))
215 |
, 216 | }; 217 | }; 218 | 219 | module SimpleSpring = { 220 | let renderValue = value => 221 | ReasonReact.string(Printf.sprintf("value: %.3f", value)); 222 | let component = ReasonReact.statelessComponent("SimpleSpring"); 223 | let make = _children => {...component, render: _ => }; 224 | }; 225 | 226 | module AnimatedTextInput = { 227 | let shrinkText = (~text, ~value) => 228 | value >= 1.0 ? 229 | text : 230 | { 231 | let len = Js.Math.round(value *. float_of_int(String.length(text))); 232 | String.sub(text, 0, int_of_float(len)); 233 | }; 234 | let renderValue = value => 235 | shrinkText(~text, ~value)) 237 | initial="edit this or click target" 238 | />; 239 | let component = ReasonReact.statelessComponent("AnimatedTextInput"); 240 | let make = _children => {...component, render: _ => }; 241 | }; 242 | 243 | module TextInputRemote = { 244 | type state = string; 245 | type action = 246 | | Text(string) 247 | | Reset; 248 | let component = ReasonReact.reducerComponent("TextInputRemote"); 249 | let textOfEvent = e => ReactEvent.Form.target(e)##value; 250 | let make = 251 | ( 252 | ~remoteAction, 253 | ~onChange=_ => (), 254 | ~showText=x => x, 255 | ~initial="", 256 | _children, 257 | ) => { 258 | ...component, 259 | initialState: () => initial, 260 | didMount: ({send, onUnmount}) => { 261 | let token = RemoteAction.subscribe(~send, remoteAction); 262 | let cleanup = () => 263 | switch (token) { 264 | | Some(token) => RemoteAction.unsubscribe(token) 265 | | None => () 266 | }; 267 | onUnmount(cleanup); 268 | }, 269 | reducer: (action, _state) => 270 | switch (action) { 271 | | Text(text) => Update(text) 272 | | Reset => Update("the text has been reset") 273 | }, 274 | render: ({state, send}) => 275 | { 279 | let text = textOfEvent(event); 280 | send(Text(text)); 281 | onChange(text); 282 | } 283 | ) 284 | />, 285 | }; 286 | }; 287 | 288 | module AnimatedTextInputRemote = { 289 | let shrinkText = (~text, ~value) => 290 | value >= 1.0 ? 291 | text : 292 | { 293 | let len = Js.Math.round(value *. float_of_int(String.length(text))); 294 | String.sub(text, 0, int_of_float(len)); 295 | }; 296 | let remoteAction = RemoteAction.create(); 297 | let renderValue = value => 298 | shrinkText(~text, ~value)) 301 | initial="edit this or click target" 302 | />; 303 | let component = ReasonReact.statelessComponent("AnimatedTextInput"); 304 | let make = _children => { 305 | ...component, 306 | render: _ => 307 |
308 | 315 |
(ReasonReact.string("-----"))
316 | 317 |
, 318 | }; 319 | }; 320 | 321 | module GrandChild = { 322 | type action = 323 | | Incr 324 | | Decr; 325 | let component = ReasonReact.reducerComponent("GrandChild"); 326 | let make = (~remoteAction, _) => { 327 | ...component, 328 | initialState: () => 0, 329 | didMount: ({send, onUnmount}) => { 330 | let token = RemoteAction.subscribe(~send, remoteAction); 331 | let cleanup = () => 332 | switch (token) { 333 | | Some(token) => RemoteAction.unsubscribe(token) 334 | | None => () 335 | }; 336 | onUnmount(cleanup); 337 | }, 338 | reducer: (action, state) => 339 | switch (action) { 340 | | Incr => Update(state + 1) 341 | | Decr => Update(state - 1) 342 | }, 343 | render: ({state}) => 344 |
345 | (ReasonReact.string("in grandchild state: " ++ string_of_int(state))) 346 |
, 347 | }; 348 | }; 349 | 350 | module Child = { 351 | let component = ReasonReact.statelessComponent("Child"); 352 | let make = (~remoteAction, _) => { 353 | ...component, 354 | render: _ => 355 |
356 | (ReasonReact.string("in child")) 357 | 358 |
, 359 | }; 360 | }; 361 | 362 | module Parent = { 363 | let component = ReasonReact.reducerComponent("Parent"); 364 | let make = _ => { 365 | ...component, 366 | initialState: () => RemoteAction.create(), 367 | reducer: ((), _) => NoUpdate, 368 | render: ({state}) => 369 |
370 | 374 | 375 |
, 376 | }; 377 | }; 378 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksReanimate.re: -------------------------------------------------------------------------------- 1 | let pxI = i => string_of_int(i) ++ "px"; 2 | 3 | let pxF = v => pxI(int_of_float(v)); 4 | 5 | module Key = { 6 | let counter = ref(0); 7 | let gen = () => { 8 | incr(counter); 9 | string_of_int(counter^); 10 | }; 11 | }; 12 | 13 | module ImageTransition: { 14 | /*** 15 | * Render function for a transition between two images. 16 | * phase is a value between 0.0 (first image) and 1.0 (second image). 17 | **/ 18 | let render: (~phase: float, int, int) => ReasonReact.reactElement; 19 | let displayHeight: int; 20 | } = { 21 | let numImages = 6; 22 | let displayHeight = 200; 23 | let displayHeightString = pxI(displayHeight); 24 | let sizes = [| 25 | (500, 350), 26 | (800, 600), 27 | (800, 400), 28 | (700, 500), 29 | (200, 650), 30 | (600, 600), 31 | |]; 32 | let displayWidths = 33 | Belt.Array.map(sizes, ((w, h)) => w * displayHeight / h); 34 | let getWidth = i => displayWidths[(i + numImages) mod numImages]; 35 | 36 | /*** 37 | * Interpolate width and left for 2 images, phase is between 0.0 and 1.0. 38 | **/ 39 | let interpolate = (~width1, ~width2, phase) => { 40 | let width1 = float_of_int(width1); 41 | let width2 = float_of_int(width2); 42 | let width = width1 *. (1. -. phase) +. width2 *. phase; 43 | let left1 = -. (width1 *. phase); 44 | let left2 = left1 +. width1; 45 | (pxF(width), pxF(left1), pxF(left2)); 46 | }; 47 | let renderImage = (~left, i) => 48 | ; 53 | let render = (~phase, image1, image2) => { 54 | let width1 = getWidth(image1); 55 | let width2 = getWidth(image2); 56 | let (width, left1, left2) = interpolate(~width1, ~width2, phase); 57 |
58 |
61 | {renderImage(~left=left1, image1)} 62 | {renderImage(~left=left2, image2)} 63 |
64 |
; 65 | }; 66 | }; 67 | 68 | module ImageGalleryAnimation = { 69 | type action = 70 | | Click 71 | | SetCursor(float); 72 | type state = { 73 | animation: SpringAnimation.t, 74 | /* cursor value 3.5 means half way between image 3 and image 4 */ 75 | cursor: float, 76 | targetImage: int, 77 | }; 78 | let component = ReasonReact.reducerComponent("ImagesExample"); 79 | let make = (~initialImage=0, ~animateMount=true, _children) => { 80 | ...component, 81 | initialState: () => { 82 | animation: SpringAnimation.create(float_of_int(initialImage)), 83 | cursor: float_of_int(initialImage), 84 | targetImage: initialImage, 85 | }, 86 | didMount: ({state: {animation}, send}) => { 87 | animation 88 | |> SpringAnimation.setOnChange(~precision=0.05, ~onChange=cursor => 89 | send(SetCursor(cursor)) 90 | ); 91 | if (animateMount) { 92 | send(Click); 93 | }; 94 | }, 95 | willUnmount: ({state: {animation}}) => SpringAnimation.stop(animation), 96 | reducer: (action, state) => 97 | switch (action) { 98 | | Click => 99 | UpdateWithSideEffects( 100 | {...state, targetImage: state.targetImage + 1}, 101 | ( 102 | ({state: {animation, targetImage}}) => 103 | animation 104 | |> SpringAnimation.setFinalValue(float_of_int(targetImage)) 105 | ), 106 | ) 107 | | SetCursor(cursor) => Update({...state, cursor}) 108 | }, 109 | render: ({state: {cursor}, send}) => { 110 | let image = int_of_float(cursor); 111 | let phase = cursor -. float_of_int(image); 112 |
send(Click)}> 113 | {ImageTransition.render(~phase, image, image + 1)} 114 |
; 115 | }, 116 | }; 117 | }; 118 | 119 | module AnimatedButton = { 120 | module Text = { 121 | let component = ReasonReact.statelessComponent("Text"); 122 | let make = (~text, _children) => { 123 | ...component, 124 | render: _ => , 125 | }; 126 | }; 127 | type size = 128 | | Small 129 | | Large; 130 | let targetHeight = 30.; 131 | let closeWidth = 50.; 132 | let smallWidth = 250.; 133 | let largeWidth = 450.; 134 | type state = { 135 | animation: SpringAnimation.t, 136 | width: int, 137 | size, 138 | clickCount: int, 139 | actionCount: int, 140 | }; 141 | type action = 142 | | Click 143 | | Reset 144 | | Unclick 145 | /* Width action triggered during animation. */ 146 | | Width(int) 147 | /* Toggle the size between small and large, and animate the width. */ 148 | | ToggleSize 149 | /* Close the button by animating the width to shrink. */ 150 | | Close; 151 | let component = ReasonReact.reducerComponent("ButtonAnimation"); 152 | let make = 153 | (~text="Button", ~rAction, ~animateMount=true, ~onClose=?, _children) => { 154 | ...component, 155 | initialState: () => { 156 | animation: SpringAnimation.create(smallWidth), 157 | width: int_of_float(smallWidth), 158 | size: Small, 159 | clickCount: 0, 160 | actionCount: 0, 161 | }, 162 | didMount: ({send}) => { 163 | RemoteAction.subscribe(~send, rAction) |> ignore; 164 | if (animateMount) { 165 | send(ToggleSize); 166 | }; 167 | }, 168 | willUnmount: ({state: {animation}}) => SpringAnimation.stop(animation), 169 | reducer: (action, state) => 170 | switch (action) { 171 | | Click => 172 | UpdateWithSideEffects( 173 | { 174 | ...state, 175 | clickCount: state.clickCount + 1, 176 | actionCount: state.actionCount + 1, 177 | }, 178 | (({send}) => send(ToggleSize)), 179 | ) 180 | | Reset => 181 | Update({...state, clickCount: 0, actionCount: state.actionCount + 1}) 182 | | Unclick => 183 | Update({ 184 | ...state, 185 | clickCount: state.clickCount - 1, 186 | actionCount: state.actionCount + 1, 187 | }) 188 | | Width(width) => Update({...state, width}) 189 | | ToggleSize => 190 | UpdateWithSideEffects( 191 | {...state, size: state.size === Small ? Large : Small}, 192 | ( 193 | ({state: {animation, size}, send}) => 194 | animation 195 | |> SpringAnimation.setOnChange( 196 | ~finalValue=size === Small ? smallWidth : largeWidth, 197 | ~precision=10., 198 | ~onChange=w => 199 | send(Width(int_of_float(w))) 200 | ) 201 | ), 202 | ) 203 | | Close => 204 | SideEffects( 205 | ( 206 | ({state: {animation}, send}) => 207 | animation 208 | |> SpringAnimation.setOnChange( 209 | ~finalValue=closeWidth, 210 | ~speedup=0.3, 211 | ~precision=10., 212 | ~onStop=onClose, 213 | ~onChange=w => 214 | send(Width(int_of_float(w))) 215 | ) 216 | ), 217 | ) 218 | }, 219 | render: ({state: {width} as state, send}) => { 220 | let buttonLabel = state => 221 | text 222 | ++ " clicks:" 223 | ++ string_of_int(state.clickCount) 224 | ++ " actions:" 225 | ++ string_of_int(state.actionCount); 226 |
send(Click)} 229 | style={ReactDOMRe.Style.make(~width=pxI(width), ())}> 230 | 231 |
; 232 | }, 233 | }; 234 | }; 235 | 236 | module AnimateHeight = { 237 | /* When the closing animation begins */ 238 | type onBeginClosing = Animation.onStop; 239 | type action = 240 | | Open(Animation.onStop) 241 | | BeginClosing(onBeginClosing, Animation.onStop) 242 | | Close(Animation.onStop) 243 | | Animate(float, Animation.onStop) 244 | | Height(float); 245 | type state = { 246 | height: float, 247 | animation: SpringAnimation.t, 248 | }; 249 | let component = ReasonReact.reducerComponent("HeightAnim"); 250 | let make = (~rAction, ~targetHeight, children) => { 251 | ...component, 252 | initialState: () => {height: 0., animation: SpringAnimation.create(0.)}, 253 | didMount: ({send}) => { 254 | RemoteAction.subscribe(~send, rAction) |> ignore; 255 | send(Animate(targetHeight, None)); 256 | }, 257 | reducer: (action, state) => 258 | switch (action) { 259 | | Height(v) => Update({...state, height: v}) 260 | | Animate(finalValue, onStop) => 261 | SideEffects( 262 | ( 263 | ({send}) => 264 | state.animation 265 | |> SpringAnimation.setOnChange( 266 | ~finalValue, ~precision=10., ~onStop, ~onChange=h => 267 | send(Height(h)) 268 | ) 269 | ), 270 | ) 271 | | Close(onClose) => 272 | SideEffects((({send}) => send(Animate(0., onClose)))) 273 | | BeginClosing(onBeginClosing, onClose) => 274 | SideEffects( 275 | ( 276 | ({send}) => { 277 | switch (onBeginClosing) { 278 | | None => () 279 | | Some(f) => f() 280 | }; 281 | send(Animate(0., onClose)); 282 | } 283 | ), 284 | ) 285 | | Open(onOpen) => 286 | SideEffects((({send}) => send(Animate(targetHeight, onOpen)))) 287 | }, 288 | willUnmount: ({state}) => SpringAnimation.stop(state.animation), 289 | render: ({state}) => 290 |
298 | {ReasonReact.array(children)} 299 |
, 300 | }; 301 | }; 302 | 303 | module ReducerAnimationExample = { 304 | type action = 305 | | SetAct(action => unit) 306 | | AddSelf 307 | | AddButton(bool) 308 | | AddButtonFirst(bool) 309 | | AddImage(bool) 310 | | DecrementAllButtons 311 | /* Remove from the list the button uniquely identified by its height RemoteAction */ 312 | | FilterOutItem(RemoteAction.t(AnimateHeight.action)) 313 | | IncrementAllButtons 314 | | CloseAllButtons 315 | | RemoveItem 316 | | ResetAllButtons 317 | | ReverseItemsAnimation 318 | | CloseHeight(Animation.onStop) /* Used by ReverseAnim */ 319 | | ReverseWithSideEffects(unit => unit) /* Used by ReverseAnim */ 320 | | OpenHeight(Animation.onStop) /* Used by ReverseAnim */ 321 | | ToggleRandomAnimation; 322 | type item = { 323 | element: ReasonReact.reactElement, 324 | rActionButton: RemoteAction.t(AnimatedButton.action), 325 | rActionHeight: RemoteAction.t(AnimateHeight.action), 326 | /* used while removing items, to find the first item not already closing */ 327 | mutable closing: bool, 328 | }; 329 | module State: { 330 | type t = { 331 | act: action => unit, 332 | randomAnimation: Animation.t, 333 | items: list(item), 334 | }; 335 | let createButton: 336 | ( 337 | ~removeFromList: RemoteAction.t(AnimateHeight.action) => unit, 338 | ~animateMount: bool=?, 339 | int 340 | ) => 341 | item; 342 | let createImage: (~animateMount: bool=?, int) => item; 343 | let getElements: t => array(ReasonReact.reactElement); 344 | let initial: unit => t; 345 | } = { 346 | type t = { 347 | act: action => unit, 348 | randomAnimation: Animation.t, 349 | items: list(item), 350 | }; 351 | let initial = () => { 352 | act: _action => (), 353 | randomAnimation: Animation.create(), 354 | items: [], 355 | }; 356 | let getElements = ({items}) => 357 | Belt.List.toArray(Belt.List.mapReverse(items, x => x.element)); 358 | let createButton = (~removeFromList, ~animateMount=?, number) => { 359 | let rActionButton = RemoteAction.create(); 360 | let rActionHeight = RemoteAction.create(); 361 | let key = Key.gen(); 362 | let onClose = () => 363 | RemoteAction.send( 364 | rActionHeight, 365 | ~action= 366 | AnimateHeight.Close(Some(() => removeFromList(rActionHeight))), 367 | ); 368 | let element: ReasonReact.reactElement = 369 | 371 | 378 | ; 379 | {element, rActionButton, rActionHeight, closing: false}; 380 | }; 381 | let createImage = (~animateMount=?, number) => { 382 | let key = Key.gen(); 383 | let rActionButton = RemoteAction.create(); 384 | let imageGalleryAnimation = 385 | ; 390 | let rActionHeight = RemoteAction.create(); 391 | let element = 392 | 396 | imageGalleryAnimation 397 | ; 398 | {element, rActionButton, rActionHeight, closing: false}; 399 | }; 400 | }; 401 | let runAll = action => { 402 | let performSideEffects = ({ReasonReact.state: {State.items}}) => 403 | Belt.List.forEach(items, ({rActionButton}) => 404 | RemoteAction.send(rActionButton, ~action) 405 | ); 406 | ReasonReact.SideEffects(performSideEffects); 407 | }; 408 | let component = ReasonReact.reducerComponent("ReducerAnimationExample"); 409 | let rec make = (~showAllButtons, _children) => { 410 | ...component, 411 | initialState: () => State.initial(), 412 | didMount: ({state: {State.randomAnimation: animation}, send}) => { 413 | let callback = 414 | (.) => { 415 | let randomAction = 416 | switch (Random.int(6)) { 417 | | 0 => AddButton(true) 418 | | 1 => AddImage(true) 419 | | 2 => RemoveItem 420 | | 3 => RemoveItem 421 | | 4 => DecrementAllButtons 422 | | 5 => IncrementAllButtons 423 | | _ => assert(false) 424 | }; 425 | send(randomAction); 426 | Animation.Continue; 427 | }; 428 | send(SetAct(send)); 429 | Animation.setCallback(animation, ~callback); 430 | }, 431 | willUnmount: ({state: {randomAnimation}}) => 432 | Animation.stop(randomAnimation), 433 | reducer: (action, {act, items, randomAnimation} as state) => 434 | switch (action) { 435 | | SetAct(act) => Update({...state, act}) 436 | | AddSelf => 437 | module Self = { 438 | let make = make(~showAllButtons); 439 | }; 440 | let key = Key.gen(); 441 | let rActionButton = RemoteAction.create(); 442 | let rActionHeight = RemoteAction.create(); 443 | let element = 444 | 445 | 446 | ; 447 | let item = {element, rActionButton, rActionHeight, closing: false}; 448 | Update({...state, items: [item, ...items]}); 449 | | AddButton(animateMount) => 450 | let removeFromList = rActionHeight => 451 | act(FilterOutItem(rActionHeight)); 452 | Update({ 453 | ...state, 454 | items: [ 455 | State.createButton( 456 | ~removeFromList, 457 | ~animateMount, 458 | Belt.List.length(items), 459 | ), 460 | ...items, 461 | ], 462 | }); 463 | | AddButtonFirst(animateMount) => 464 | let removeFromList = rActionHeight => 465 | act(FilterOutItem(rActionHeight)); 466 | Update({ 467 | ...state, 468 | items: 469 | items 470 | @ [ 471 | State.createButton( 472 | ~removeFromList, 473 | ~animateMount, 474 | Belt.List.length(items), 475 | ), 476 | ], 477 | }); 478 | | AddImage(animateMount) => 479 | Update({ 480 | ...state, 481 | items: [ 482 | State.createImage(~animateMount, Belt.List.length(items)), 483 | ...items, 484 | ], 485 | }) 486 | | FilterOutItem(rAction) => 487 | let filter = item => item.rActionHeight !== rAction; 488 | Update({...state, items: Belt.List.keep(items, filter)}); 489 | | DecrementAllButtons => runAll(Unclick) 490 | | IncrementAllButtons => runAll(Click) 491 | | CloseAllButtons => runAll(Close) 492 | | RemoveItem => 493 | switch (Belt.List.getBy(items, item => item.closing === false)) { 494 | | Some(firstItemNotClosing) => 495 | let onBeginClosing = 496 | Some((() => firstItemNotClosing.closing = true)); 497 | let onClose = 498 | Some( 499 | (() => act(FilterOutItem(firstItemNotClosing.rActionHeight))), 500 | ); 501 | SideEffects( 502 | ( 503 | _ => 504 | RemoteAction.send( 505 | firstItemNotClosing.rActionHeight, 506 | ~action=BeginClosing(onBeginClosing, onClose), 507 | ) 508 | ), 509 | ); 510 | | None => NoUpdate 511 | } 512 | | ResetAllButtons => runAll(Reset) 513 | | CloseHeight(onStop) => 514 | let len = Belt.List.length(items); 515 | let count = ref(len); 516 | let onClose = () => { 517 | decr(count); 518 | if (count^ === 0) { 519 | switch (onStop) { 520 | | None => () 521 | | Some(f) => f() 522 | }; 523 | }; 524 | }; 525 | let iter = _ => 526 | Belt.List.forEach(items, item => 527 | RemoteAction.send( 528 | item.rActionHeight, 529 | ~action=Close(Some(onClose)), 530 | ) 531 | ); 532 | SideEffects(iter); 533 | | OpenHeight(onStop) => 534 | let len = Belt.List.length(items); 535 | let count = ref(len); 536 | let onClose = () => { 537 | decr(count); 538 | if (count^ === 0) { 539 | switch (onStop) { 540 | | None => () 541 | | Some(f) => f() 542 | }; 543 | }; 544 | }; 545 | let iter = _ => 546 | Belt.List.forEach(items, item => 547 | RemoteAction.send( 548 | item.rActionHeight, 549 | ~action=Open(Some(onClose)), 550 | ) 551 | ); 552 | SideEffects(iter); 553 | | ReverseWithSideEffects(performSideEffects) => 554 | UpdateWithSideEffects( 555 | {...state, items: Belt.List.reverse(items)}, 556 | (_ => performSideEffects()), 557 | ) 558 | | ReverseItemsAnimation => 559 | let onStopClose = () => 560 | act(ReverseWithSideEffects(() => act(OpenHeight(None)))); 561 | SideEffects((_ => act(CloseHeight(Some(onStopClose))))); 562 | | ToggleRandomAnimation => 563 | SideEffects( 564 | ( 565 | _ => 566 | Animation.isActive(randomAnimation) ? 567 | Animation.stop(randomAnimation) : 568 | Animation.start(randomAnimation) 569 | ), 570 | ) 571 | }, 572 | render: ({state}) => { 573 | let button = (~repeat=1, ~hide=false, txt, action) => 574 | hide ? 575 | ReasonReact.null : 576 |
581 | for (_ in 1 to repeat) { 582 | state.act(action); 583 | } 584 | }> 585 | {ReasonReact.string(txt)} 586 |
; 587 | let hide = !showAllButtons; 588 |
589 |
590 | {ReasonReact.string("Control:")} 591 | {button("Add Button", AddButton(true))} 592 | {button("Add Image", AddImage(true))} 593 | {button("Add Button On Top", AddButtonFirst(true))} 594 | {button("Remove Item", RemoveItem)} 595 | { 596 | button( 597 | ~hide, 598 | ~repeat=100, 599 | "Add 100 Buttons On Top", 600 | AddButtonFirst(false), 601 | ) 602 | } 603 | {button(~hide, ~repeat=100, "Add 100 Images", AddImage(false))} 604 | {button("Click all the Buttons", IncrementAllButtons)} 605 | {button(~hide, "Unclick all the Buttons", DecrementAllButtons)} 606 | {button("Close all the Buttons", CloseAllButtons)} 607 | { 608 | button( 609 | ~hide, 610 | ~repeat=10, 611 | "Click all the Buttons 10 times", 612 | IncrementAllButtons, 613 | ) 614 | } 615 | {button(~hide, "Reset all the Buttons' states", ResetAllButtons)} 616 | {button("Reverse Items", ReverseItemsAnimation)} 617 | { 618 | button( 619 | "Random Animation " 620 | ++ (Animation.isActive(state.randomAnimation) ? "ON" : "OFF"), 621 | ToggleRandomAnimation, 622 | ) 623 | } 624 | {button("Add Self", AddSelf)} 625 |
626 |
629 |
630 | { 631 | ReasonReact.string( 632 | "Items:" ++ string_of_int(Belt.List.length(state.items)), 633 | ) 634 | } 635 |
636 | {ReasonReact.array(State.getElements(state))} 637 |
638 |
; 639 | }, 640 | }; 641 | }; 642 | 643 | module ChatHead = { 644 | type action = 645 | | MoveX(float) 646 | | MoveY(float); 647 | type state = { 648 | x: float, 649 | y: float, 650 | }; 651 | let component = ReasonReact.reducerComponent("ChatHead"); 652 | let make = (~rAction, ~headNum, ~imageGallery, _children) => { 653 | ...component, 654 | initialState: () => {x: 0., y: 0.}, 655 | didMount: ({send}) => RemoteAction.subscribe(~send, rAction) |> ignore, 656 | reducer: (action, state: state) => 657 | switch (action) { 658 | | MoveX(x) => Update({...state, x}) 659 | | MoveY(y) => Update({...state, y}) 660 | }, 661 | render: ({state: {x, y}}) => { 662 | let left = pxF(x -. 25.); 663 | let top = pxF(y -. 25.); 664 | imageGallery ? 665 |
675 | 676 |
: 677 |
; 688 | }, 689 | }; 690 | }; 691 | 692 | module ChatHeadsExample = { 693 | [@bs.val] 694 | external addEventListener: (string, Js.t({..}) => unit) => unit = 695 | "window.addEventListener"; 696 | let numHeads = 6; 697 | type control = { 698 | rAction: RemoteAction.t(ChatHead.action), 699 | animX: SpringAnimation.t, 700 | animY: SpringAnimation.t, 701 | }; 702 | type state = { 703 | controls: array(control), 704 | chatHeads: array(ReasonReact.reactElement), 705 | }; 706 | let createControl = () => { 707 | rAction: RemoteAction.create(), 708 | animX: SpringAnimation.create(0.), 709 | animY: SpringAnimation.create(0.), 710 | }; 711 | 712 | [@react.component] 713 | let make = (~imageGallery, _) => { 714 | let ({chatHeads, controls}, _send) = 715 | React.useReducer( 716 | (state, _action) => state, 717 | { 718 | let controls = Belt.Array.makeBy(numHeads, _ => createControl()); 719 | let chatHeads = 720 | Belt.Array.makeBy(numHeads, i => 721 | 727 | ); 728 | 729 | {controls, chatHeads}; 730 | }, 731 | ); 732 | 733 | React.useEffect0(() => { 734 | let setupAnimation = headNum => { 735 | let setOnChange = (~isX, afterChange) => { 736 | let control = controls[headNum]; 737 | let animation = isX ? control.animX : control.animY; 738 | animation 739 | |> SpringAnimation.setOnChange( 740 | ~preset=Spring.gentle, 741 | ~speedup=2., 742 | ~onChange=v => { 743 | RemoteAction.send( 744 | control.rAction, 745 | ~action=isX ? MoveX(v) : MoveY(v), 746 | ); 747 | afterChange(v); 748 | }, 749 | ); 750 | }; 751 | let isLastHead = headNum == numHeads - 1; 752 | let afterChangeX = x => 753 | isLastHead ? 754 | () : 755 | controls[headNum + 1].animX |> SpringAnimation.setFinalValue(x); 756 | let afterChangeY = y => 757 | isLastHead ? 758 | () : 759 | controls[headNum + 1].animY |> SpringAnimation.setFinalValue(y); 760 | setOnChange(~isX=true, afterChangeX); 761 | setOnChange(~isX=false, afterChangeY); 762 | }; 763 | Belt.Array.forEachWithIndex(controls, (i, _) => setupAnimation(i)); 764 | let onMove = e => { 765 | let x = e##pageX; 766 | let y = e##pageY; 767 | controls[0].animX |> SpringAnimation.setFinalValue(x); 768 | controls[0].animY |> SpringAnimation.setFinalValue(y); 769 | }; 770 | addEventListener("mousemove", onMove); 771 | addEventListener("touchmove", onMove); 772 | 773 | Some( 774 | () => 775 | Belt.Array.forEach( 776 | controls, 777 | ({animX, animY}) => { 778 | SpringAnimation.stop(animX); 779 | SpringAnimation.stop(animY); 780 | }, 781 | ), 782 | ); 783 | }); 784 | 785 |
{ReasonReact.array(chatHeads)}
; 786 | }; 787 | }; 788 | 789 | module ChatHeadsExampleStarter = { 790 | type state = 791 | | StartMessage 792 | | ChatHeads 793 | | ImageGalleryHeads; 794 | let component = ReasonReact.reducerComponent("ChatHeadsExampleStarter"); 795 | let make = _children => { 796 | ...component, 797 | initialState: () => StartMessage, 798 | reducer: (actionIsState, _) => Update(actionIsState), 799 | render: ({state, send}) => 800 | switch (state) { 801 | | StartMessage => 802 |
803 |
804 | 807 |
808 | 811 |
812 | | ChatHeads => 813 | React.createElement( 814 | ChatHeadsExample.make, 815 | ChatHeadsExample.makeProps(~imageGallery=false, ()), 816 | ) 817 | | ImageGalleryHeads => 818 | React.createElement( 819 | ChatHeadsExample.make, 820 | ChatHeadsExample.makeProps(~imageGallery=true, ()), 821 | ) 822 | }, 823 | }; 824 | }; 825 | 826 | module GalleryItem = { 827 | let component = ReasonReact.statelessComponent("GalleryItem"); 828 | let make = (~title="Untitled", ~description="no description", child) => { 829 | let title =
{ReasonReact.string(title)}
; 830 | let description = 831 |
; 835 | let leftRight = 836 |
837 |
child
838 |
; 839 | { 840 | ...component, 841 | render: _self => 842 |
title description leftRight
, 843 | }; 844 | }; 845 | }; 846 | 847 | module GalleryContainer = { 848 | let component = ReasonReact.statelessComponent("GalleryContainer"); 849 | let megaHeaderTitle = "Animating With Reason React Reducers"; 850 | let megaHeaderSubtext = {| 851 | Examples With Animations. 852 | |}; 853 | let megaHeaderSubtextDetails = {| 854 | Explore animation with ReasonReact and reducers. 855 | 856 | |}; 857 | let make = children => { 858 | ...component, 859 | render: _self => 860 |
863 |
864 | {ReasonReact.string(megaHeaderTitle)} 865 |
866 |
867 | {ReasonReact.string(megaHeaderSubtext)} 868 |
869 |
870 | {ReasonReact.string(megaHeaderSubtextDetails)} 871 |
872 | { 873 | ReasonReact.array( 874 | Array.map(c =>
c
, children), 875 | ) 876 | } 877 |
, 878 | }; 879 | }; 880 | 881 | module ComponentGallery = { 882 | let component = ReasonReact.statelessComponent("ComponentGallery"); 883 | let make = _children => { 884 | let globalStateExample = 885 | 886 | ... 887 | ; 888 | let localStateExample = 889 | 890 | ... 891 | ; 892 | let simpleTextInput = 893 | 895 | ... Js.log2("onChange:", text)} /> 896 | ; 897 | let simpleSpring = 898 | 900 | ... 901 | ; 902 | let animatedTextInput = 903 | 906 | ... 907 | ; 908 | let animatedTextInputRemote = 909 | 912 | ... 913 | ; 914 | let callActionsOnGrandChild = 915 | 917 | ... 918 | ; 919 | let chatHeads = 920 | 921 | ... 922 | ; 923 | let imageGallery = 924 | 927 | ... 928 | ; 929 | let reducerAnimation = 930 | 931 | ... 932 | ; 933 | { 934 | ...component, 935 | render: _self => 936 | 937 | globalStateExample 938 | localStateExample 939 | simpleTextInput 940 | simpleSpring 941 | animatedTextInput 942 | animatedTextInputRemote 943 | callActionsOnGrandChild 944 | chatHeads 945 | imageGallery 946 | reducerAnimation 947 | , 948 | }; 949 | }; 950 | }; -------------------------------------------------------------------------------- /src/hooks-animation/HooksRemoteAction.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | 5 | function sendDefault() { 6 | return /* () */0; 7 | } 8 | 9 | function create() { 10 | return /* record */[/* send */sendDefault]; 11 | } 12 | 13 | function subscribe(send, x) { 14 | if (x[/* send */0] === sendDefault) { 15 | x[/* send */0] = send; 16 | return x; 17 | } 18 | 19 | } 20 | 21 | function unsubscribe(x) { 22 | x[/* send */0] = sendDefault; 23 | return /* () */0; 24 | } 25 | 26 | function send(x, action) { 27 | return Curry._1(x[/* send */0], action); 28 | } 29 | 30 | exports.create = create; 31 | exports.subscribe = subscribe; 32 | exports.unsubscribe = unsubscribe; 33 | exports.send = send; 34 | /* No side effect */ 35 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksRemoteAction.re: -------------------------------------------------------------------------------- 1 | type t('action) = {mutable send: 'action => unit}; 2 | 3 | type token('action) = t('action); 4 | 5 | let sendDefault = _action => (); 6 | 7 | let create = () => {send: sendDefault}; 8 | 9 | /*** 10 | * The return type of subscribe is constrained as a token 11 | * by the interface file. This means that only the caller of 12 | * a given RemoteAction has the ability to unsubscribe. 13 | */ 14 | let subscribe = (~send, x) => 15 | if (x.send === sendDefault) { 16 | x.send = send; 17 | Some(x); 18 | } else { 19 | None; 20 | }; 21 | 22 | let unsubscribe = x => x.send = sendDefault; 23 | 24 | let send = (x, ~action) => x.send(action); 25 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksRemoteAction.rei: -------------------------------------------------------------------------------- 1 | /*** 2 | * RemoteAction provides a way to send actions to a remote component. 3 | * The sender creates a fresh RemoteAction and passes it down. 4 | * The recepient component calls subscribe in the didMount method. 5 | * The caller can then send actions to the recipient components via send. 6 | */ 7 | type t('action); 8 | 9 | type token('action); 10 | 11 | 12 | /*** Create a new remote action, to which one component will subscribe. */ 13 | let create: unit => t('action); 14 | 15 | 16 | /*** 17 | * Subscribe to the remote action, via the component's `send` function. 18 | * Returns an unsubscribe token which can be used to end the connection 19 | * to this particular send function. Will only return a token if the remote 20 | * action passed does not already have an active subscription. 21 | */ 22 | let subscribe: 23 | (~send: 'action => unit, t('action)) => option(token('action)); 24 | 25 | 26 | /*** Unsubscribe from a subscription */ 27 | let unsubscribe: token('action) => unit; 28 | 29 | 30 | /*** Perform an action on the subscribed component. */ 31 | let send: (t('action), ~action: 'action) => unit; -------------------------------------------------------------------------------- /src/hooks-animation/HooksSpring.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var defaultSecondsPerFrame = 1 / 60; 5 | 6 | var noWobble = /* record */[ 7 | /* stiffness */170, 8 | /* damping */26 9 | ]; 10 | 11 | function createState(value) { 12 | return /* record */[ 13 | /* value */value, 14 | /* velocity */0, 15 | /* finalValue */value 16 | ]; 17 | } 18 | 19 | function stepper($staropt$star, speedup, $staropt$star$1, $staropt$star$2, state) { 20 | var finalValue = state[/* finalValue */2]; 21 | var velocity = state[/* velocity */1]; 22 | var value = state[/* value */0]; 23 | var secondsPerFrame = $staropt$star !== undefined ? $staropt$star : defaultSecondsPerFrame; 24 | var precision = $staropt$star$1 !== undefined ? $staropt$star$1 : 0.01; 25 | var preset = $staropt$star$2 !== undefined ? $staropt$star$2 : noWobble; 26 | var secondsPerFrame$1 = speedup !== undefined ? secondsPerFrame * speedup : secondsPerFrame; 27 | var forceSpring = -preset[/* stiffness */0] * (value - finalValue); 28 | var forceDamper = -preset[/* damping */1] * velocity; 29 | var acceleration = forceSpring + forceDamper; 30 | var newVelocity = velocity + acceleration * secondsPerFrame$1; 31 | var newValue = value + newVelocity * secondsPerFrame$1; 32 | var match = Math.abs(newVelocity) < precision && Math.abs(newValue - finalValue) < precision; 33 | if (match) { 34 | return /* record */[ 35 | /* value */finalValue, 36 | /* velocity */0.0, 37 | /* finalValue */state[/* finalValue */2] 38 | ]; 39 | } else { 40 | return /* record */[ 41 | /* value */newValue, 42 | /* velocity */newVelocity, 43 | /* finalValue */state[/* finalValue */2] 44 | ]; 45 | } 46 | } 47 | 48 | function isFinished(param) { 49 | if (param[/* value */0] === param[/* finalValue */2]) { 50 | return param[/* velocity */1] === 0; 51 | } else { 52 | return false; 53 | } 54 | } 55 | 56 | function test() { 57 | var _state = /* record */[ 58 | /* value */0.0, 59 | /* velocity */0.0, 60 | /* finalValue */1.0 61 | ]; 62 | while(true) { 63 | var state = _state; 64 | console.log(state); 65 | if (isFinished(state)) { 66 | return 0; 67 | } else { 68 | _state = stepper(undefined, undefined, undefined, undefined, state); 69 | continue ; 70 | } 71 | }; 72 | } 73 | 74 | var defaultPrecision = 0.01; 75 | 76 | var gentle = /* record */[ 77 | /* stiffness */120, 78 | /* damping */14 79 | ]; 80 | 81 | var wobbly = /* record */[ 82 | /* stiffness */180, 83 | /* damping */12 84 | ]; 85 | 86 | var stiff = /* record */[ 87 | /* stiffness */210, 88 | /* damping */20 89 | ]; 90 | 91 | var defaultPreset = noWobble; 92 | 93 | exports.defaultSecondsPerFrame = defaultSecondsPerFrame; 94 | exports.defaultPrecision = defaultPrecision; 95 | exports.noWobble = noWobble; 96 | exports.gentle = gentle; 97 | exports.wobbly = wobbly; 98 | exports.stiff = stiff; 99 | exports.defaultPreset = defaultPreset; 100 | exports.createState = createState; 101 | exports.stepper = stepper; 102 | exports.isFinished = isFinished; 103 | exports.test = test; 104 | /* No side effect */ 105 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksSpring.re: -------------------------------------------------------------------------------- 1 | let defaultSecondsPerFrame = 1. /. 60.; 2 | 3 | let defaultPrecision = 0.01; 4 | 5 | type preset = { 6 | stiffness: float, 7 | damping: float, 8 | }; 9 | 10 | let noWobble = {stiffness: 170., damping: 26.}; 11 | 12 | let gentle = {stiffness: 120., damping: 14.}; 13 | 14 | let wobbly = {stiffness: 180., damping: 12.}; 15 | 16 | let stiff = {stiffness: 210., damping: 20.}; 17 | 18 | let defaultPreset = noWobble; 19 | 20 | type state = { 21 | value: float, 22 | velocity: float, 23 | finalValue: float, 24 | }; 25 | 26 | let createState = value => {value, velocity: 0., finalValue: value}; 27 | 28 | let stepper = 29 | ( 30 | ~secondsPerFrame=defaultSecondsPerFrame, 31 | ~speedup=?, 32 | ~precision=defaultPrecision, 33 | ~preset=defaultPreset, 34 | {value, velocity, finalValue} as state, 35 | ) => { 36 | let secondsPerFrame = 37 | switch (speedup) { 38 | | None => secondsPerFrame 39 | | Some(x) => secondsPerFrame *. x 40 | }; 41 | let forceSpring = -. preset.stiffness *. (value -. finalValue); 42 | let forceDamper = -. preset.damping *. velocity; 43 | let acceleration = forceSpring +. forceDamper; 44 | let newVelocity = velocity +. acceleration *. secondsPerFrame; 45 | let newValue = value +. newVelocity *. secondsPerFrame; 46 | abs_float(newVelocity) < precision 47 | && abs_float(newValue -. finalValue) < precision ? 48 | {...state, value: finalValue, velocity: 0.0} : 49 | {...state, value: newValue, velocity: newVelocity}; 50 | }; 51 | 52 | let isFinished = ({value, velocity, finalValue}) => 53 | value == finalValue && velocity == 0.; 54 | 55 | let test = () => { 56 | let rec iterate = state => { 57 | Js.log(state); 58 | if (!isFinished(state)) { 59 | iterate(stepper(state)); 60 | }; 61 | }; 62 | iterate({value: 0.0, velocity: 0.0, finalValue: 1.0}); 63 | }; 64 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksSpringAnimation.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 5 | var Spring$ReasonReactExample = require("../animation/Spring.bs.js"); 6 | var Animation$ReasonReactExample = require("../animation/Animation.bs.js"); 7 | 8 | function create(initialValue) { 9 | var animation = Animation$ReasonReactExample.create(/* () */0); 10 | var state = Spring$ReasonReactExample.createState(initialValue); 11 | return /* record */[ 12 | /* animation */animation, 13 | /* state */state 14 | ]; 15 | } 16 | 17 | function setOnChange(preset, speedup, precision, $staropt$star, onChange, finalValue, a) { 18 | var onStop = $staropt$star !== undefined ? Js_primitive.valFromOption($staropt$star) : undefined; 19 | var callback = function () { 20 | a[/* state */1] = Spring$ReasonReactExample.stepper(undefined, speedup, precision, preset, a[/* state */1]); 21 | var isFinished = Spring$ReasonReactExample.isFinished(a[/* state */1]); 22 | Curry._1(onChange, a[/* state */1][/* value */0]); 23 | if (isFinished) { 24 | return /* Stop */[onStop]; 25 | } else { 26 | return /* Continue */0; 27 | } 28 | }; 29 | Animation$ReasonReactExample.stop(a[/* animation */0]); 30 | ((function (param) { 31 | return Animation$ReasonReactExample.setCallback(param, callback); 32 | })(a[/* animation */0])); 33 | if (finalValue !== undefined) { 34 | var init = a[/* state */1]; 35 | a[/* state */1] = /* record */[ 36 | /* value */init[/* value */0], 37 | /* velocity */init[/* velocity */1], 38 | /* finalValue */finalValue 39 | ]; 40 | return Animation$ReasonReactExample.start(a[/* animation */0]); 41 | } else { 42 | return /* () */0; 43 | } 44 | } 45 | 46 | function setFinalValue(finalValue, a) { 47 | Animation$ReasonReactExample.stop(a[/* animation */0]); 48 | var init = a[/* state */1]; 49 | a[/* state */1] = /* record */[ 50 | /* value */init[/* value */0], 51 | /* velocity */init[/* velocity */1], 52 | /* finalValue */finalValue 53 | ]; 54 | return Animation$ReasonReactExample.start(a[/* animation */0]); 55 | } 56 | 57 | function stop(a) { 58 | return Animation$ReasonReactExample.stop(a[/* animation */0]); 59 | } 60 | 61 | exports.create = create; 62 | exports.setOnChange = setOnChange; 63 | exports.setFinalValue = setFinalValue; 64 | exports.stop = stop; 65 | /* No side effect */ 66 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksSpringAnimation.re: -------------------------------------------------------------------------------- 1 | type t = { 2 | animation: Animation.t, 3 | mutable state: Spring.state, 4 | }; 5 | 6 | let create = initialValue => { 7 | let animation = Animation.create(); 8 | let state = Spring.createState(initialValue); 9 | {animation, state}; 10 | }; 11 | 12 | type onChange = float => unit; 13 | 14 | let setOnChange = 15 | ( 16 | ~preset=?, 17 | ~speedup=?, 18 | ~precision=?, 19 | ~onStop=None, 20 | ~onChange, 21 | ~finalValue=?, 22 | a, 23 | ) => { 24 | let callback = 25 | (.) => { 26 | a.state = Spring.stepper(~preset?, ~speedup?, ~precision?, a.state); 27 | let isFinished = Spring.isFinished(a.state); 28 | onChange(a.state.value); 29 | isFinished ? Animation.Stop(onStop) : Continue; 30 | }; 31 | a.animation |> Animation.stop; 32 | a.animation |> Animation.setCallback(~callback); 33 | switch (finalValue) { 34 | | None => () 35 | | Some(finalValue) => 36 | a.state = {...a.state, finalValue}; 37 | a.animation |> Animation.start; 38 | }; 39 | }; 40 | 41 | let setFinalValue = (finalValue, a) => { 42 | a.animation |> Animation.stop; 43 | a.state = {...a.state, finalValue}; 44 | a.animation |> Animation.start; 45 | }; 46 | 47 | let stop = a => a.animation |> Animation.stop; 48 | -------------------------------------------------------------------------------- /src/hooks-animation/HooksSpringAnimation.rei: -------------------------------------------------------------------------------- 1 | type t; 2 | 3 | let create: float => t; 4 | 5 | type onChange = float => unit; 6 | 7 | /** 8 | * Set the onChange function and other parameters of a spring animation. 9 | * The animation is stopped, and only re-started if finalValue is supplied. 10 | */ 11 | let setOnChange: 12 | ( 13 | ~preset: Spring.preset=?, 14 | ~speedup: float=?, 15 | ~precision: float=?, 16 | ~onStop: Animation.onStop=?, 17 | ~onChange: onChange, 18 | ~finalValue: float=?, 19 | t 20 | ) => 21 | unit; 22 | 23 | /** 24 | * Update the final value of the animation, and start it if it was stopped. 25 | */ 26 | let setFinalValue: (float, t) => unit; 27 | 28 | let stop: t => unit; -------------------------------------------------------------------------------- /src/hooks-animation/head0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head0.jpg -------------------------------------------------------------------------------- /src/hooks-animation/head1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head1.jpg -------------------------------------------------------------------------------- /src/hooks-animation/head2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head2.jpg -------------------------------------------------------------------------------- /src/hooks-animation/head3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head3.jpg -------------------------------------------------------------------------------- /src/hooks-animation/head4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head4.jpg -------------------------------------------------------------------------------- /src/hooks-animation/head5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rickyvetter/ocaml-nyc-2019-02/556d92bb7b553d349fe69a986c0138b30fe2df56/src/hooks-animation/head5.jpg -------------------------------------------------------------------------------- /src/hooks-animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Animating With Reducers 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/hooks-animation/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | html, body { 5 | height: 100%; 6 | } 7 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | outline: 0; 12 | font-weight: inherit; 13 | font-style: inherit; 14 | font-family: inherit; 15 | font-size: 100%; 16 | vertical-align: baseline; 17 | } 18 | code, xmp, plaintext, listing { 19 | font-family: monospace; 20 | white-space: pre; 21 | margin: 1em 0px; 22 | line-height: normal; 23 | color: hsla(221, 0%, 31%, 1); 24 | background-color: hsla(221, 0%, 93%, 1); 25 | /* border: 1px solid hsla(221, 48%, 51%, 1); */ 26 | border-radius: 1px; 27 | padding-left: 4px; 28 | padding-right: 4px; 29 | } 30 | pre { 31 | display: block; 32 | font-family: monospace; 33 | white-space: pre; 34 | margin: 1em 0px; 35 | line-height: normal; 36 | } 37 | /* Color scheme for code */ 38 | .Function { color: #4078f2; } 39 | .Conditional { color: #a626a4; } 40 | .Macro { color: #a626a4; } 41 | .Keyword { color: #e45649; } 42 | .StorageClass { color: #c18401; } 43 | .Type { color: #c18401; } 44 | .Comment { color: #a0a1a7; font-style: italic; } 45 | .Constant { color: #50a14f; } 46 | .String { color: #50a14f; } 47 | .Number { color: #986801; } 48 | .Special { color: #4078f2; } 49 | code, pre { 50 | font-family: "Menlo"; 51 | font-size: 12px; 52 | } 53 | pre { 54 | color: #494b53; 55 | } 56 | 57 | body { 58 | -webkit-font-smoothing: antialiased; 59 | text-rendering: optimizeLegibility; 60 | color: #555; 61 | background-color: #fafafa; 62 | } 63 | body, td, textarea, input { 64 | /* overflow-x: hidden; */ 65 | font-family: Helvetica Neue, Open Sans, sans-serif; 66 | line-height: 1.6; 67 | font-size: 13px; 68 | color: #505050; 69 | } 70 | .componentBox { 71 | display: flex; 72 | -webkit-user-select: none; /* Chrome all / Safari all */ 73 | -moz-user-select: none; /* Firefox all */ 74 | -ms-user-select: none; /* IE 10+ */ 75 | user-select: none; /* Likely future */ 76 | border-top: 18px solid hsla(31, 0%, 80%, 0.2); 77 | text-shadow: 0px 1px rgba(250, 250, 250, 0.05); 78 | background-color:hsla(25, 0%, 86%, 0.20); 79 | color: hsla(25, 0%, 53%, 1); 80 | padding: 14px; 81 | } 82 | .componentColumn { 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: flex-start; 86 | align-items: center; 87 | 88 | } 89 | .componentBox pre { 90 | text-shadow: none; 91 | } 92 | .exampleButton { 93 | display: flex; 94 | flex-direction: column; 95 | justify-content: center; 96 | overflow: hidden; 97 | white-space: nowrap; 98 | cursor: pointer; 99 | -webkit-user-select: none; /* Chrome all / Safari all */ 100 | -moz-user-select: none; /* Firefox all */ 101 | -ms-user-select: none; /* IE 10+ */ 102 | user-select: none; /* Likely future */ 103 | background-color: rgba(0,0,0, 0.05); 104 | text-align: center; 105 | } 106 | .exampleButton:hover { 107 | background-color: rgba(0,0,0, 0.1); 108 | } 109 | .exampleButton:active { 110 | background-color: rgba(0,0,0, 0.12); 111 | } 112 | .exampleButton.small { 113 | font-size: 12px; 114 | } 115 | .exampleButton.medium { 116 | font-size: 13px; 117 | } 118 | .exampleButton.large { 119 | font-size: 15px; 120 | } 121 | 122 | .block { 123 | display: block; 124 | } 125 | .megaHeader { 126 | max-width: 600px; 127 | font-weight: bold; 128 | padding: 10px 0; 129 | text-transform: uppercase; 130 | font-size: 2.8em; 131 | letter-spacing: 1px; 132 | } 133 | .megaHeaderSubtext { 134 | max-width: 600px; 135 | font-size: 1.3em; 136 | font-family: Open Sans, sans-serif; 137 | font-weight: 300; 138 | margin-bottom: 40px; 139 | } 140 | .megaHeaderSubtextDetails { 141 | max-width: 600px; 142 | margin-bottom: 40px; 143 | } 144 | .header { 145 | max-width: 600px; 146 | border-left: 4px solid hsl(31, 0%, 79%); 147 | padding-left: 14px; 148 | font-weight: 500; 149 | text-transform: uppercase; 150 | padding-bottom: 20px; 151 | font-size: 1.4em; 152 | letter-spacing: 1px; 153 | } 154 | .headerSubtext { 155 | max-width: 600px; 156 | border-left: 4px solid hsl(31, 0%, 79%); 157 | padding-left: 14px; 158 | padding-bottom: 8px; 159 | margin-bottom: 30px; 160 | } 161 | .headerSubtext p { 162 | margin-bottom: 1em; 163 | } 164 | .mainGallery { 165 | padding: 50px; 166 | } 167 | .stateLogger { 168 | font-family: Monospace; 169 | font-family: "Menlo"; 170 | font-size: 12px; 171 | padding: 8px; 172 | margin: 8px; 173 | font-weight: bold; 174 | } 175 | .galleryItem { 176 | margin-bottom: 60px; 177 | } 178 | .galleryItemDemo { 179 | margin-bottom: 60px; 180 | margin-top: 14px; 181 | } 182 | .leftRightContainer { 183 | display: flex; 184 | flex-wrap: wrap; 185 | } 186 | .left { 187 | flex: 1; 188 | /* For when it breaks */ 189 | margin-bottom: 18px; 190 | } 191 | .right { 192 | flex: 1; 193 | margin-left: 18px; 194 | margin-bottom: 18px; 195 | } 196 | .sourceContainer { 197 | border-top: 18px solid hsla(31, 0%, 80%, 0.2); 198 | /* Header left border + padding-left */ 199 | margin-left: 18px; 200 | background-color:hsla(25, 0%, 86%, 0.20); 201 | padding: 14px; 202 | } 203 | .fileName { 204 | position: absolute; 205 | font-size: .9em; 206 | top: -32px; 207 | left: -8px; 208 | color: #bbb; 209 | } 210 | .interactionContainer { 211 | /* Header left border + padding-left */ 212 | margin-left: 18px; 213 | } 214 | .photo-outer { 215 | overflow: hidden; 216 | position: relative; 217 | margin: auto; 218 | } 219 | .photo-inner { 220 | position: absolute; 221 | } 222 | .chat-head { 223 | border-radius: 99px; 224 | background-color: white; 225 | width: 50px; 226 | height: 50px; 227 | border: 3px solid white; 228 | position: absolute; 229 | background-size: 50px; 230 | } 231 | .chat-head-image-gallery { 232 | position: absolute; 233 | transform: translate(-50px, -50px) scale(0.7) ; 234 | } 235 | 236 | .chat-head-0 { 237 | background-image: url(head0.jpg); 238 | } 239 | .chat-head-1 { 240 | background-image: url(head1.jpg); 241 | } 242 | .chat-head-2 { 243 | background-image: url(head2.jpg); 244 | } 245 | .chat-head-3 { 246 | background-image: url(head3.jpg); 247 | } 248 | .chat-head-4 { 249 | background-image: url(head4.jpg); 250 | } 251 | .chat-head-5 { 252 | background-image: url(head5.jpg); 253 | } 254 | 255 | -------------------------------------------------------------------------------- /src/hooks/HooksPage.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require("react"); 4 | 5 | function handleClick() { 6 | console.log("clicked!"); 7 | return /* () */0; 8 | } 9 | 10 | function make(Props) { 11 | var message = Props.message; 12 | React.useEffect((function () { 13 | console.log("Hey!"); 14 | return undefined; 15 | })); 16 | return React.createElement("button", { 17 | onClick: handleClick 18 | }, message); 19 | } 20 | 21 | exports.handleClick = handleClick; 22 | exports.make = make; 23 | /* react Not a pure module */ 24 | -------------------------------------------------------------------------------- /src/hooks/HooksPage.re: -------------------------------------------------------------------------------- 1 | /* This is your familiar handleClick from ReactJS. This mandatorily takes the payload, 2 | then the `self` record, which contains state (none here), `handle`, `send` 3 | and other utilities */ 4 | let handleClick = _event => Js.log("clicked!"); 5 | 6 | /* Which desugars to 7 | 8 | `let make = ({message}) => ...` */ 9 | [@react.component] 10 | let make = (~message, ()) => { 11 | React.useEffect(() => { 12 | Js.log("Hey!"); 13 | None; 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /src/hooks/HooksRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require("react"); 4 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 5 | var HooksPage$ReasonReactExample = require("./HooksPage.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId( 8 | React.createElement(HooksPage$ReasonReactExample.make, { 9 | message: "Hello!" 10 | }), "index"); 11 | 12 | /* Not a pure module */ 13 | -------------------------------------------------------------------------------- /src/hooks/HooksRoot.re: -------------------------------------------------------------------------------- 1 | [@bs.config {jsx: 3}]; 2 | 3 | 4 | /* Desugars to 5 | 6 | `React.createElement(HooksPage.make, HooksPage.makeProps(~message="hello", ()))` */ 7 | ReactDOMRe.renderToElementWithId(, "index"); 8 | -------------------------------------------------------------------------------- /src/hooks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pure Reason Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reason React Examples 6 | 7 | 8 |

Reason React Examples

9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/interop/GreetingRe.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require("react"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 6 | var MyBannerRe$ReasonReactExample = require("./MyBannerRe.bs.js"); 7 | 8 | var component = ReasonReact.statelessComponent("PageReason"); 9 | 10 | function make(message, extraGreeting, _) { 11 | return /* record */[ 12 | /* debugName */component[/* debugName */0], 13 | /* reactClassInternal */component[/* reactClassInternal */1], 14 | /* handedOffState */component[/* handedOffState */2], 15 | /* willReceiveProps */component[/* willReceiveProps */3], 16 | /* didMount */component[/* didMount */4], 17 | /* didUpdate */component[/* didUpdate */5], 18 | /* willUnmount */component[/* willUnmount */6], 19 | /* willUpdate */component[/* willUpdate */7], 20 | /* shouldUpdate */component[/* shouldUpdate */8], 21 | /* render */(function () { 22 | var greeting = extraGreeting !== undefined ? extraGreeting : "How are you?"; 23 | return React.createElement("div", undefined, ReasonReact.element(undefined, undefined, MyBannerRe$ReasonReactExample.make(true, message + (" " + greeting), /* array */[]))); 24 | }), 25 | /* initialState */component[/* initialState */10], 26 | /* retainedProps */component[/* retainedProps */11], 27 | /* reducer */component[/* reducer */12], 28 | /* jsElementWrapped */component[/* jsElementWrapped */13] 29 | ]; 30 | } 31 | 32 | var jsComponent = ReasonReact.wrapReasonForJs(component, (function (jsProps) { 33 | return make(jsProps.message, Js_primitive.nullable_to_opt(jsProps.extraGreeting), jsProps.children); 34 | })); 35 | 36 | exports.component = component; 37 | exports.make = make; 38 | exports.jsComponent = jsComponent; 39 | /* component Not a pure module */ 40 | -------------------------------------------------------------------------------- /src/interop/GreetingRe.re: -------------------------------------------------------------------------------- 1 | /* ReasonReact used by ReactJS */ 2 | /* This is just a normal stateless component. The only change you need to turn 3 | it into a ReactJS-compatible component is the wrapReasonForJs call below */ 4 | let component = ReasonReact.statelessComponent("PageReason"); 5 | 6 | let make = (~message, ~extraGreeting=?, _children) => { 7 | ...component, 8 | render: _self => { 9 | let greeting = 10 | switch (extraGreeting) { 11 | | None => "How are you?" 12 | | Some(g) => g 13 | }; 14 |
; 15 | }, 16 | }; 17 | 18 | /* The following exposes a `jsComponent` that the ReactJS side can use as 19 | require('greetingRe.js').jsComponent */ 20 | [@bs.deriving abstract] 21 | type jsProps = { 22 | message: string, 23 | extraGreeting: Js.nullable(string), 24 | children: array(ReasonReact.reactElement), 25 | }; 26 | 27 | /* if **you know what you're doing** and have 28 | the correct babel/webpack setup, you can also do `let default = ...` and use it 29 | on the JS side as a default export. */ 30 | let jsComponent = 31 | ReasonReact.wrapReasonForJs(~component, jsProps => 32 | make( 33 | ~message=jsProps->messageGet, 34 | ~extraGreeting=?Js.Nullable.toOption(jsProps->extraGreetingGet), 35 | jsProps->childrenGet, 36 | ) 37 | ); 38 | -------------------------------------------------------------------------------- /src/interop/InteropRoot.js: -------------------------------------------------------------------------------- 1 | var ReactDOM = require('react-dom'); 2 | var React = require('react'); 3 | 4 | // Import a ReasonReact component! `jsComponent` is the exposed, underlying ReactJS class 5 | var PageReason = require('./GreetingRe.bs').jsComponent; 6 | 7 | var App = function() { 8 | return React.createElement('div', null, 9 | React.createElement(PageReason, {message: 'Hello!'}) 10 | ); 11 | // didn't feel like dragging in Babel. Here's the equivalent JSX: 12 | //
13 | }; 14 | App.displayName = 'ExampleInteropRoot'; 15 | 16 | ReactDOM.render(React.createElement(App), document.getElementById('index')); 17 | -------------------------------------------------------------------------------- /src/interop/MyBanner.js: -------------------------------------------------------------------------------- 1 | // This file isn't used directly by JS; it's used to myBanner.re, which is then 2 | // used by the ReasonReact component GreetingRe. 3 | 4 | var ReactDOM = require('react-dom'); 5 | var React = require('react'); 6 | 7 | var App = function(props) { 8 | if (props.show) { 9 | return React.createElement('div', null, 10 | 'Here\'s the message from the owner: ' + props.message 11 | ); 12 | } else { 13 | return null; 14 | } 15 | }; 16 | App.displayName = "MyBanner"; 17 | 18 | module.exports = App; 19 | -------------------------------------------------------------------------------- /src/interop/MyBannerRe.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MyBanner = require("./MyBanner"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | 6 | function make(show, message, children) { 7 | return ReasonReact.wrapJsForReason(MyBanner, { 8 | show: show, 9 | message: message 10 | }, children); 11 | } 12 | 13 | exports.make = make; 14 | /* ./MyBanner Not a pure module */ 15 | -------------------------------------------------------------------------------- /src/interop/MyBannerRe.re: -------------------------------------------------------------------------------- 1 | /* ReactJS used by ReasonReact */ 2 | /* This component wraps a ReactJS one, so that ReasonReact components can consume it */ 3 | /* Typing the myBanner.js component's output as a `reactClass`. */ 4 | [@bs.module] external myBanner: ReasonReact.reactClass = "./MyBanner"; 5 | 6 | [@bs.deriving abstract] 7 | type jsProps = { 8 | show: bool, 9 | message: string, 10 | }; 11 | 12 | /* This is like declaring a normal ReasonReact component's `make` function, except the body is a the interop hook wrapJsForReason */ 13 | let make = (~show, ~message, children) => 14 | ReasonReact.wrapJsForReason( 15 | ~reactClass=myBanner, 16 | ~props=jsProps(~show, ~message), 17 | children, 18 | ); 19 | -------------------------------------------------------------------------------- /src/interop/README.md: -------------------------------------------------------------------------------- 1 | ## Interoperate with Existing ReactJS Components 2 | 3 | This subdirectory demonstrate the ReasonReact <-> ReactJS interop APIs. 4 | 5 | The entry point, `InteropRoot.js`, illustrates ReactJS requiring a ReasonReact component, `GreetingRe`. 6 | 7 | `GreetingRe` itself illustrates ReasonReact 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/retainedProps/RetainedPropsExample.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require("react"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | 6 | var component = ReasonReact.statelessComponentWithRetainedProps("RetainedPropsExample"); 7 | 8 | function make(message, _) { 9 | return /* record */[ 10 | /* debugName */component[/* debugName */0], 11 | /* reactClassInternal */component[/* reactClassInternal */1], 12 | /* handedOffState */component[/* handedOffState */2], 13 | /* willReceiveProps */component[/* willReceiveProps */3], 14 | /* didMount */component[/* didMount */4], 15 | /* didUpdate */(function (param) { 16 | if (param[/* oldSelf */0][/* retainedProps */2][/* message */0] !== param[/* newSelf */1][/* retainedProps */2][/* message */0]) { 17 | console.log("props `message` changed!"); 18 | return /* () */0; 19 | } else { 20 | return 0; 21 | } 22 | }), 23 | /* willUnmount */component[/* willUnmount */6], 24 | /* willUpdate */component[/* willUpdate */7], 25 | /* shouldUpdate */component[/* shouldUpdate */8], 26 | /* render */(function () { 27 | return React.createElement("div", undefined, message); 28 | }), 29 | /* initialState */component[/* initialState */10], 30 | /* retainedProps : record */[/* message */message], 31 | /* reducer */component[/* reducer */12], 32 | /* jsElementWrapped */component[/* jsElementWrapped */13] 33 | ]; 34 | } 35 | 36 | exports.component = component; 37 | exports.make = make; 38 | /* component Not a pure module */ 39 | -------------------------------------------------------------------------------- /src/retainedProps/RetainedPropsExample.re: -------------------------------------------------------------------------------- 1 | /* The component's retainedProps type. It can be anything, including, commonly, being a record type */ 2 | /* retainedProps allows you to access the previous props information, like how ReactJS does it for you in lifecycle events */ 3 | type retainedProps = {message: string}; 4 | 5 | let component = 6 | ReasonReact.statelessComponentWithRetainedProps("RetainedPropsExample"); 7 | 8 | let make = (~message, _children) => { 9 | ...component, 10 | retainedProps: { 11 | message: message, 12 | }, 13 | didUpdate: ({oldSelf, newSelf}) => 14 | if (oldSelf.retainedProps.message !== newSelf.retainedProps.message) { 15 | Js.log("props `message` changed!"); 16 | }, 17 | render: _self =>
(ReasonReact.string(message))
, 18 | /* do whatever sneaky imperative things here */ 19 | }; 20 | -------------------------------------------------------------------------------- /src/retainedProps/RetainedPropsRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var RetainedPropsExample$ReasonReactExample = require("./RetainedPropsExample.bs.js"); 6 | 7 | var toggle = /* record */[/* contents */false]; 8 | 9 | function render() { 10 | toggle[0] = !toggle[/* contents */0]; 11 | var match = toggle[/* contents */0]; 12 | return ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, RetainedPropsExample$ReasonReactExample.make(match ? "Hello!" : "Goodbye", /* array */[])), "index"); 13 | } 14 | 15 | setInterval(render, 1000); 16 | 17 | render(/* () */0); 18 | 19 | exports.toggle = toggle; 20 | exports.render = render; 21 | /* Not a pure module */ 22 | -------------------------------------------------------------------------------- /src/retainedProps/RetainedPropsRoot.re: -------------------------------------------------------------------------------- 1 | let toggle = ref(false); 2 | 3 | let render = () => { 4 | toggle := !toggle.contents; 5 | ReactDOMRe.renderToElementWithId( 6 | , 7 | "index", 8 | ); 9 | }; 10 | 11 | Js.Global.setInterval(render, 1000); 12 | 13 | /* render once first! */ 14 | render(); 15 | -------------------------------------------------------------------------------- /src/retainedProps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pure Reason Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/simple/Page.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Curry = require("bs-platform/lib/js/curry.js"); 4 | var React = require("react"); 5 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 6 | 7 | var component = ReasonReact.statelessComponent("Page"); 8 | 9 | function handleClick(_, _$1) { 10 | console.log("clicked!"); 11 | return /* () */0; 12 | } 13 | 14 | function make(message, _) { 15 | return /* record */[ 16 | /* debugName */component[/* debugName */0], 17 | /* reactClassInternal */component[/* reactClassInternal */1], 18 | /* handedOffState */component[/* handedOffState */2], 19 | /* willReceiveProps */component[/* willReceiveProps */3], 20 | /* didMount */component[/* didMount */4], 21 | /* didUpdate */component[/* didUpdate */5], 22 | /* willUnmount */component[/* willUnmount */6], 23 | /* willUpdate */component[/* willUpdate */7], 24 | /* shouldUpdate */component[/* shouldUpdate */8], 25 | /* render */(function (self) { 26 | return React.createElement("button", { 27 | onClick: Curry._1(self[/* handle */0], handleClick) 28 | }, message); 29 | }), 30 | /* initialState */component[/* initialState */10], 31 | /* retainedProps */component[/* retainedProps */11], 32 | /* reducer */component[/* reducer */12], 33 | /* jsElementWrapped */component[/* jsElementWrapped */13] 34 | ]; 35 | } 36 | 37 | exports.component = component; 38 | exports.handleClick = handleClick; 39 | exports.make = make; 40 | /* component Not a pure module */ 41 | -------------------------------------------------------------------------------- /src/simple/Page.re: -------------------------------------------------------------------------------- 1 | /* This is the basic component. */ 2 | let component = ReasonReact.statelessComponent("Page"); 3 | 4 | /* This is your familiar handleClick from ReactJS. This mandatorily takes the payload, 5 | then the `self` record, which contains state (none here), `handle`, `send` 6 | and other utilities */ 7 | let handleClick = (_event, _self) => Js.log("clicked!"); 8 | 9 | /* `make` is the function that mandatorily takes `children` (if you want to use 10 | `JSX). `message` is a named argument, which simulates ReactJS props. Usage: 11 | `` 12 | Which desugars to 13 | `ReasonReact.element (Page.make message::"hello" [||])` */ 14 | let make = (~message, _children) => { 15 | ...component, 16 | render: self => 17 | , 20 | }; -------------------------------------------------------------------------------- /src/simple/SimpleRoot.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | var Page$ReasonReactExample = require("./Page.bs.js"); 6 | 7 | ReactDOMRe.renderToElementWithId(ReasonReact.element(undefined, undefined, Page$ReasonReactExample.make("Hello!", /* array */[])), "index"); 8 | 9 | /* Not a pure module */ 10 | -------------------------------------------------------------------------------- /src/simple/SimpleRoot.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/todomvc/App.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Block = require("bs-platform/lib/js/block.js"); 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var React = require("react"); 6 | var $$String = require("bs-platform/lib/js/string.js"); 7 | var Caml_obj = require("bs-platform/lib/js/caml_obj.js"); 8 | var Belt_List = require("bs-platform/lib/js/belt_List.js"); 9 | var Pervasives = require("bs-platform/lib/js/pervasives.js"); 10 | var ReactDOMRe = require("reason-react/src/ReactDOMRe.js"); 11 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 12 | var TodoItem$ReasonReactExample = require("./TodoItem.bs.js"); 13 | var TodoFooter$ReasonReactExample = require("./TodoFooter.bs.js"); 14 | 15 | var localStorageNamespace = "reason-react-todos"; 16 | 17 | function saveLocally(todos) { 18 | var match = JSON.stringify(todos); 19 | if (match !== undefined) { 20 | localStorage.setItem(localStorageNamespace, match); 21 | return /* () */0; 22 | } else { 23 | return /* () */0; 24 | } 25 | } 26 | 27 | function urlToShownPage(hash) { 28 | switch (hash) { 29 | case "active" : 30 | return /* ActiveTodos */1; 31 | case "completed" : 32 | return /* CompletedTodos */2; 33 | default: 34 | return /* AllTodos */0; 35 | } 36 | } 37 | 38 | var component = ReasonReact.reducerComponent("TodoAppRe"); 39 | 40 | function make() { 41 | return /* record */[ 42 | /* debugName */component[/* debugName */0], 43 | /* reactClassInternal */component[/* reactClassInternal */1], 44 | /* handedOffState */component[/* handedOffState */2], 45 | /* willReceiveProps */component[/* willReceiveProps */3], 46 | /* didMount */(function (self) { 47 | var token = ReasonReact.Router[/* watchUrl */1]((function (url) { 48 | return Curry._1(self[/* send */3], /* Navigate */Block.__(0, [urlToShownPage(url[/* hash */1])])); 49 | })); 50 | return Curry._1(self[/* onUnmount */4], (function () { 51 | return ReasonReact.Router[/* unwatchUrl */2](token); 52 | })); 53 | }), 54 | /* didUpdate */component[/* didUpdate */5], 55 | /* willUnmount */component[/* willUnmount */6], 56 | /* willUpdate */component[/* willUpdate */7], 57 | /* shouldUpdate */component[/* shouldUpdate */8], 58 | /* render */(function (param) { 59 | var state = param[/* state */1]; 60 | var todos = state[/* todos */3]; 61 | var editing = state[/* editing */1]; 62 | var send = param[/* send */3]; 63 | var __x = Belt_List.keep(todos, (function (todo) { 64 | var match = state[/* nowShowing */0]; 65 | switch (match) { 66 | case 0 : 67 | return true; 68 | case 1 : 69 | return !todo[/* completed */2]; 70 | case 2 : 71 | return todo[/* completed */2]; 72 | 73 | } 74 | })); 75 | var todoItems = Belt_List.map(__x, (function (todo) { 76 | var editing$1 = editing !== undefined ? editing === todo[/* id */0] : false; 77 | return ReasonReact.element(todo[/* id */0], undefined, TodoItem$ReasonReactExample.make(todo, editing$1, (function () { 78 | return Curry._1(send, /* Destroy */Block.__(4, [todo])); 79 | }), (function (text) { 80 | return Curry._1(send, /* Save */Block.__(2, [ 81 | todo, 82 | text 83 | ])); 84 | }), (function () { 85 | return Curry._1(send, /* Edit */Block.__(3, [todo])); 86 | }), (function () { 87 | return Curry._1(send, /* Toggle */Block.__(5, [todo])); 88 | }), (function () { 89 | return Curry._1(send, /* Cancel */3); 90 | }), /* array */[])); 91 | })); 92 | var todosLength = Belt_List.length(todos); 93 | var completedCount = Belt_List.length(Belt_List.keep(todos, (function (todo) { 94 | return todo[/* completed */2]; 95 | }))); 96 | var activeTodoCount = todosLength - completedCount | 0; 97 | var footer; 98 | var exit = 0; 99 | if (activeTodoCount !== 0 || completedCount !== 0) { 100 | exit = 1; 101 | } else { 102 | footer = null; 103 | } 104 | if (exit === 1) { 105 | footer = ReasonReact.element(undefined, undefined, TodoFooter$ReasonReactExample.make(activeTodoCount, completedCount, state[/* nowShowing */0], (function () { 106 | return Curry._1(send, /* ClearCompleted */2); 107 | }), /* array */[])); 108 | } 109 | var match = todosLength === 0; 110 | var main = match ? null : React.createElement("section", { 111 | className: "main" 112 | }, React.createElement("input", { 113 | className: "toggle-all", 114 | checked: activeTodoCount === 0, 115 | type: "checkbox", 116 | onChange: (function ($$event) { 117 | var checked = $$event.target.checked; 118 | return Curry._1(send, /* ToggleAll */Block.__(6, [checked])); 119 | }) 120 | }), React.createElement("ul", { 121 | className: "todo-list" 122 | }, Belt_List.toArray(todoItems))); 123 | return React.createElement("div", undefined, React.createElement("header", { 124 | className: "header" 125 | }, React.createElement("h1", undefined, "todos"), React.createElement("input", { 126 | className: "new-todo", 127 | autoFocus: true, 128 | placeholder: "What needs to be done?", 129 | value: state[/* newTodo */2], 130 | onKeyDown: (function ($$event) { 131 | if ($$event.keyCode === 13) { 132 | $$event.preventDefault(); 133 | return Curry._1(send, /* NewTodoEnterKeyDown */0); 134 | } else { 135 | return Curry._1(send, /* NewTodoOtherKeyDown */1); 136 | } 137 | }), 138 | onChange: (function ($$event) { 139 | return Curry._1(send, /* ChangeTodo */Block.__(1, [$$event.target.value])); 140 | }) 141 | })), main, footer); 142 | }), 143 | /* initialState */(function () { 144 | var match = localStorage.getItem(localStorageNamespace); 145 | var todos = match !== null ? JSON.parse(match) : /* [] */0; 146 | return /* record */[ 147 | /* nowShowing */urlToShownPage(ReasonReact.Router[/* dangerouslyGetInitialUrl */3](/* () */0)[/* hash */1]), 148 | /* editing */undefined, 149 | /* newTodo */"", 150 | /* todos */todos 151 | ]; 152 | }), 153 | /* retainedProps */component[/* retainedProps */11], 154 | /* reducer */(function (action, state) { 155 | if (typeof action === "number") { 156 | switch (action) { 157 | case 0 : 158 | var nonEmptyValue = $$String.trim(state[/* newTodo */2]); 159 | if (nonEmptyValue === "") { 160 | return /* NoUpdate */0; 161 | } else { 162 | var todos = Pervasives.$at(state[/* todos */3], /* :: */[ 163 | /* record */[ 164 | /* id */Pervasives.string_of_float(Date.now()), 165 | /* title */nonEmptyValue, 166 | /* completed */false 167 | ], 168 | /* [] */0 169 | ]); 170 | saveLocally(todos); 171 | return /* Update */Block.__(0, [/* record */[ 172 | /* nowShowing */state[/* nowShowing */0], 173 | /* editing */state[/* editing */1], 174 | /* newTodo */"", 175 | /* todos */todos 176 | ]]); 177 | } 178 | case 1 : 179 | return /* NoUpdate */0; 180 | case 2 : 181 | var todos$1 = Belt_List.keep(state[/* todos */3], (function (todo) { 182 | return !todo[/* completed */2]; 183 | })); 184 | return /* UpdateWithSideEffects */Block.__(2, [ 185 | /* record */[ 186 | /* nowShowing */state[/* nowShowing */0], 187 | /* editing */state[/* editing */1], 188 | /* newTodo */state[/* newTodo */2], 189 | /* todos */todos$1 190 | ], 191 | (function () { 192 | return saveLocally(todos$1); 193 | }) 194 | ]); 195 | case 3 : 196 | return /* Update */Block.__(0, [/* record */[ 197 | /* nowShowing */state[/* nowShowing */0], 198 | /* editing */undefined, 199 | /* newTodo */state[/* newTodo */2], 200 | /* todos */state[/* todos */3] 201 | ]]); 202 | 203 | } 204 | } else { 205 | switch (action.tag | 0) { 206 | case 0 : 207 | return /* Update */Block.__(0, [/* record */[ 208 | /* nowShowing */action[0], 209 | /* editing */state[/* editing */1], 210 | /* newTodo */state[/* newTodo */2], 211 | /* todos */state[/* todos */3] 212 | ]]); 213 | case 1 : 214 | return /* Update */Block.__(0, [/* record */[ 215 | /* nowShowing */state[/* nowShowing */0], 216 | /* editing */state[/* editing */1], 217 | /* newTodo */action[0], 218 | /* todos */state[/* todos */3] 219 | ]]); 220 | case 2 : 221 | var text = action[1]; 222 | var todoToSave = action[0]; 223 | var todos$2 = Belt_List.map(state[/* todos */3], (function (todo) { 224 | var match = Caml_obj.caml_equal(todo, todoToSave); 225 | if (match) { 226 | return /* record */[ 227 | /* id */todo[/* id */0], 228 | /* title */text, 229 | /* completed */todo[/* completed */2] 230 | ]; 231 | } else { 232 | return todo; 233 | } 234 | })); 235 | return /* UpdateWithSideEffects */Block.__(2, [ 236 | /* record */[ 237 | /* nowShowing */state[/* nowShowing */0], 238 | /* editing */undefined, 239 | /* newTodo */state[/* newTodo */2], 240 | /* todos */todos$2 241 | ], 242 | (function () { 243 | return saveLocally(todos$2); 244 | }) 245 | ]); 246 | case 3 : 247 | return /* Update */Block.__(0, [/* record */[ 248 | /* nowShowing */state[/* nowShowing */0], 249 | /* editing */action[0][/* id */0], 250 | /* newTodo */state[/* newTodo */2], 251 | /* todos */state[/* todos */3] 252 | ]]); 253 | case 4 : 254 | var todo = action[0]; 255 | var todos$3 = Belt_List.keep(state[/* todos */3], (function (candidate) { 256 | return candidate !== todo; 257 | })); 258 | return /* UpdateWithSideEffects */Block.__(2, [ 259 | /* record */[ 260 | /* nowShowing */state[/* nowShowing */0], 261 | /* editing */state[/* editing */1], 262 | /* newTodo */state[/* newTodo */2], 263 | /* todos */todos$3 264 | ], 265 | (function () { 266 | return saveLocally(todos$3); 267 | }) 268 | ]); 269 | case 5 : 270 | var todoToToggle = action[0]; 271 | var todos$4 = Belt_List.map(state[/* todos */3], (function (todo) { 272 | var match = Caml_obj.caml_equal(todo, todoToToggle); 273 | if (match) { 274 | return /* record */[ 275 | /* id */todo[/* id */0], 276 | /* title */todo[/* title */1], 277 | /* completed */!todo[/* completed */2] 278 | ]; 279 | } else { 280 | return todo; 281 | } 282 | })); 283 | return /* UpdateWithSideEffects */Block.__(2, [ 284 | /* record */[ 285 | /* nowShowing */state[/* nowShowing */0], 286 | /* editing */state[/* editing */1], 287 | /* newTodo */state[/* newTodo */2], 288 | /* todos */todos$4 289 | ], 290 | (function () { 291 | return saveLocally(todos$4); 292 | }) 293 | ]); 294 | case 6 : 295 | var checked = action[0]; 296 | var todos$5 = Belt_List.map(state[/* todos */3], (function (todo) { 297 | return /* record */[ 298 | /* id */todo[/* id */0], 299 | /* title */todo[/* title */1], 300 | /* completed */checked 301 | ]; 302 | })); 303 | return /* UpdateWithSideEffects */Block.__(2, [ 304 | /* record */[ 305 | /* nowShowing */state[/* nowShowing */0], 306 | /* editing */state[/* editing */1], 307 | /* newTodo */state[/* newTodo */2], 308 | /* todos */todos$5 309 | ], 310 | (function () { 311 | return saveLocally(todos$5); 312 | }) 313 | ]); 314 | 315 | } 316 | } 317 | }), 318 | /* jsElementWrapped */component[/* jsElementWrapped */13] 319 | ]; 320 | } 321 | 322 | var Top = /* module */[ 323 | /* urlToShownPage */urlToShownPage, 324 | /* component */component, 325 | /* make */make 326 | ]; 327 | 328 | ReactDOMRe.renderToElementWithClassName(ReasonReact.element(undefined, undefined, make(/* array */[])), "todoapp"); 329 | 330 | exports.localStorageNamespace = localStorageNamespace; 331 | exports.saveLocally = saveLocally; 332 | exports.Top = Top; 333 | /* component Not a pure module */ 334 | -------------------------------------------------------------------------------- /src/todomvc/App.re: -------------------------------------------------------------------------------- 1 | /* The new stdlib additions */ 2 | open Belt; 3 | 4 | [@bs.val] external unsafeJsonParse: string => 'a = "JSON.parse"; 5 | 6 | let localStorageNamespace = "reason-react-todos"; 7 | 8 | let saveLocally = todos => 9 | switch (Js.Json.stringifyAny(todos)) { 10 | | None => () 11 | | Some(stringifiedTodos) => 12 | Dom.Storage.( 13 | localStorage |> setItem(localStorageNamespace, stringifiedTodos) 14 | ) 15 | }; 16 | 17 | module Top = { 18 | type action = 19 | | Navigate(TodoFooter.showingState) 20 | /* todo actions */ 21 | | NewTodoEnterKeyDown 22 | | NewTodoOtherKeyDown 23 | | ClearCompleted 24 | | Cancel 25 | | ChangeTodo(string) 26 | | Save(TodoItem.todo, string) 27 | | Edit(TodoItem.todo) 28 | | Destroy(TodoItem.todo) 29 | | Toggle(TodoItem.todo) 30 | | ToggleAll(bool); 31 | type state = { 32 | nowShowing: TodoFooter.showingState, 33 | editing: option(string), 34 | newTodo: string, 35 | todos: list(TodoItem.todo), 36 | }; 37 | let urlToShownPage = hash => 38 | switch (hash) { 39 | | "active" => TodoFooter.ActiveTodos 40 | | "completed" => CompletedTodos 41 | | _ => AllTodos 42 | }; 43 | let component = ReasonReact.reducerComponent("TodoAppRe"); 44 | let make = _children => { 45 | ...component, 46 | reducer: (action, state) => 47 | switch (action) { 48 | | Navigate(page) => ReasonReact.Update({...state, nowShowing: page}) 49 | | Cancel => ReasonReact.Update({...state, editing: None}) 50 | | ChangeTodo(text) => ReasonReact.Update({...state, newTodo: text}) 51 | | NewTodoOtherKeyDown => ReasonReact.NoUpdate 52 | | NewTodoEnterKeyDown => 53 | switch (String.trim(state.newTodo)) { 54 | | "" => ReasonReact.NoUpdate 55 | | nonEmptyValue => 56 | let todos = 57 | state.todos 58 | @ [ 59 | { 60 | id: string_of_float(Js.Date.now()), 61 | title: nonEmptyValue, 62 | completed: false, 63 | }, 64 | ]; 65 | saveLocally(todos); 66 | ReasonReact.Update({...state, newTodo: "", todos}); 67 | } 68 | | ClearCompleted => 69 | let todos = List.keep(state.todos, todo => !TodoItem.(todo.completed)); 70 | ReasonReact.UpdateWithSideEffects( 71 | {...state, todos}, 72 | (_self => saveLocally(todos)), 73 | ); 74 | | ToggleAll(checked) => 75 | let todos = 76 | List.map(state.todos, todo => 77 | {...todo, TodoItem.completed: checked} 78 | ); 79 | ReasonReact.UpdateWithSideEffects( 80 | {...state, todos}, 81 | (_self => saveLocally(todos)), 82 | ); 83 | | Save(todoToSave, text) => 84 | let todos = 85 | List.map(state.todos, todo => 86 | todo == todoToSave ? {...todo, TodoItem.title: text} : todo 87 | ); 88 | ReasonReact.UpdateWithSideEffects( 89 | {...state, editing: None, todos}, 90 | (_self => saveLocally(todos)), 91 | ); 92 | | Edit(todo) => 93 | ReasonReact.Update({...state, editing: Some(TodoItem.(todo.id))}) 94 | | Destroy(todo) => 95 | let todos = List.keep(state.todos, candidate => candidate !== todo); 96 | ReasonReact.UpdateWithSideEffects( 97 | {...state, todos}, 98 | (_self => saveLocally(todos)), 99 | ); 100 | | Toggle(todoToToggle) => 101 | let todos = 102 | List.map(state.todos, todo => 103 | todo == todoToToggle ? 104 | {...todo, TodoItem.completed: !TodoItem.(todo.completed)} : todo 105 | ); 106 | ReasonReact.UpdateWithSideEffects( 107 | {...state, todos}, 108 | (_self => saveLocally(todos)), 109 | ); 110 | }, 111 | initialState: () => { 112 | let todos = 113 | switch (Dom.Storage.(localStorage |> getItem(localStorageNamespace))) { 114 | | None => [] 115 | | Some(todos) => unsafeJsonParse(todos) 116 | }; 117 | { 118 | nowShowing: 119 | urlToShownPage(ReasonReact.Router.dangerouslyGetInitialUrl().hash), 120 | editing: None, 121 | newTodo: "", 122 | todos, 123 | }; 124 | }, 125 | didMount: self => { 126 | let token = 127 | ReasonReact.Router.watchUrl(url => 128 | self.send(Navigate(urlToShownPage(url.hash))) 129 | ); 130 | self.onUnmount(() => ReasonReact.Router.unwatchUrl(token)); 131 | }, 132 | /* router actions */ 133 | render: ({state, send}) => { 134 | let {todos, editing} = state; 135 | let todoItems = 136 | List.keep(todos, todo => 137 | TodoItem.( 138 | switch (state.nowShowing) { 139 | | ActiveTodos => !todo.completed 140 | | CompletedTodos => todo.completed 141 | | AllTodos => true 142 | } 143 | ) 144 | ) 145 | |> List.map( 146 | _, 147 | todo => { 148 | let editing = 149 | switch (editing) { 150 | | None => false 151 | | Some(editing) => editing === TodoItem.(todo.id) 152 | }; 153 | send(Toggle(todo))) 157 | onDestroy=(_event => send(Destroy(todo))) 158 | onEdit=(_event => send(Edit(todo))) 159 | editing 160 | onSave=(text => send(Save(todo, text))) 161 | onCancel=(_event => send(Cancel)) 162 | />; 163 | }, 164 | ); 165 | let todosLength = List.length(todos); 166 | let completedCount = 167 | List.keep(todos, todo => TodoItem.(todo.completed)) |> List.length; 168 | let activeTodoCount = todosLength - completedCount; 169 | let footer = 170 | switch (activeTodoCount, completedCount) { 171 | | (0, 0) => ReasonReact.null 172 | | _ => 173 | send(ClearCompleted)) 178 | /> 179 | }; 180 | let main = 181 | todosLength === 0 ? 182 | ReasonReact.null : 183 |
184 | { 189 | let checked = ReactEvent.Form.target(event)##checked; 190 | send(ToggleAll(checked)); 191 | } 192 | ) 193 | checked=(activeTodoCount === 0) 194 | /> 195 |
    196 | (ReasonReact.array(List.toArray(todoItems))) 197 |
198 |
; 199 |
200 |
201 |

(ReasonReact.string("todos"))

202 | 208 | if (ReactEvent.Keyboard.keyCode(event) === 13) { 209 | ReactEvent.Keyboard.preventDefault(event); 210 | send(NewTodoEnterKeyDown); 211 | } else { 212 | send(NewTodoOtherKeyDown); 213 | } 214 | ) 215 | onChange=( 216 | event => 217 | send(ChangeTodo(ReactEvent.Form.target(event)##value)) 218 | ) 219 | autoFocus=true 220 | /> 221 |
222 | main 223 | footer 224 |
; 225 | }, 226 | }; 227 | }; 228 | 229 | ReactDOMRe.renderToElementWithClassName(, "todoapp"); 230 | -------------------------------------------------------------------------------- /src/todomvc/TodoFooter.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require("react"); 4 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 5 | 6 | var component = ReasonReact.statelessComponent("TodoFooterRe"); 7 | 8 | function push(path, $$event) { 9 | $$event.preventDefault(); 10 | return ReasonReact.Router[/* push */0]("#" + path); 11 | } 12 | 13 | function make(count, completedCount, nowShowing, onClearCompleted, _) { 14 | return /* record */[ 15 | /* debugName */component[/* debugName */0], 16 | /* reactClassInternal */component[/* reactClassInternal */1], 17 | /* handedOffState */component[/* handedOffState */2], 18 | /* willReceiveProps */component[/* willReceiveProps */3], 19 | /* didMount */component[/* didMount */4], 20 | /* didUpdate */component[/* didUpdate */5], 21 | /* willUnmount */component[/* willUnmount */6], 22 | /* willUpdate */component[/* willUpdate */7], 23 | /* shouldUpdate */component[/* shouldUpdate */8], 24 | /* render */(function () { 25 | var match = count === 1; 26 | var activeTodoWord = match ? "item" : "items"; 27 | var match$1 = completedCount > 0; 28 | var clearButton = match$1 ? React.createElement("button", { 29 | className: "clear-completed", 30 | onClick: onClearCompleted 31 | }, "Clear completed") : null; 32 | var match$2; 33 | switch (nowShowing) { 34 | case 0 : 35 | match$2 = /* tuple */[ 36 | "selected", 37 | "", 38 | "" 39 | ]; 40 | break; 41 | case 1 : 42 | match$2 = /* tuple */[ 43 | "", 44 | "selected", 45 | "" 46 | ]; 47 | break; 48 | case 2 : 49 | match$2 = /* tuple */[ 50 | "", 51 | "", 52 | "selected" 53 | ]; 54 | break; 55 | 56 | } 57 | return React.createElement("footer", { 58 | className: "footer" 59 | }, React.createElement("span", { 60 | className: "todo-count" 61 | }, React.createElement("strong", undefined, String(count)), " " + (activeTodoWord + " left")), React.createElement("ul", { 62 | className: "filters" 63 | }, React.createElement("li", undefined, React.createElement("a", { 64 | className: match$2[0], 65 | onClick: (function (param) { 66 | return push("", param); 67 | }) 68 | }, "All")), " ", React.createElement("li", undefined, React.createElement("a", { 69 | className: match$2[1], 70 | onClick: (function (param) { 71 | return push("active", param); 72 | }) 73 | }, "Active")), " ", React.createElement("li", undefined, React.createElement("a", { 74 | className: match$2[2], 75 | onClick: (function (param) { 76 | return push("completed", param); 77 | }) 78 | }, "Completed"))), clearButton); 79 | }), 80 | /* initialState */component[/* initialState */10], 81 | /* retainedProps */component[/* retainedProps */11], 82 | /* reducer */component[/* reducer */12], 83 | /* jsElementWrapped */component[/* jsElementWrapped */13] 84 | ]; 85 | } 86 | 87 | exports.component = component; 88 | exports.push = push; 89 | exports.make = make; 90 | /* component Not a pure module */ 91 | -------------------------------------------------------------------------------- /src/todomvc/TodoFooter.re: -------------------------------------------------------------------------------- 1 | type showingState = 2 | | AllTodos 3 | | ActiveTodos 4 | | CompletedTodos; 5 | 6 | let component = ReasonReact.statelessComponent("TodoFooterRe"); 7 | 8 | let push = (path, event) => { 9 | ReactEvent.Mouse.preventDefault(event); 10 | ReasonReact.Router.push("#" ++ path); 11 | }; 12 | 13 | let make = 14 | (~count, ~completedCount, ~nowShowing, ~onClearCompleted, _children) => { 15 | ...component, 16 | render: _self => { 17 | let activeTodoWord = count === 1 ? "item" : "items"; 18 | let clearButton = 19 | completedCount > 0 ? 20 | : 23 | ReasonReact.null; 24 | let (all, active, completed) = 25 | switch (nowShowing) { 26 | | AllTodos => ("selected", "", "") 27 | | ActiveTodos => ("", "selected", "") 28 | | CompletedTodos => ("", "", "selected") 29 | }; 30 | ; 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/todomvc/TodoItem.bs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Block = require("bs-platform/lib/js/block.js"); 4 | var Curry = require("bs-platform/lib/js/curry.js"); 5 | var React = require("react"); 6 | var $$String = require("bs-platform/lib/js/string.js"); 7 | var ReasonReact = require("reason-react/src/ReasonReact.js"); 8 | var Js_primitive = require("bs-platform/lib/js/js_primitive.js"); 9 | 10 | var component = ReasonReact.reducerComponent("TodoItemRe"); 11 | 12 | function setEditFieldRef(r, param) { 13 | param[/* state */1][/* editFieldRef */2][0] = (r == null) ? undefined : Js_primitive.some(r); 14 | return /* () */0; 15 | } 16 | 17 | function make(todo, editing, onDestroy, onSave, onEdit, onToggle, onCancel, _) { 18 | var submitHelper = function (state) { 19 | var nonEmptyValue = $$String.trim(state[/* editText */0]); 20 | if (nonEmptyValue === "") { 21 | return /* SideEffects */Block.__(1, [(function () { 22 | return Curry._1(onDestroy, /* () */0); 23 | })]); 24 | } else { 25 | return /* UpdateWithSideEffects */Block.__(2, [ 26 | /* record */[ 27 | /* editText */nonEmptyValue, 28 | /* editing */state[/* editing */1], 29 | /* editFieldRef */state[/* editFieldRef */2] 30 | ], 31 | (function () { 32 | return Curry._1(onSave, nonEmptyValue); 33 | }) 34 | ]); 35 | } 36 | }; 37 | return /* record */[ 38 | /* debugName */component[/* debugName */0], 39 | /* reactClassInternal */component[/* reactClassInternal */1], 40 | /* handedOffState */component[/* handedOffState */2], 41 | /* willReceiveProps */(function (param) { 42 | var state = param[/* state */1]; 43 | return /* record */[ 44 | /* editText */state[/* editText */0], 45 | /* editing */editing, 46 | /* editFieldRef */state[/* editFieldRef */2] 47 | ]; 48 | }), 49 | /* didMount */component[/* didMount */4], 50 | /* didUpdate */(function (param) { 51 | var match = param[/* oldSelf */0][/* state */1][/* editing */1]; 52 | var match$1 = param[/* newSelf */1][/* state */1][/* editFieldRef */2][0]; 53 | if (match || !(editing && match$1 !== undefined)) { 54 | return /* () */0; 55 | } else { 56 | var field = Js_primitive.valFromOption(match$1); 57 | field.focus(); 58 | field.setSelectionRange(field.value.length, field.value.length); 59 | return /* () */0; 60 | } 61 | }), 62 | /* willUnmount */component[/* willUnmount */6], 63 | /* willUpdate */component[/* willUpdate */7], 64 | /* shouldUpdate */component[/* shouldUpdate */8], 65 | /* render */(function (param) { 66 | var send = param[/* send */3]; 67 | var match = todo[/* completed */2]; 68 | var className = $$String.concat(" ", /* :: */[ 69 | match ? "completed" : "", 70 | /* :: */[ 71 | editing ? "editing" : "", 72 | /* [] */0 73 | ] 74 | ]); 75 | return React.createElement("li", { 76 | className: className 77 | }, React.createElement("div", { 78 | className: "view" 79 | }, React.createElement("input", { 80 | className: "toggle", 81 | checked: todo[/* completed */2], 82 | type: "checkbox", 83 | onChange: (function () { 84 | return Curry._1(onToggle, /* () */0); 85 | }) 86 | }), React.createElement("label", { 87 | onDoubleClick: (function () { 88 | Curry._1(onEdit, /* () */0); 89 | return Curry._1(send, /* Edit */0); 90 | }) 91 | }, todo[/* title */1]), React.createElement("button", { 92 | className: "destroy", 93 | onClick: (function () { 94 | return Curry._1(onDestroy, /* () */0); 95 | }) 96 | })), React.createElement("input", { 97 | ref: Curry._1(param[/* handle */0], setEditFieldRef), 98 | className: "edit", 99 | value: param[/* state */1][/* editText */0], 100 | onKeyDown: (function ($$event) { 101 | return Curry._1(send, /* KeyDown */Block.__(0, [$$event.which])); 102 | }), 103 | onBlur: (function () { 104 | return Curry._1(send, /* Submit */1); 105 | }), 106 | onChange: (function ($$event) { 107 | return Curry._1(send, /* Change */Block.__(1, [$$event.target.value])); 108 | }) 109 | })); 110 | }), 111 | /* initialState */(function () { 112 | return /* record */[ 113 | /* editText */todo[/* title */1], 114 | /* editing */editing, 115 | /* editFieldRef : record */[/* contents */undefined] 116 | ]; 117 | }), 118 | /* retainedProps */component[/* retainedProps */11], 119 | /* reducer */(function (action) { 120 | if (typeof action === "number") { 121 | if (action === 0) { 122 | return (function (state) { 123 | return /* Update */Block.__(0, [/* record */[ 124 | /* editText */todo[/* title */1], 125 | /* editing */state[/* editing */1], 126 | /* editFieldRef */state[/* editFieldRef */2] 127 | ]]); 128 | }); 129 | } else { 130 | return submitHelper; 131 | } 132 | } else if (action.tag) { 133 | var text = action[0]; 134 | return (function (state) { 135 | if (editing) { 136 | return /* Update */Block.__(0, [/* record */[ 137 | /* editText */text, 138 | /* editing */state[/* editing */1], 139 | /* editFieldRef */state[/* editFieldRef */2] 140 | ]]); 141 | } else { 142 | return /* NoUpdate */0; 143 | } 144 | }); 145 | } else { 146 | var match = action[0]; 147 | if (match !== 13) { 148 | if (match !== 27) { 149 | return (function () { 150 | return /* NoUpdate */0; 151 | }); 152 | } else { 153 | Curry._1(onCancel, /* () */0); 154 | return (function (state) { 155 | return /* Update */Block.__(0, [/* record */[ 156 | /* editText */todo[/* title */1], 157 | /* editing */state[/* editing */1], 158 | /* editFieldRef */state[/* editFieldRef */2] 159 | ]]); 160 | }); 161 | } 162 | } else { 163 | return submitHelper; 164 | } 165 | } 166 | }), 167 | /* jsElementWrapped */component[/* jsElementWrapped */13] 168 | ]; 169 | } 170 | 171 | exports.component = component; 172 | exports.setEditFieldRef = setEditFieldRef; 173 | exports.make = make; 174 | /* component Not a pure module */ 175 | -------------------------------------------------------------------------------- /src/todomvc/TodoItem.re: -------------------------------------------------------------------------------- 1 | type todo = { 2 | id: string, 3 | title: string, 4 | completed: bool, 5 | }; 6 | 7 | type state = { 8 | editText: string, 9 | editing: bool, 10 | editFieldRef: ref(option(Dom.element)), 11 | }; 12 | 13 | type action = 14 | | Edit 15 | | Submit 16 | | KeyDown(int) 17 | | Change(string); 18 | 19 | let component = ReasonReact.reducerComponent("TodoItemRe"); 20 | 21 | let setEditFieldRef = (r, {ReasonReact.state}) => 22 | state.editFieldRef := Js.Nullable.toOption(r); 23 | 24 | let make = 25 | ( 26 | ~todo, 27 | ~editing, 28 | ~onDestroy, 29 | ~onSave, 30 | ~onEdit, 31 | ~onToggle, 32 | ~onCancel, 33 | _children, 34 | ) => { 35 | let submitHelper = state => 36 | switch (String.trim(state.editText)) { 37 | | "" => ReasonReact.SideEffects((_self => onDestroy())) 38 | | nonEmptyValue => 39 | ReasonReact.UpdateWithSideEffects( 40 | {...state, editText: nonEmptyValue}, 41 | (_self => onSave(nonEmptyValue)), 42 | ) 43 | }; 44 | { 45 | ...component, 46 | initialState: () => { 47 | editText: todo.title, 48 | editFieldRef: ref(None), 49 | editing, 50 | }, 51 | reducer: action => 52 | switch (action) { 53 | | Edit => ( 54 | state => ReasonReact.Update({...state, editText: todo.title}) 55 | ) 56 | | Submit => submitHelper 57 | | Change(text) => ( 58 | state => 59 | editing ? 60 | ReasonReact.Update({...state, editText: text}) : 61 | ReasonReact.NoUpdate 62 | ) 63 | | KeyDown(27) => 64 | onCancel(); 65 | (state => ReasonReact.Update({...state, editText: todo.title})); 66 | | KeyDown(13) => submitHelper 67 | | KeyDown(_) => (_state => ReasonReact.NoUpdate) 68 | }, 69 | willReceiveProps: ({state}) => {...state, editing}, 70 | didUpdate: ({oldSelf, newSelf}) => 71 | switch (oldSelf.state.editing, editing, newSelf.state.editFieldRef^) { 72 | | (false, true, Some(field)) => 73 | let node = ReactDOMRe.domElementToObj(field); 74 | ignore(node##focus()); 75 | ignore( 76 | node##setSelectionRange(node##value##length, node##value##length), 77 | ); 78 | | _ => () 79 | }, 80 | /* escape key */ 81 | render: ({state, handle, send}) => { 82 | let className = 83 | [todo.completed ? "completed" : "", editing ? "editing" : ""] 84 | |> String.concat(" "); 85 |
  • 86 |
    87 | onToggle()) 92 | /> 93 | 102 |
    104 | send(Submit)) 109 | onChange=( 110 | event => send(Change(ReactEvent.Form.target(event)##value)) 111 | ) 112 | onKeyDown=( 113 | event => send(KeyDown(ReactEvent.Keyboard.which(event))) 114 | ) 115 | /> 116 |
  • ; 117 | }, 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /src/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReasonReact • 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 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const isProd = process.env.NODE_ENV === 'production'; 4 | 5 | module.exports = { 6 | entry: { 7 | async: './src/async/CounterRoot.bs.js', 8 | simple: './src/simple/SimpleRoot.bs.js', 9 | fetch: './src/fetch/FetchExampleRoot.bs.js', 10 | todomvc: './src/todomvc/App.bs.js', 11 | interop: './src/interop/InteropRoot.js', 12 | retainedProps: './src/retainedProps/RetainedPropsRoot.bs.js', 13 | animation: './src/animation/AnimationRoot.bs.js', 14 | hooks: './src/hooks/HooksRoot.bs.js', 15 | "hooks-animation": './src/hooks-animation/HooksAnimationRoot.bs.js', 16 | }, 17 | mode: isProd ? 'production' : 'development', 18 | output: { 19 | path: path.join(__dirname, "bundledOutputs"), 20 | filename: '[name].js', 21 | }, 22 | }; 23 | --------------------------------------------------------------------------------