├── .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 | ;
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 | ;
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 | ;
199 |
200 |
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 |
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 |
--------------------------------------------------------------------------------