├── prepublish.el ├── .gitignore ├── README.org ├── webpack.config.js ├── src ├── index.html ├── example.re ├── reactReact.re └── reactReact.rei ├── shell.nix ├── bsconfig.json ├── LICENSE ├── tasks.json └── package.json /prepublish.el: -------------------------------------------------------------------------------- 1 | (with-current-buffer (find-file-noselect "./README.org") 2 | (require 'org) 3 | (org-md-export-to-markdown)) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .DS_Store 3 | .merlin 4 | npm-debug.log 5 | /lib/ 6 | /node_modules/ 7 | bundledOutputs/ 8 | *.bs.js 9 | .bsb.lock 10 | *.md 11 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * DEPRECATED 2 | 3 | Since ReasonML now uses melange and not bucklescript, and Erratique's 4 | React is not compatible with melange, this library can no longer be 5 | used. 6 | 7 | I'm investigating FRP libraries compatible with melange. 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | example: './src/example.bs.js', 6 | }, 7 | output: { 8 | path: path.join(__dirname, "bundledOutputs"), 9 | filename: '[name].js', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | let 4 | ocaml_4_06 = callPackage {}; 5 | ocamlPackages' = ocamlPackages.overrideScope' (self: super: { 6 | ocaml = ocaml_4_06; 7 | }); 8 | in 9 | mkShell { 10 | buildInputs = [ 11 | ocaml_4_06 12 | nodejs 13 | python 14 | stdenv 15 | git 16 | nodePackages.ocaml-language-server 17 | ocamlPackages'.merlin 18 | emacs 19 | ocamlPackages'.reason 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | // This is the configuration file used by BuckleScript's build system bsb. Its documentation lives here: http://bucklescript.github.io/bucklescript/docson/#build-schema.json 2 | // BuckleScript comes with its own parser for bsconfig.json, which is normal JSON, with the extra support of comments and trailing commas. 3 | { 4 | "name": "@denommus/react-react", 5 | "reason": { "react-jsx": 3 }, 6 | "refmt": 3, 7 | "version": "1.1.3", 8 | "sources": [ 9 | { 10 | "dir": "src", 11 | "subdirs": true 12 | } 13 | ], 14 | "package-specs": [ 15 | { 16 | "module": "commonjs", 17 | "in-source": true 18 | } 19 | ], 20 | "suffix": ".bs.js", 21 | "bs-dependencies": ["react-frp", "reason-react"] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Yuri Albuquerque 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "options": { 5 | "cwd": "${workspaceRoot}" 6 | }, 7 | "isShellCommand": true, 8 | "args": [ 9 | "run", 10 | "watch" 11 | ], 12 | "showOutput": "always", 13 | "isBackground": true, 14 | "problemMatcher": { 15 | "fileLocation": "absolute", 16 | "owner": "ocaml", 17 | "watching": { 18 | "activeOnStart": false, 19 | "beginsPattern": ">>>> Start compiling", 20 | "endsPattern": ">>>> Finish compiling" 21 | }, 22 | "pattern": [ 23 | { 24 | "regexp": "^File \"(.*)\", line (\\d+)(?:, characters (\\d+)-(\\d+))?:$", 25 | "file": 1, 26 | "line": 2, 27 | "column": 3, 28 | "endColumn": 4 29 | }, 30 | { 31 | "regexp": "^(?:(?:Parse\\s+)?(Warning|[Ee]rror)(?:\\s+\\d+)?:)?\\s+(.*)$", 32 | "severity": 1, 33 | "message": 2, 34 | "loop": true 35 | } 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@denommus/react-react", 3 | "version": "1.1.3", 4 | "scripts": { 5 | "prepublish": "emacs --script prepublish.el", 6 | "build": "bsb -make-world", 7 | "start": "bsb -make-world -w", 8 | "clean": "bsb -clean-world", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "webpack": "webpack --mode=development -w", 11 | "webpack:production": "NODE_ENV=production webpack --mode=production", 12 | "server": "webpack-dev-server" 13 | }, 14 | "keywords": [ 15 | "BuckleScript" 16 | ], 17 | "license": "MIT", 18 | "peerDependencies": { 19 | "react": "^16.8.1", 20 | "react-dom": "^16.8.1", 21 | "reason-react": ">=0.7.1" 22 | }, 23 | "devDependencies": { 24 | "bs-platform": "^8.2.0", 25 | "html-webpack-plugin": "^3.2.0", 26 | "moduleserve": "^0.9.0", 27 | "react": "^16.13.1", 28 | "react-dom": "^16.13.1", 29 | "reason-react": ">=0.7.1", 30 | "webpack": "^4.44.0", 31 | "webpack-cli": "^3.3.12", 32 | "webpack-dev-server": "^3.11.0" 33 | }, 34 | "dependencies": { 35 | "react-frp": "git://github.com/Denommus/react.git#bucklescript-files" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/example.re: -------------------------------------------------------------------------------- 1 | open ReactFrp.React; 2 | open ReactReact; 3 | open ReactReact.Utils; 4 | 5 | module ShowName = { 6 | let vdomS = propsS => 7 | S.map( 8 | ~eq=(_, _) => false, 9 | name => 10 | if (name != "") { 11 | let greeting = {j|Hello, $name!|j}; 12 |

{ReasonReact.string(greeting)}

; 13 | } else { 14 |

{ReasonReact.string("Hello, unknown person!")}

; 15 | }, 16 | propsS, 17 | ); 18 | [@react.component] 19 | let make = (~name) => componentFromSignal(vdomS, name); 20 | }; 21 | 22 | module Timer = { 23 | let counter = ref(0); 24 | let (timeS, timeF) = S.create(counter^); 25 | let timeIncrement = () => { 26 | counter := counter^ + 1; 27 | timeF(counter^); 28 | }; 29 | let timerId = Js.Global.setInterval(timeIncrement, 1000); 30 | let vdomS = _ => 31 | S.map( 32 | ~eq=(_, _) => false, 33 | time => { 34 | let timeMessage = time == 1 ? "second" : "seconds"; 35 | let message = {j|You've spent $time $timeMessage on this page!|j}; 36 |
{ReasonReact.string(message)}
; 37 | }, 38 | timeS, 39 | ); 40 | [@react.component] 41 | let make = () => componentFromSignal(vdomS, ()); 42 | }; 43 | 44 | module Input = { 45 | let (nameS, nameF) = S.create(""); 46 | let vdomS = _ => 47 | S.map( 48 | ~eq=(_, _) => false, 49 | name => 50 |
51 | emitEventToStream(nameF, ev)} /> 52 | 53 | 54 |
, 55 | nameS, 56 | ); 57 | [@react.component] 58 | let make = () => componentFromSignal(vdomS, ()); 59 | }; 60 | 61 | ReactDOMRe.renderToElementWithId(, "index"); 62 | -------------------------------------------------------------------------------- /src/reactReact.re: -------------------------------------------------------------------------------- 1 | open ReactFrp.React; 2 | 3 | type signalPair('a) = { 4 | signal: signal('a), 5 | setSignal: 'a => unit, 6 | }; 7 | 8 | let componentFromSignal = (propsToVdom, props) => { 9 | /* Starts with an empty element. That is, renders nothing */ 10 | let (element, setElement) = React.useState(() => React.null); 11 | 12 | /* I'm now using useRef because it allows me to clean up resources */ 13 | let propsPair = React.useRef(None); 14 | let watcher = React.useRef(None); 15 | 16 | React.useEffect0(() => { 17 | let (propsS, propsF) = S.create(props); 18 | propsPair.current = Some({signal: propsS, setSignal: x => propsF(x)}); 19 | let vdomS = propsToVdom(propsS); 20 | /* I need to store the watcher in a variable, otherwise it can be gc'd 21 | and the signal(element) will stop updating the state */ 22 | watcher.current = 23 | Some(S.map(newElement => setElement(_ => newElement), vdomS)); 24 | Some( 25 | () => { 26 | /* Cleaning up resources */ 27 | Belt.Option.map(propsPair.current, x => 28 | S.stop(~strong=true, x.signal) 29 | ) 30 | |> ignore; 31 | Belt.Option.map(watcher.current, S.stop(~strong=true)) |> ignore; 32 | propsPair.current = None; 33 | watcher.current = None; 34 | }, 35 | ); 36 | }); 37 | 38 | React.useEffect1(() => { 39 | /* This is where the props passed by parameter become a signal */ 40 | Belt.Option.map(propsPair.current, x => x.setSignal(props)) |> ignore; 41 | None 42 | }, [|props|]); 43 | 44 | element; 45 | }; 46 | 47 | module Utils = { 48 | let valueFromEvent = ev => ReactEvent.Form.target(ev)##value; 49 | let emitEventToStream = (streamF, ev) => ev |> valueFromEvent |> streamF; 50 | let eventFromPromise = promise => { 51 | open Belt.Result; 52 | open Js.Promise; 53 | let (promiseE, promiseF) = E.create(); 54 | promise 55 | |> then_(x => { 56 | promiseF(Ok(x)); 57 | promise; 58 | }) 59 | |> catch(x => { 60 | promiseF(Error(x)); 61 | promise; 62 | }) 63 | |> ignore; 64 | promiseE; 65 | }; 66 | module Event = { 67 | let join = ee => E.switch_(E.never, ee); 68 | let bind = (e, f) => join(E.map(f, e)); 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/reactReact.rei: -------------------------------------------------------------------------------- 1 | open ReactFrp.React; 2 | 3 | /* 4 | Generates a ReasonReact component from a ReactFrp signal. 5 | 6 | propsEq: overrides the equality function used to detect changes on 7 | the props. When a change is detected the props signal is updated 8 | accordingly. 9 | 10 | propsToVdom: a function that takes the props signal and returns a 11 | reactElement signal. The reactElement signal is the actual vdom you 12 | want to be rendered. 13 | 14 | props: the current value of the props (as received by make). It's 15 | usually a tuple, but you can create a custom type for it. The value 16 | will be fed to a signal. 17 | */ 18 | let componentFromSignal: 19 | (signal('a) => signal(React.element), 'a) => React.element; 20 | 21 | module Utils: { 22 | /* 23 | 24 | Emits the value of a ReasonReact event into a signal or an event, 25 | returns a callback function to be used by the event prop of an 26 | element. 27 | 28 | For example, you can have a signal created with 29 | 30 | let (nameS, nameF) = S.create(""); 31 | 32 | And in a text input you can have 33 | 34 | 35 | 36 | This way, whenever onChange happens, nameF is called and nameS is updated. 37 | 38 | You can do it for events in the same way. 39 | 40 | signalF: a function that emits a value to a signal or to an event 41 | 42 | It's called ToStream because it supports both signals and events 43 | 44 | */ 45 | let emitEventToStream: ('a => unit, ReactEvent.Form.t) => unit; 46 | /* 47 | 48 | Converts a Js.Promise.t into an event. 49 | 50 | */ 51 | let eventFromPromise: 52 | Js.Promise.t('a) => event(Belt.Result.t('a, Js.Promise.error)); 53 | module Event: { 54 | /* 55 | 56 | Takes an event that produces other events and keeps it as a single 57 | stream of the latest event that was emitted. 58 | 59 | In other words, when a new event is emitted, the stream from 60 | the previous event is now ignored. 61 | 62 | */ 63 | let join: event(event('a)) => event('a); 64 | /* 65 | 66 | Takes an event and a function that takes the value emitted by 67 | the first parameter to return an event, and then returns the 68 | event produced by this function. 69 | 70 | Similar to Js.Promise.then_ 71 | 72 | */ 73 | let bind: (event('a), 'a => event('b)) => event('b); 74 | }; 75 | }; 76 | --------------------------------------------------------------------------------