├── .githooks └── pre-push ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── .nojekyll ├── _sidebar.md ├── all_snippets.ml ├── api.html ├── children.md ├── composite_components.md ├── context.md ├── custom_elements.md ├── dune ├── dune_jsoo.md ├── first_components.md ├── gen_js_api.md ├── hooks.md ├── host_components.md ├── index.html ├── lists_and_keys.md ├── new_to_ocaml.md ├── quickstart.md ├── readme.md ├── refs.md ├── snippets │ └── basic.ml └── use_js_components.md ├── dune-project ├── example ├── package.json ├── src │ ├── App.re │ ├── Bindings.rei │ ├── Click.re │ ├── Code.re │ ├── EffectsAndState.re │ ├── Events.re │ ├── HelloWorldOCaml.ml │ ├── HelloWorldReason.re │ ├── Interface.re │ ├── Interface.rei │ ├── Main.re │ ├── Refs.re │ ├── UseEffect.re │ ├── WebComponent.ml │ ├── dune │ ├── index.html │ ├── static-requires.js │ └── static │ │ ├── leafy.js │ │ ├── primitive.css │ │ ├── prism.css │ │ └── prism.js ├── webpack.config.js └── yarn.lock ├── interop.md ├── jsoo-react.opam ├── lib ├── core.mli ├── dom.mli ├── dom_aria_attributes.ml ├── dom_aria_attributes.mli ├── dom_dsl_core.ml ├── dom_dsl_core.mli ├── dom_html.ml ├── dom_html.mli ├── dom_svg.ml ├── dom_svg.mli ├── dune ├── event.ml ├── event.mli ├── hooks.ml ├── hooks.mli ├── imports.ml ├── react.ml ├── router.ml ├── router.mli └── test_utils │ ├── ReactDOMTestUtils.js │ ├── dune │ └── reactDOMTestUtils.mli ├── ppx ├── dune ├── html.ml ├── html.mli ├── ppx.ml └── test │ ├── .ocamlformat-ignore │ ├── dune │ ├── input_errors_01_external_component_multi_prim.ml │ ├── input_errors_02_external_component_unlabelled_arg.ml │ ├── input_ocaml.ml │ ├── input_reason.re │ ├── main.ml │ ├── pp_errors_01_external_component_multi_prim.expected │ ├── pp_errors_02_external_component_unlabelled_arg.expected │ ├── pp_ocaml.expected │ ├── pp_ocaml_dev.expected │ └── pp_reason.expected ├── rfcs ├── 0000-template.md └── 0001-ocaml-syntax.md └── test ├── dune ├── external.js ├── jsdom.js ├── jsdom.mli ├── package.json ├── react-requires.js ├── test_jsoo_react.ml ├── test_ml.ml ├── test_reason.re └── yarn.lock /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! ( make format-check ); then 4 | echo "some files are not properly formatted, refusing to push" 5 | exit 1 6 | fi -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jchavarri 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | # Prime the caches every Monday 8 | - cron: 0 1 * * MON 9 | 10 | jobs: 11 | build: 12 | name: Build and test 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - macos-latest 21 | - ubuntu-latest 22 | - windows-latest 23 | ocaml-compiler: 24 | - 4.12.1 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | 30 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 31 | uses: ocaml/setup-ocaml@v2 32 | with: 33 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 34 | dune-cache: ${{ matrix.os != 'windows-latest' }} 35 | 36 | - name: Use Node 12 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 12 40 | 41 | - name: Install dependencies 42 | run: | 43 | opam install . --deps-only --with-doc --with-test 44 | opam install ocamlformat.0.21.0 45 | 46 | - name: Build 47 | run: make build 48 | 49 | - name: Check formatting 50 | run: make format-check 51 | 52 | - name: "Install npm packages for tests" 53 | run: yarn install 54 | working-directory: test 55 | 56 | - name: Run tests 57 | run: make test 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### OS ### 5 | .DS_Store 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (http://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Typescript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | 68 | # End of https://www.gitignore.io/api/node 69 | 70 | *.annot 71 | *.cmo 72 | *.cma 73 | *.cmi 74 | *.a 75 | *.o 76 | *.cmx 77 | *.cmxs 78 | *.cmxa 79 | 80 | # ocamlbuild working directory 81 | _build/ 82 | 83 | # ocamlbuild targets 84 | *.byte 85 | *.native 86 | 87 | # oasis generated files 88 | setup.data 89 | setup.log 90 | 91 | # Merlin configuring file for Vim and Emacs 92 | .merlin 93 | 94 | # Dune generated files 95 | *.install 96 | 97 | # Local OPAM switch 98 | _opam/ 99 | 100 | # Example output folder 101 | build 102 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | break-separators = before 3 | dock-collection-brackets = false 4 | exp-grouping = preserve 5 | type-decl = sparse 6 | break-fun-decl = fit-or-vertical 7 | break-fun-sig = fit-or-vertical 8 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.1 (2023-04-06) 2 | 3 | ### What's Changed 4 | * Add fragments and remove jsxv2 code from ppx by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/9 5 | * Refs + fix for keys by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/11 6 | * Bindings to useEffect Hook by @schinns in https://github.com/ml-in-barcelona/jsoo-react/pull/10 7 | * Use pipe first for more ergonomic Lwt.bind calls by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/16 8 | * Consolidate option libs and return unmount effects by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/17 9 | * Fix forwardRef unsafety + make DOM refs types more idiomatic by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/19 10 | * Eslint Config by @schinns in https://github.com/ml-in-barcelona/jsoo-react/pull/20 11 | * Fix deep elements () tests by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/21 12 | * Use metaquot by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/22 13 | * Fix example by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/24 14 | * Upgrade to OCaml 4.08 + new bindings approach + gen_js_api by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/25 15 | * Fix displayName so components names show properly in React devtools. by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/26 16 | * Add example of autogenerated `code` element reading from file with ppx-blob by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/27 17 | * changed dune lib name to jsoo_react_ppx by @idkjs in https://github.com/ml-in-barcelona/jsoo-react/pull/29 18 | * Add more examples, add ReactEvent and domProps bindings by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/30 19 | * fix example cd path by @naartjie in https://github.com/ml-in-barcelona/jsoo-react/pull/31 20 | * Bump acorn from 6.3.0 to 6.4.1 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/33 21 | * Fix build for dune 2, esy 0.6 and lwt 5 by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/35 22 | * OCaml 4.10 + more by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/40 23 | * Add ReactTestUtils, jsdom bindings, some initial tests for the bindings by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/41 24 | * readme: minor fixes by @zindel in https://github.com/ml-in-barcelona/jsoo-react/pull/42 25 | * Add memo and memoCustomCompareProps by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/44 26 | * dune: remove `(wrapped false)` by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/45 27 | * Allow consumers to decide how React is provided by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/48 28 | * add ci by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/49 29 | * Bump ini from 1.3.5 to 1.3.7 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/50 30 | * Bump elliptic from 6.5.3 to 6.5.4 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/53 31 | * Specify minimum on gen-js-api 1.0.7 by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/55 32 | * Switch to ppxlib by @keremc in https://github.com/ml-in-barcelona/jsoo-react/pull/64 33 | * CI: Update setup-ocaml to v2 by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/68 34 | * Add pre-push hook to check formatting by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/70 35 | * Inline props as object by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/66 36 | * refactor: Rename createMarkup to makeInnerHtml by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/74 37 | * Add 'help' command in Make by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/73 38 | * Better support for OCaml syntax by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/72 39 | * RFC: Better support for OCaml syntax by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/67 40 | * Type-safe HTML and SVG tags by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/71 41 | * fix: Ensure optionals are passed as undefineds by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/84 42 | * Create a test case for capturing error messages at build(ppx)-time by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/87 43 | * replace {dev} with :with-test by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/80 44 | * Remove filtering of optional props in make_js_props_obj by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/89 45 | * Update README.md by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/97 46 | * refactor: require `react`, `react-dom` within OCaml by @zbaylin in https://github.com/ml-in-barcelona/jsoo-react/pull/96 47 | * Keep useReducer dispatch and useState updater refs constant by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/101 48 | * Allow `string option` transformations for native elements by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/92 49 | * chore(deps): open jsoo constraint to 3.11.0 by @zbaylin in https://github.com/ml-in-barcelona/jsoo-react/pull/103 50 | * fix build & test instructions in CONTRIBUTING by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/107 51 | * Unify tests by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/108 52 | * Remove React.list by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/102 53 | * ppx: fix inexistent locations on comps that return element list by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/110 54 | * style: use functions as style key creation mechanism by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/116 55 | * chore(deps): bump follow-redirects from 1.14.5 to 1.14.7 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/114 56 | * Bindings to external JS components by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/106 57 | * fix: Add location on errors for invalid key/ref usage on props by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/118 58 | * ppx: don't assume external components are functions by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/124 59 | * ppx: fix external optional args by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/126 60 | * PPX-less API for creating DOM elements and props. by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/119 61 | * Wrong component type signature when using type annotation #85 by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/117 62 | * PoC: automatic js ffi conversion of external props by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/127 63 | * add Webcomponent example by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/129 64 | * ppx: Remove @@@react.dom + transform lowercase elements to Dsl functions by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/128 65 | * fix(ppx): Ensure we pass rec_flag on value_binding by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/133 66 | * Help Merlin find ast nodes inside let%component by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/134 67 | * ppx: fix bug on externals by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/135 68 | * Convert API to snake case by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/138 69 | * build(deps): bump follow-redirects from 1.14.7 to 1.14.8 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/140 70 | * Docs: experiment with docsify by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/139 71 | * Add ARIA 1.1 into global-attributes by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/145 72 | * Add global_attributes into both html and svg Props. by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/144 73 | * build(deps): bump url-parse from 1.5.3 to 1.5.7 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/148 74 | * Experiment: more ergonomic/idiomatic API by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/141 75 | * chore: bump js_of_ocaml to 4.0.0 by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/150 76 | * build(deps): bump url-parse from 1.5.7 to 1.5.10 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/152 77 | * consistent project-wide formatting by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/155 78 | * ci: try to fix win by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/159 79 | * dsl: fix react runtime warning when using maybe with None by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/156 80 | * allow locally abstract type and type constraint on component definitions by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/151 81 | * dsl: add classNames helper to Html and Svg props by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/157 82 | * dom: add create_portal by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/158 83 | * dsl: expose ARIA attributes by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/160 84 | * Update ppxlib 0.26 ocamlformat 0.21 by @jchavarri in https://github.com/ml-in-barcelona/jsoo-react/pull/163 85 | * Relax upper bounds to allow OCaml 4.14 and dune 3 by @sim642 in https://github.com/ml-in-barcelona/jsoo-react/pull/162 86 | * build(deps): bump minimist from 1.2.5 to 1.2.6 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/161 87 | * Hooks using OCaml idioms and equality semantics by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/154 88 | * ppx: set display name on let%component by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/166 89 | * A few ergonomic additions for dealing with conditional rendering by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/165 90 | * dom: add missing svg props by @glennsl in https://github.com/ml-in-barcelona/jsoo-react/pull/164 91 | * build(deps): bump async from 2.6.3 to 2.6.4 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/168 92 | * chore(deps): relax `gen_js_api` constraint to allow ^1.1.0 by @zbaylin in https://github.com/ml-in-barcelona/jsoo-react/pull/167 93 | * Add bindings to StrictMode by @schinns in https://github.com/ml-in-barcelona/jsoo-react/pull/169 94 | * build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 in /example by @dependabot in https://github.com/ml-in-barcelona/jsoo-react/pull/172 95 | * OPAM: upgrade Js_of_ocaml to 5.1.0 by @zbaylin in https://github.com/ml-in-barcelona/jsoo-react/pull/177 96 | * Prefix Option and Array with Stdlib by @davesnx in https://github.com/ml-in-barcelona/jsoo-react/pull/178 97 | 98 | ### New Contributors 99 | * @jchavarri made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/9 100 | * @schinns made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/10 101 | * @idkjs made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/29 102 | * @naartjie made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/31 103 | * @dependabot made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/33 104 | * @zindel made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/42 105 | * @davesnx made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/55 106 | * @keremc made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/64 107 | * @zbaylin made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/96 108 | * @glennsl made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/107 109 | * @sim642 made their first contribution in https://github.com/ml-in-barcelona/jsoo-react/pull/162 110 | 111 | **Full Changelog**: https://github.com/ml-in-barcelona/jsoo-react/commits/0.1 112 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting started 4 | 5 | - Install opam: https://opam.ocaml.org/doc/Install.html 6 | - Create local switch by running `make init` (this step will take a few minutes, as it will build whole OCaml compiler) 7 | - Build with `make build` 8 | - Run tests with 9 | - Install test dependencies with `(cd test; yarn)` 10 | - `make test` 11 | 12 | It is recommended to run `git config --local core.hooksPath .githooks/` to make sure git hooks run, 13 | otherwise you could get CI errors due to formatting. 14 | 15 | ## Ideas / decisions 16 | 17 | - Abstract over Js_of_ocaml "native" types (`js_string `, `js_array`) as much as 18 | possible behind the bindings. To do so, the library uses [`gen_js_api`](https://github.com/LexiFi/gen_js_api) 19 | to convert from/to these types to their more idiomatic OCaml representation. 20 | - Keep the API as close as possible to ReasonReact. This is useful for many reasons: 21 | - Battle tested. 22 | - Reduce cognitive load by leveraging ReasonReact knowledge. 23 | - Maximize potential reuse of existing components and libraries. 24 | 25 | ## Bindings 26 | 27 | See [`interop.md`](interop.md). 28 | 29 | ## Running the example 30 | 31 | ```bash 32 | git clone https://github.com/ml-in-barcelona/jsoo-react/ 33 | cd ./jsoo-react 34 | make init 35 | eval $(opam env) 36 | make dev 37 | # in another tab / terminal session 38 | cd ./jsoo-react/example 39 | yarn && yarn server 40 | ``` 41 | 42 | After that, open up `localhost:8000`. Then modify `App.re` file in `src` and refresh the page to see the changes. The example 43 | will not work without server as it relies on history / client-side routing using `jsoo-react` router to navigate through the pages. 44 | 45 | ### Ppx 46 | 47 | - `make test` to run the test against the expected result. 48 | - `make test-promote` when you want to update the expected results. 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Javier Chávarri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | project_name = jsoo-react 2 | 3 | DUNE = opam exec -- dune 4 | opam_file = $(project_name).opam 5 | current_hash = $(shell git rev-parse HEAD) 6 | 7 | .PHONY: build build-prod dev test test-promote deps format format-check init publish-example 8 | 9 | .PHONY: help 10 | help: ## Print this help message 11 | @echo "List of available make commands"; 12 | @echo ""; 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'; 14 | @echo ""; 15 | 16 | build: ## Build the project, including non installable libraries and executables 17 | $(DUNE) build @@default 18 | 19 | build-prod: ## Build for production (--profile=prod) 20 | $(DUNE) build --profile=prod @@default 21 | 22 | dev: ## Build in watch mode 23 | $(DUNE) build -w @@default 24 | 25 | test: ## Run the unit tests 26 | $(DUNE) build @runtest 27 | 28 | test-watch: ## Run the unit tests in watch mode 29 | $(DUNE) build @runtest -w 30 | 31 | test-promote: ## Updates snapshots and promotes it to correct 32 | $(DUNE) build @runtest --auto-promote 33 | 34 | deps: $(opam_file) ## Alias to update the opam file and install the needed deps 35 | 36 | format: ## Format the codebase with ocamlformat 37 | $(DUNE) build @fmt --auto-promote 38 | 39 | format-check: ## Checks if format is correct 40 | $(DUNE) build @fmt 41 | 42 | publish-example: ## Publish example/ to gh-pages 43 | git checkout main && $(DUNE) build --profile=prod @@default && cd example && yarn webpack:production \ 44 | && cd - && git checkout gh-pages && cp example/build/* . && git commit -am "$(current_hash)" 45 | 46 | $(opam_file): dune-project ## Update the package dependencies when new deps are added to dune-project 47 | $(DUNE) build @install 48 | opam install . --deps-only --with-test # Install the new dependencies 49 | 50 | init: ## Create a local opam switch and setups githooks 51 | git config core.hooksPath .githooks 52 | opam switch create . 4.12.1 --deps-only --with-test 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsoo-react 2 | 3 | [![Actions Status](https://github.com/ml-in-barcelona/jsoo-react/workflows/CI/badge.svg?branch=main)](https://github.com/ml-in-barcelona/jsoo-react/actions?query=branch%3Amain) 4 | 5 | Bindings to [React](https://reactjs.org/) for [js_of_ocaml](https://ocsigen.org/js_of_ocaml/), including the JSX ppx. 6 | 7 | > **Status**: experimental phase 8 | > 9 | > The library is expected to break backwards compatibility on minor releases. 10 | 11 | Adapted from [ReasonReact](https://github.com/reasonml/reason-react/). 12 | 13 | Bug reports and contributions are welcome! 14 | 15 | ## Getting started 16 | 17 | ### New project 18 | 19 | For new projects, the best way to start is by using [the jsoo-react template](https://github.com/ml-in-barcelona/jsoo-react-template). 20 | 21 | ### Existing project 22 | 23 | 1. Install the `jsoo-react` package: 24 | 25 | ```bash 26 | opam install jsoo-react 27 | ``` 28 | 29 | 2. Add `jsoo-react` library and ppx to [dune](https://dune.readthedocs.io/en/stable/) file of your executable JavaScript app: 30 | 31 | ```dune 32 | (executables 33 | (names index) 34 | (modes js) 35 | (libraries jsoo-react.lib) 36 | (preprocess 37 | (pps jsoo-react.ppx))) 38 | ``` 39 | 40 | 3. Provision React.js library 41 | 42 | `jsoo-react` uses `require` to import React and ReactDOM. This means that you will likely need to use a bundler such as Webpack or rollup.js. 43 | 44 | Note that at this moment, `jsoo-react` is compatible with **React 16**, so be sure to have the appropriate constraints in your `package.json`. 45 | 46 | ## Contributing 47 | 48 | Take a look at our [Contributing Guide](CONTRIBUTING.md). 49 | 50 | ## Acknowledgements 51 | 52 | Thanks to the authors and maintainers of ReasonReact, in particular [@rickyvetter](https://github.com/rickyvetter) for his work on the v3 of the JSX ppx. 53 | 54 | Thanks to the authors and maintainers of Js_of_ocaml, in particular [@hhugo](https://github.com/hhugo) who has been answering many many questions in GitHub threads. 55 | 56 | Thanks to the Lexifi team for creating and maintaining [gen_js_api](https://github.com/LexiFi/gen_js_api). 57 | 58 | Thanks to [@tmattio](https://github.com/tmattio/) for creating Spin and the jsoo-react template :raised_hands: 59 | 60 | And thanks to the team behind React.js! What an amazing library :) 61 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-in-barcelona/jsoo-react/308aceb57bac5f06e761af3f6e5dee39cc059213/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Introduction 2 | 3 | - [New to OCaml?](new_to_ocaml.md) 4 | - [Quick start](quickstart.md) 5 | - [Your first components](first_components.md) 6 | 7 | - Main concepts 8 | 9 | - [Host (lowercase) components](host_components.md) 10 | - [Composite (uppercase) components](composite_components.md) 11 | - [Children](children.md) 12 | - [Hooks](hooks.md) 13 | - [Refs](refs.md) 14 | - [Lists and keys](lists_and_keys.md) 15 | 16 | - Guides 17 | 18 | - [Context](context.md) 19 | - [Custom elements and data-* attributes](custom_elements.md) 20 | - [Building with Dune and Js_of_ocaml](dune_jsoo.md) 21 | - [Gen_js_api](gen_js_api.md) 22 | - [Use JavaScript components](use_js_components.md) 23 | 24 | - [API](api.html) 25 | -------------------------------------------------------------------------------- /docs/all_snippets.ml: -------------------------------------------------------------------------------- 1 | include Basic 2 | -------------------------------------------------------------------------------- /docs/children.md: -------------------------------------------------------------------------------- 1 | # Children 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/composite_components.md: -------------------------------------------------------------------------------- 1 | # Composite (uppercase) components 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/custom_elements.md: -------------------------------------------------------------------------------- 1 | # Custom elements 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/dune: -------------------------------------------------------------------------------- 1 | (include_subdirs unqualified) 2 | 3 | (executables 4 | (names All_snippets) 5 | (modes js) 6 | (libraries js_of_ocaml react) 7 | (preprocess 8 | (pps jsoo_react_ppx))) 9 | 10 | (rule 11 | (alias runtest) 12 | (deps All_snippets.bc.js) 13 | (action 14 | (run echo "docs"))) 15 | -------------------------------------------------------------------------------- /docs/dune_jsoo.md: -------------------------------------------------------------------------------- 1 | # Building with Dune and Js_of_ocaml 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/first_components.md: -------------------------------------------------------------------------------- 1 | # Your first components 2 | 3 | ## A simple component 4 | 5 | Jsoo-react components are functions that take input data and returns what to display. The library provides a DSL that allows to create elements like `div`, or `p` with just plain OCaml functions. The uppercase component props are passed as labelled arguments, while lowercase components take an array of attributes. 6 | 7 | [filename](snippets/basic.ml ':include :type=code :fragment=demo') 8 | -------------------------------------------------------------------------------- /docs/gen_js_api.md: -------------------------------------------------------------------------------- 1 | # Gen_js_api 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/host_components.md: -------------------------------------------------------------------------------- 1 | # Host (lowercase) components 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/lists_and_keys.md: -------------------------------------------------------------------------------- 1 | # Lists and keys 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/new_to_ocaml.md: -------------------------------------------------------------------------------- 1 | # New to OCaml? 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ## New project 4 | 5 | For new projects, the best way to start is by cloning [the jsoo-react template](https://github.com/ml-in-barcelona/jsoo-react-template). 6 | 7 | ## Existing project 8 | 9 | 1. Install the `jsoo-react` package: 10 | 11 | ```bash 12 | opam pin add -y jsoo-react https://github.com/ml-in-barcelona/jsoo-react.git 13 | ``` 14 | 15 | 2. Add `jsoo-react` library and ppx to [dune](https://dune.readthedocs.io/en/stable/) file of your executable JavaScript app: 16 | 17 | ```dune 18 | (executables 19 | (names index) 20 | (modes js) 21 | (libraries jsoo-react.lib) 22 | (preprocess 23 | (pps jsoo-react.ppx))) 24 | ``` 25 | 26 | 3. Provision React.js library 27 | 28 | `jsoo-react` uses `require` to import React and ReactDOM. This means that you will likely need to use a bundler such as Webpack or rollup.js. 29 | 30 | Note that at this moment, `jsoo-react` is compatible with **React 16.x**, so be sure to have the appropriate constraints in your `package.json`. 31 | 32 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # jsoo-react 2 | 3 | > React.js in OCaml. 4 | 5 | ## What it is 6 | 7 | Jsoo-react are the bindings to [React.js](reactjs.org/) for the [OCaml](https://ocaml.org/) language, in particular, for [Js_of_ocaml](https://ocsigen.org/js_of_ocaml/latest/manual/overview), a compiler that transforms OCaml bytecode into JavaScript. 8 | 9 | Jsoo-react allows to write components in a way that is familiar for both OCaml and JavaScript developers. 10 | 11 | See the [Quick start](quickstart.md) guide for more details. 12 | 13 | ## Features 14 | 15 | - Support for both Reason syntax and OCaml syntax 16 | - Integrated with Js_of_ocaml and the OCaml ecosystem 17 | - Optimized output, small footprint on JavaScript bundles 18 | - Heavily tested 19 | 20 | ## Examples 21 | 22 | Check out the [Real World Example App](https://github.com/jchavarri/jsoo-react-realworld-example-app) to see Jsoo-react in use. 23 | 24 | ## Community 25 | 26 | Users and the development team usually hang out in the [Reason](https://discord.gg/reasonml) and [OCaml](https://discord.gg/cCYQbqN) Discord servers. If you have specific questions, don't hesitate to start a [discussion on the GitHub repo](https://github.com/ml-in-barcelona/jsoo-react/discussions). 27 | -------------------------------------------------------------------------------- /docs/refs.md: -------------------------------------------------------------------------------- 1 | # Refs 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /docs/snippets/basic.ml: -------------------------------------------------------------------------------- 1 | (* [demo] *) 2 | open React.Dom.Dsl 3 | open Html 4 | 5 | module Hello_message = struct 6 | let%component make ~name = div [||] [ React.string ("Hello " ^ name) ] 7 | end 8 | 9 | let () = 10 | React.Dom.render_to_element ~id:"hello-example" 11 | (Hello_message.make ~name:"Taylor" ()) 12 | (* [demo] *) 13 | -------------------------------------------------------------------------------- /docs/use_js_components.md: -------------------------------------------------------------------------------- 1 | # Use JavaScript components 2 | 3 | todo 4 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.7) 2 | 3 | (name jsoo-react) 4 | 5 | (license MIT) 6 | 7 | (maintainers "Javier Chávarri ") 8 | 9 | (authors "Javier Chávarri ") 10 | 11 | (source 12 | (github ml-in-barcelona/jsoo-react)) 13 | 14 | (generate_opam_files true) 15 | 16 | (implicit_transitive_deps false) 17 | 18 | (package 19 | (name jsoo-react) 20 | (synopsis "Bindings to ReactJS for js_of_ocaml, including JSX ppx") 21 | (depends 22 | ;; General system dependencies 23 | (ocaml (>= 4.12.0)) 24 | 25 | (js_of_ocaml (>= 4.0.0)) 26 | (gen_js_api (and (>= 1.0.8) (< 1.2.0))) 27 | (ppxlib (>= 0.23.0)) 28 | 29 | ;; Test dependencies 30 | (webtest :with-test) 31 | (webtest-js :with-test) 32 | (js_of_ocaml-ppx :with-test) 33 | (conf-npm :with-test) 34 | 35 | ;; Dev dependencies, using with-test so that consumers don't install them (until package is released in opam) 36 | (ocamlformat (and (= 0.21.0) :with-test)) 37 | (reason (and (= 3.8.2) :with-test)) 38 | 39 | ;; Example dependencies, using with-test so that consumers don't install them (until package is released in opam) 40 | (ppx_blob :with-test) 41 | (js_of_ocaml-lwt :with-test) 42 | )) 43 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "webpack": "webpack -w", 6 | "webpack:production": "webpack --mode=production", 7 | "server": "webpack-dev-server", 8 | "analyze": "webpack --mode=production --profile --json > stats.json && webpack-bundle-analyzer stats.json" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "css-loader": "^3.2.0", 13 | "html-webpack-plugin": "^3.2.0", 14 | "style-loader": "^1.0.0", 15 | "webpack": "^4.44.2", 16 | "webpack-bundle-analyzer": "^3.9.0", 17 | "webpack-cli": "^3.3.12", 18 | "webpack-dev-server": "^3.11.0" 19 | }, 20 | "dependencies": { 21 | "react": "^16.13.1", 22 | "react-dom": "^16.13.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/App.re: -------------------------------------------------------------------------------- 1 | React.Dom.render_to_element(~id="app",
); 2 | -------------------------------------------------------------------------------- /example/src/Bindings.rei: -------------------------------------------------------------------------------- 1 | module Console: { 2 | [@js.global "console.log"] 3 | let log: 'a => unit; 4 | [@js.global "console.log"] 5 | let log2: ('a, 'b) => unit; 6 | [@js.global "console.log"] 7 | let log3: ('a, 'b, 'c) => unit; 8 | [@js.global "console.log"] 9 | let log4: ('a, 'b, 'c, 'd) => unit; 10 | }; 11 | 12 | module Window: { 13 | type window; 14 | [@js.global "window"] 15 | let get: option(window); 16 | [@js.call] 17 | let alert: (window, string) => unit; 18 | [@js.get] 19 | let value: Ojs.t => string; 20 | }; 21 | -------------------------------------------------------------------------------- /example/src/Click.re: -------------------------------------------------------------------------------- 1 | type action = 2 | | Clicked(int, int); 3 | 4 | let reducer = (state, action) => 5 | switch (action) { 6 | | Clicked(x, y) => [(x, y), ...state] 7 | }; 8 | 9 | [@react.component] 10 | let make = () => { 11 | let (state, dispatch) = React.use_reducer(~init=[], reducer); 12 |
{ 15 | dispatch @@ 16 | React.Event.Mouse.(Clicked(event |> clientX, event |> clientY)) 17 | }}> 18 | {"Hello" |> React.string} 19 | {"Pos: " 20 | ++ String.concat( 21 | "\n", 22 | state 23 | |> List.map(((x, y)) => 24 | string_of_int(x) ++ ", " ++ string_of_int(y) 25 | ), 26 | ) 27 | |> React.string} 28 |
; 29 | }; 30 | -------------------------------------------------------------------------------- /example/src/Code.re: -------------------------------------------------------------------------------- 1 | open Js_of_ocaml; 2 | open React.Dom.Dsl; 3 | open Html; 4 | 5 | [@react.component] 6 | let make = (~text) => { 7 | let codeRef = React.use_ref(Js.null); 8 | React.use_effect_always(() => { 9 | switch (codeRef |> React.Ref.current |> Js.Opt.to_option) { 10 | | Some(el) => Js.Unsafe.global##.Prism##highlightElement(el) 11 | | None => () 12 | }; 13 | None; 14 | }); 15 |
16 |     
17 |       {text |> React.string}
18 |     
19 |   
; 20 | }; 21 | -------------------------------------------------------------------------------- /example/src/EffectsAndState.re: -------------------------------------------------------------------------------- 1 | open Bindings; 2 | open React.Dom.Dsl; 3 | open Html; 4 | 5 | type action = 6 | | Increment 7 | | Decrement; 8 | 9 | let reducer = (state, action) => 10 | switch (action) { 11 | | Increment => state + 1 12 | | Decrement => state - 1 13 | }; 14 | 15 | let space = { 16 | " " |> React.string; 17 | }; 18 | 19 | [@react.component] 20 | let make = (~name="Billy", ~children=?) => { 21 | let (count, setCount) = React.use_state(() => 0); 22 | let (state, dispatch) = React.use_reducer(~init=() => 0, reducer); 23 | 24 |
25 | 26 |

27 | {"Hello from EffectsAndState component, " ++ name ++ "!" |> React.string} 28 |

29 | 36 | space 37 | 40 | space 41 | {string_of_int(state) |> React.string} 42 | space 43 | 46 | space 47 | {switch (children) { 48 | | Some(c) =>
c
49 | | None => React.null 50 | }} 51 |
; 52 | }; 53 | -------------------------------------------------------------------------------- /example/src/Events.re: -------------------------------------------------------------------------------- 1 | open Bindings; 2 | open React.Dom.Dsl; 3 | open Html; 4 | 5 | type coords = { 6 | x: int, 7 | y: int, 8 | }; 9 | 10 | module Title = { 11 | [@react.component] 12 | let make = (~children) => { 13 |
children
; 14 | }; 15 | }; 16 | 17 | [@react.component] 18 | let make = () => { 19 | let (coords, setCoords) = React.use_state(() => {x: 0, y: 0}); 20 | let (inputText, setInputText) = React.use_state(() => ""); 21 |
22 |
{"text input via \"onChange\"" |> React.string}
23 | { 25 | let value = React.Event.Form.target(event) |> Window.value; 26 | setInputText(_ => value); 27 | }} 28 | value=inputText 29 | /> 30 |
{"form submission via \"onSubmit\"" |> React.string}
31 |
{ 33 | React.Event.Form.prevent_default(event); 34 | switch (Window.get) { 35 | | None => () 36 | | Some(w) => 37 | Window.alert( 38 | w, 39 | "Quoteth Shakespeare, \"You cad! " ++ inputText ++ "\"", 40 | ) 41 | }; 42 | }}> 43 | { 45 | let value = React.Event.Form.target(event) |> Window.value; 46 | setInputText(_ => value); 47 | }} 48 | style=React.Dom.Style.(make([|margin_right("15px")|])) 49 | value=inputText 50 | /> 51 | 52 |
53 |
{"mouse movement via \"onMouseMove\"" |> React.string}
54 |
55 | { 58 | let (x, y) = 59 | React.Event.Mouse.(screen_x(event), screen_y(event)); 60 | setCoords(_ => {{x, y}}); 61 | }} 62 | /> 63 |
64 | {string_of_int(coords.x) 65 | ++ "px / " 66 | ++ string_of_int(coords.y) 67 | ++ "px" 68 | |> React.string} 69 |
70 |
71 |
; 72 | }; 73 | -------------------------------------------------------------------------------- /example/src/HelloWorldOCaml.ml: -------------------------------------------------------------------------------- 1 | open React.Dom.Dsl 2 | open Html 3 | 4 | let%component make () = 5 | fragment 6 | [ a 7 | [| href "https://github.com/jchavarri/jsoo-react-realworld-example-app" 8 | ; target "_blank" 9 | |] 10 | [ i [| className "ion-social-github" |] []; string "Fork on GitHub" ] 11 | ; footer [||] 12 | [ div 13 | [| className "container" |] 14 | [ span 15 | [| className "attribution" |] 16 | [ string "An interactive learning project from " 17 | ; a [| href "https://thinkster.io" |] [ string "Thinkster" ] 18 | ; string ". Code & design licensed under MIT." 19 | ] 20 | ] 21 | ] 22 | ] 23 | -------------------------------------------------------------------------------- /example/src/HelloWorldReason.re: -------------------------------------------------------------------------------- 1 | open React.Dom.Dsl; 2 | open Html; 3 | 4 | [@react.component] 5 | let make = () => { 6 |
{"Hello World from Reason" |> React.string}
; 7 | }; 8 | -------------------------------------------------------------------------------- /example/src/Interface.re: -------------------------------------------------------------------------------- 1 | open React.Dom.Dsl; 2 | open Html; 3 | 4 | let s = React.string; 5 | 6 | [@react.component] 7 | let make = (~title, ~children) => { 8 |
...{[ {title |> s} , ...children]}
; 9 | }; 10 | -------------------------------------------------------------------------------- /example/src/Interface.rei: -------------------------------------------------------------------------------- 1 | let make: 2 | (~title: string, ~children: list(React.element), ~key: string=?, unit) => 3 | React.element; 4 | -------------------------------------------------------------------------------- /example/src/Main.re: -------------------------------------------------------------------------------- 1 | open React.Dom.Dsl; 2 | open Html; 3 | 4 | let s = React.string; 5 | 6 | type example = { 7 | path: string, 8 | title: string, 9 | element: React.element, 10 | code: React.element, 11 | showTitle: bool, 12 | }; 13 | 14 | let firstExample = { 15 | path: "hello-world-ocaml", 16 | title: "Hello World OCaml", 17 | element: , 18 | code: , 19 | showTitle: true, 20 | }; 21 | let examples = [ 22 | firstExample, 23 | { 24 | path: "hello-world-reason", 25 | title: "Hello World Reason", 26 | element: , 27 | code: , 28 | showTitle: true, 29 | }, 30 | { 31 | path: "events", 32 | title: "Events", 33 | element: , 34 | code: , 35 | showTitle: true, 36 | }, 37 | { 38 | path: "effects-and-state", 39 | title: "Effects and state", 40 | element: , 41 | code: 42 | <> 43 |

{"EffectsAndState component:" |> s}

44 | 45 |

{"UseEffect component:" |> s}

46 | 47 | , 48 | showTitle: true, 49 | }, 50 | { 51 | path: "refs", 52 | title: "Refs", 53 | element: , 54 | code: , 55 | showTitle: true, 56 | }, 57 | { 58 | path: "web-components", 59 | title: "Web Components", 60 | element: 61 |
62 | 63 |
, 64 | code: , 65 | showTitle: true, 66 | }, 67 | { 68 | path: "interfaces", 69 | title: "Interface files", 70 | element: 71 | 74 | React.null 75 | , 76 | code: 77 |
78 | {"Interface" |> s} 79 | 80 | {"Implementation" |> s} 81 | 82 |
, 83 | showTitle: false, 84 | }, 85 | { 86 | path: "bindings", 87 | title: "Bindings to JavaScript code", 88 | element: 89 |

90 | {"Bindings can be written easily by using " |> s} 91 | 92 | {"gen_js_api" |> s} 93 | 94 | {"." |> s} 95 |

, 96 | code: , 97 | showTitle: false, 98 | }, 99 | { 100 | path: "ppxs", 101 | title: "PPXs (preprocessing extensions)", 102 | element: 103 | <> 104 |

105 | {"You have seen the embedded code snippets in the examples. The one below is from the Main component of this page." 106 | |> s} 107 | {"Interface.re" |> s} 108 | {"." |> s} 109 |

110 |

111 | {"These snippets always stay in sync with the code itself. The strings from the files get captured at compile time via a " 112 | |> s} 113 | 116 | {"ppx" |> s} 117 | 118 | {" (preprocessor extension) that is called " |> s} 119 | 120 | {"ppx_blob" |> s} 121 | 122 | {" by calling " |> s} 123 | {"" |> s} 124 | {". Then, we just pass that string to a component " |> s} 125 | {"Code" |> s} 126 | {"." |> s} 127 |

128 |

129 | {"Here is the implementation of" |> s} 130 | {"Code.re" |> s} 131 | {":" |> s} 132 |

133 | , 134 | code: , 135 | showTitle: false, 136 | }, 137 | ]; 138 | 139 | [@react.component] 140 | let make = () => { 141 | let url = React.Router.use_url(); 142 | 143 |
144 |
145 |

{"jsoo-react" |> s}

146 | 165 |
166 |
167 |
168 | {let example = 169 | examples 170 | |> List.find_opt(e => { 171 | e.path 172 | == ( 173 | List.length(url.path) > 0 174 | ? List.nth_opt(url.path, List.length(url.path) - 1) 175 | |> Option.value(~default="") 176 | : "" 177 | ) 178 | }) 179 | |> Option.value(~default=firstExample); 180 |
181 |

{example.title |> s}

182 | {example.showTitle 183 | ?

{"Rendered component" |> s}

: React.null} 184 | {example.element} 185 |

{"Code" |> s}

186 | {example.code} 187 |
} 188 |
189 |
190 |
; 191 | }; 192 | -------------------------------------------------------------------------------- /example/src/Refs.re: -------------------------------------------------------------------------------- 1 | open Bindings; 2 | open Js_of_ocaml; 3 | open React.Dom.Dsl; 4 | open Html; 5 | 6 | module FancyLink = { 7 | [@react.component] 8 | let make = 9 | React.Dom.forward_ref((~href, ~repo, ref) => 10 | {repo |> React.string} 11 | ); 12 | }; 13 | 14 | [@react.component] 15 | let make = () => { 16 | let (show, setShow) = React.use_state(() => true); 17 | /* You can now get a ref directly to the DOM button: */ 18 | let ref = 19 | React.Dom.Ref.callback_dom_ref(ref => { 20 | Console.log(Js.string("Ref is:")); 21 | Console.log(ref); 22 | }); 23 | <> 24 | 30 | {show 31 | ? 37 | : React.null} 38 |

39 | {"Open the console to see the value of the ref changing." |> React.string} 40 |

41 | ; 42 | }; 43 | -------------------------------------------------------------------------------- /example/src/UseEffect.re: -------------------------------------------------------------------------------- 1 | open Bindings; 2 | open React.Dom.Dsl; 3 | open Html; 4 | 5 | [@react.component] 6 | let make = (~count) => { 7 | open Js_of_ocaml_lwt; 8 | React.use_effect1( 9 | () => { 10 | open Lwt; 11 | open Lwt_js; 12 | bind( 13 | sleep(1.), 14 | () => { 15 | Console.log( 16 | "count changed 1 sec ago! Value is: " ++ string_of_int(count), 17 | ); 18 | return(); 19 | }, 20 | ) 21 | |> ignore; 22 | Some( 23 | () => 24 | Console.log( 25 | "Unmounting effect for value: " ++ string_of_int(count), 26 | ), 27 | ); 28 | }, 29 | [|count|], 30 | ); 31 | 32 | React.use_effect_always( 33 | ~before_render=true, 34 | () => { 35 | Console.log("use_layout_effect: component updated"); 36 | None; 37 | }, 38 | ); 39 | 40 |
41 | {"UseEffect: printing delayed counts on the console since 2019 :)" 42 | |> React.string} 43 |
; 44 | }; 45 | -------------------------------------------------------------------------------- /example/src/WebComponent.ml: -------------------------------------------------------------------------------- 1 | open React.Dom.Dsl 2 | open Html 3 | 4 | let oslo_polygon = 5 | {|{ 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [ 12 | 10.489165172838884, 13 | 60.017259872374645 14 | ], 15 | [ 16 | 10.580764868996987, 17 | 60.0762384207017 18 | ], 19 | [ 20 | 10.592122568549627, 21 | 60.09394183519897 22 | ], 23 | [ 24 | 10.572782530207661, 25 | 60.11678480264957 26 | ], 27 | [ 28 | 10.600720249305056, 29 | 60.13160981872188 30 | ], 31 | [ 32 | 10.68031961054535, 33 | 60.13353032001292 34 | ], 35 | [ 36 | 10.73711867703991, 37 | 60.125733600579316 38 | ], 39 | [ 40 | 10.78802079942288, 41 | 60.06755422118711 42 | ], 43 | [ 44 | 10.819765048019693, 45 | 60.064296771632726 46 | ], 47 | [ 48 | 10.811720634337512, 49 | 60.02561911878851 50 | ], 51 | [ 52 | 10.876109308200913, 53 | 59.98547372050647 54 | ], 55 | [ 56 | 10.933734244914053, 57 | 59.97416166211912 58 | ], 59 | [ 60 | 10.951389441905969, 61 | 59.94924298867558 62 | ], 63 | [ 64 | 10.914816194580183, 65 | 59.91161920924281 66 | ], 67 | [ 68 | 10.907158498257449, 69 | 59.869893465966655 70 | ], 71 | [ 72 | 10.933102370207493, 73 | 59.83659145034232 74 | ], 75 | [ 76 | 10.936527591798225, 77 | 59.831669697457514 78 | ], 79 | [ 80 | 10.88029688872709, 81 | 59.81138930328435 82 | ], 83 | [ 84 | 10.770788935602035, 85 | 59.82510863617183 86 | ], 87 | [ 88 | 10.744019668227386, 89 | 59.83928320264522 90 | ], 91 | [ 92 | 10.73100663891497, 93 | 59.877178566827084 94 | ], 95 | [ 96 | 10.658082484659966, 97 | 59.884410483442366 98 | ], 99 | [ 100 | 10.632783389561938, 101 | 59.915118906971855 102 | ], 103 | [ 104 | 10.63388386110467, 105 | 59.95342058502221 106 | ], 107 | [ 108 | 10.610456248652959, 109 | 59.97660952873646 110 | ], 111 | [ 112 | 10.55585521816055, 113 | 59.99672657430896 114 | ], 115 | [ 116 | 10.518070354830757, 117 | 59.999291170702094 118 | ], 119 | [ 120 | 10.489165172838884, 121 | 60.017259872374645 122 | ] 123 | ] 124 | ] 125 | }, 126 | "properties": { 127 | "kommunenummer": "0301", 128 | "objtype": "Kommune", 129 | "lokalid": "173018", 130 | "oppdateringsdato": null, 131 | "datauttaksdato": "20191220110355", 132 | "versjonid": "4.1", 133 | "opphav": null, 134 | "samiskforvaltningsomrade": false, 135 | "datafangstdato": null, 136 | "navnerom": "http://skjema.geonorge.no/SOSI/produktspesifikasjon/AdmEnheter/4.1", 137 | "navn": [ 138 | { 139 | "rekkefolge": "", 140 | "sprak": "nor", 141 | "navn": "Oslo" 142 | } 143 | ] 144 | } 145 | }|} 146 | 147 | let%component make () = 148 | h "leafy-map" [||] 149 | [ h "leafy-feature-group" 150 | Prop.[| bool "zoom-to-fit" true |] 151 | [ h "leafy-geojson" 152 | Prop. 153 | [| string "data" oslo_polygon 154 | ; string "fill" "forestgreen" 155 | ; string "stroke" "#006400" 156 | ; int "stroke-wdith" 1 157 | |] 158 | [] 159 | ; h "leafy-marker" 160 | Prop. 161 | [| string "lat" "59.9147857" 162 | ; string "lng" "10.7470423" 163 | ; string "tooltip" "Hello Oslo!" 164 | |] 165 | [] 166 | ] 167 | ] 168 | -------------------------------------------------------------------------------- /example/src/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names App) 3 | (modes js) 4 | (libraries gen_js_api lwt js_of_ocaml-lwt react) 5 | (js_of_ocaml 6 | (javascript_files static-requires.js)) 7 | (preprocess 8 | (pps gen_js_api.ppx ppx_blob js_of_ocaml-ppx jsoo_react_ppx))) 9 | 10 | (rule 11 | (targets Bindings.ml) 12 | (deps Bindings.rei) 13 | (action 14 | (progn 15 | (with-stdout-to 16 | Bindings.ml.tmp 17 | (run %{bin:refmt} --parse=re -i true --print=ml %{deps})) 18 | (run %{bin:gen_js_api} -o %{targets} Bindings.ml.tmp)))) 19 | 20 | (alias 21 | (name default) 22 | (deps App.js static/primitive.css static/prism.css static/prism.js)) 23 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Example usages of jsoo-react 9 | 10 | 14 | 18 | 19 | 42 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example/src/static-requires.js: -------------------------------------------------------------------------------- 1 | var primitive = require("./static/primitive.css"); 2 | var prismJs = require("./static/prism.js"); 3 | var prismCss = require("./static/prism.css"); 4 | var leafy = require("./static/leafy.js"); 5 | -------------------------------------------------------------------------------- /example/src/static/leafy.js: -------------------------------------------------------------------------------- 1 | const LEAFLET_CSS_URL = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"; 2 | const STYLESHEET = ` 3 | #map { 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | .leaflet-top .leaflet-control { 9 | margin-top: 2em; 10 | } 11 | .leaflet-left .leaflet-control { 12 | margin-left: 2em; 13 | } 14 | .leaflet-bottom .leaflet-control { 15 | margin-bottom: 2em; 16 | } 17 | .leaflet-right .leaflet-control { 18 | margin-right: 2em; 19 | } 20 | .leaflet-control.leaflet-control-attribution { 21 | margin: 0; 22 | margin-top: -1.5em; /* make this "transparent" to other controls margins */ 23 | } 24 | .leaflet-control-zoom, 25 | .leaflet-touch .leaflet-control-zoom { 26 | box-shadow: none; 27 | border: none; 28 | } 29 | .leaflet-control-zoom > a.leaflet-control-zoom-in, 30 | .leaflet-control-zoom > a.leaflet-control-zoom-out, 31 | .leaflet-touch .leaflet-control-zoom > a.leaflet-control-zoom-in, 32 | .leaflet-touch .leaflet-control-zoom > a.leaflet-control-zoom-out { 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | width: 1.5em; 37 | height: 1.5em; 38 | font-size: 26px; 39 | font-weight: normal; 40 | color: #E16246; 41 | border-radius: 50%; 42 | box-shadow: 0 1px 5px rgb(0 0 0 / 10%); 43 | } 44 | .leaflet-control-zoom > a:first-child { 45 | margin-bottom: .25em; 46 | } 47 | `; 48 | 49 | // leafy-map 50 | 51 | class Map extends HTMLElement { 52 | static get observedAttribtues() { 53 | return ["lat", "lng", "zoom"]; 54 | } 55 | 56 | get lat() { 57 | return this.getAttribute("lat") || 51; 58 | } 59 | get lng() { 60 | return this.getAttribute("lng") || 0; 61 | } 62 | get zoom() { 63 | return this.getAttribute("zoom") || -1; 64 | } 65 | 66 | attributeChangedCallback(name, oldValue, newValue) { 67 | super.attributeChangedCallback(name, oldValue, newValue); 68 | 69 | switch (name) { 70 | case "lat": 71 | case "lng": 72 | this.map && this.map.setView([this.lat, this.lng]); 73 | break; 74 | 75 | case "zoom": 76 | this.map && this.map.setZoom(this.zoom); 77 | break; 78 | } 79 | } 80 | 81 | constructor() { 82 | super(); 83 | 84 | this.render(); 85 | 86 | // ensure we've rendered before initializing 87 | setTimeout(() => { 88 | this.initMap(); 89 | this.initView(); 90 | this.initChildren(); 91 | }); 92 | } 93 | 94 | render() { 95 | this.attachShadow({ mode: "open" }); 96 | 97 | const styleEl = document.createElement("style"); 98 | styleEl.textContent = STYLESHEET; 99 | const leafletStyleEl = document.createElement("link"); 100 | leafletStyleEl.setAttribute("rel", "stylesheet"); 101 | leafletStyleEl.setAttribute("href", LEAFLET_CSS_URL); 102 | 103 | this.mapEl = document.createElement("div"); 104 | this.mapEl.setAttribute("id", "map"); 105 | 106 | this.shadowRoot.append(leafletStyleEl, styleEl, this.mapEl); 107 | } 108 | 109 | initMap() { 110 | if (this.map) this.map.remove(); 111 | 112 | this.map = L.map(this.mapEl, { 113 | zoomControl: false, 114 | }); 115 | 116 | L.control 117 | .zoom({ 118 | position: "bottomright", 119 | }) 120 | .addTo(this.map); 121 | 122 | L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 123 | attribution: 124 | 'Map data © CC-BY-SA, Imagery © Mapbox', 125 | maxZoom: 18, 126 | }).addTo(this.map); 127 | } 128 | 129 | initView() { 130 | this.map.setView([this.lat, this.lng], this.zoom); 131 | if (this.zoom == -1) { 132 | this.map.fitWorld({ animate: false }); 133 | } 134 | } 135 | 136 | initChildren() { 137 | for (let child of this.children) { 138 | child.container = this.map; 139 | } 140 | 141 | this.mutationObserver = new MutationObserver((mutations) => 142 | mutations.forEach((mutation) => { 143 | for (let child of mutation.addedNodes) { 144 | child.container = this.map; 145 | } 146 | 147 | for (let child of mutation.removedNodes) { 148 | child.container = null; 149 | } 150 | }) 151 | ); 152 | this.mutationObserver.observe(this, { childList: true }); 153 | } 154 | } 155 | 156 | customElements.define("leafy-map", Map); 157 | 158 | // Feature 159 | 160 | class Feature extends HTMLElement { 161 | static get observedAttributes() { 162 | return ["data", "tooltip", "tooltip-open"]; 163 | } 164 | 165 | get tooltip() { 166 | return this.getAttribute("tooltip"); 167 | } 168 | 169 | get tooltipOpen() { 170 | return this.getAttribute("tooltip-open"); 171 | } 172 | 173 | attributeChangedCallback(name, oldValue, newValue) { 174 | switch (name) { 175 | case "tooltip": 176 | case "tooltip-open": 177 | this._setPopup(); 178 | break; 179 | } 180 | } 181 | 182 | get feature() { 183 | return this._feature; 184 | } 185 | set feature(new_feature) { 186 | if (this.container && this._feature) { 187 | this.container.removeLayer(this._feature); 188 | } 189 | 190 | this._feature = new_feature; 191 | 192 | if (this.container && this._feature) { 193 | this._feature.addTo(this.container); 194 | this._setPopup(); 195 | } 196 | } 197 | 198 | get container() { 199 | return this._container; 200 | } 201 | set container(new_container) { 202 | if (this._container && this.feature) { 203 | this._container.removeLayer(this.feature); 204 | } 205 | 206 | this._container = new_container; 207 | 208 | if (this._container && this.feature) { 209 | this.feature.addTo(this._container); 210 | this._setPopup(); 211 | } 212 | } 213 | 214 | _setPopup() { 215 | if (this.feature) { 216 | this.feature.unbindTooltip(); 217 | 218 | if (this.tooltip) { 219 | this.feature.bindTooltip(this.tooltip, { 220 | direction: "top", 221 | }); 222 | 223 | if (this.tooltipOpen) { 224 | this.feature.openTooltip(); 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | // InteractiveFeature 232 | 233 | class InteractiveFeature extends Feature { 234 | connectedCallback() {} 235 | 236 | get feature() { 237 | return this._feature; 238 | } 239 | set feature(new_feature) { 240 | if (this.container && this._feature) { 241 | this.container.removeLayer(this._feature); 242 | } 243 | 244 | this._feature = new_feature; 245 | 246 | if (this._feature) { 247 | [ 248 | "click", 249 | "dblclick", 250 | "mousedown", 251 | "mouseup", 252 | "mouseover", 253 | "mouseout", 254 | "contextmenu", 255 | ].forEach((eventName) => 256 | this._feature.on(eventName, (event) => 257 | this.dispatchEvent(new MouseEvent(eventName, event)) 258 | ) 259 | ); 260 | } 261 | 262 | if (this.container && this._feature) { 263 | this._feature.addTo(this.container); 264 | this._setPopup(); 265 | } 266 | } 267 | } 268 | 269 | // FeatureGroup 270 | 271 | class FeatureGroup extends Feature { 272 | static get observedAttributes() { 273 | return ["zoom-to-fit", ...Feature.observedAttributes]; 274 | } 275 | 276 | get zoomToFit() { 277 | return this.getAttribute("zoom-to-fit") || false; 278 | } 279 | 280 | attributeChangedCallback(name, oldValue, newValue) { 281 | super.attributeChangedCallback(name, oldValue, newValue); 282 | 283 | switch (name) { 284 | case "zoom-to-fit": 285 | if (this.zoomToFit) { 286 | this.fitBounds(); 287 | } 288 | break; 289 | } 290 | } 291 | 292 | constructor() { 293 | super(); 294 | 295 | this.feature = L.featureGroup(); 296 | 297 | for (let child of this.children) { 298 | child.container = this.feature; 299 | } 300 | 301 | this.mutationObserver = new MutationObserver((mutations) => 302 | mutations.forEach((mutation) => { 303 | for (let child of mutation.addedNodes) { 304 | child.container = this.feature; 305 | } 306 | 307 | for (let child of mutation.removedNodes) { 308 | child.container = null; 309 | } 310 | 311 | this.fitBounds(); 312 | }) 313 | ); 314 | this.mutationObserver.observe(this, { childList: true }); 315 | 316 | this.fitBounds(); 317 | } 318 | 319 | get container() { 320 | return super.container; 321 | } 322 | set container(new_container) { 323 | super.container = new_container; 324 | 325 | if (this.zoomToFit) { 326 | this.fitBounds(); 327 | } 328 | } 329 | 330 | fitBounds() { 331 | if (this.container) { 332 | let bounds = this.feature.getBounds(); 333 | if (bounds.isValid()) { 334 | this.container.fitBounds(bounds, { 335 | paddingBottomRight: L.point(0, 32), 336 | animate: false, 337 | }); 338 | } 339 | } 340 | } 341 | } 342 | 343 | customElements.define("leafy-feature-group", FeatureGroup); 344 | 345 | // Marker 346 | 347 | class Marker extends InteractiveFeature { 348 | static get observedAttributes() { 349 | return ["lat", "lng", ...Feature.observedAttributes]; 350 | } 351 | 352 | get lat() { 353 | return this.getAttribute("lat") || 0; 354 | } 355 | get lng() { 356 | return this.getAttribute("lng") || 0; 357 | } 358 | 359 | attributeChangedCallback(name, oldValue, newValue) { 360 | super.attributeChangedCallback(name, oldValue, newValue); 361 | 362 | switch (name) { 363 | case "lat": 364 | case "lng": 365 | this.feature.setLatLng(L.latLng(this.lat, this.lng)); 366 | break; 367 | } 368 | } 369 | 370 | constructor() { 371 | super(); 372 | 373 | this.feature = L.marker([this.lat, this.lng]); 374 | } 375 | } 376 | 377 | customElements.define("leafy-marker", Marker); 378 | 379 | // Path 380 | 381 | class Path extends InteractiveFeature { 382 | static get observedAttributes() { 383 | return [ 384 | "stroke", 385 | "stroke-width", 386 | "stroke-dasharray", 387 | "fill", 388 | "fill-opacity", 389 | "class", 390 | ...Feature.observedAttributes, 391 | ]; 392 | } 393 | 394 | get pathOptions() { 395 | let fill = this.getAttribute("fill"); 396 | return { 397 | color: this.getAttribute("stroke") || "red", 398 | weight: this.getAttribute("stroke-width") || 3, 399 | dashArray: this.getAttribute("stroke-dasharray"), 400 | fill: fill == null ? true : !!fill, 401 | fillColor: fill || "red", 402 | fillOpacity: this.getAttribute("fill-opacity") || 0.2, 403 | className: this.getAttribute("class"), 404 | }; 405 | } 406 | 407 | attributeChangedCallback(name, oldValue, newValue) { 408 | super.attributeChangedCallback(name, oldValue, newValue); 409 | 410 | switch (name) { 411 | case "stroke": 412 | case "stroke-width": 413 | case "stroke-dasharray": 414 | case "fill": 415 | case "fill-opacity": 416 | case "class": 417 | this.feature && this.feature.setStyle(this.pathOptions); 418 | break; 419 | } 420 | } 421 | } 422 | 423 | // Polyline 424 | 425 | class Polyline extends Path { 426 | static get observedAttributes() { 427 | return ["zoom-to-fit", ...Path.observedAttributes]; 428 | } 429 | 430 | get zoomToFit() { 431 | return this.getAttribute("zoom-to-fit") || false; 432 | } 433 | 434 | attributeChangedCallback(name, oldValue, newValue) { 435 | super.attributeChangedCallback(name, oldValue, newValue); 436 | 437 | switch (name) { 438 | case "zoom-to-fit": 439 | if (this.zoomToFit) { 440 | this.fitBounds(); 441 | } 442 | break; 443 | } 444 | } 445 | 446 | get feature() { 447 | return super.feature; 448 | } 449 | set feature(new_feature) { 450 | super.feature = new_feature; 451 | 452 | if (this.zoomToFit) { 453 | this.fitBounds(); 454 | } 455 | } 456 | 457 | get container() { 458 | return super.container; 459 | } 460 | set container(new_container) { 461 | super.container = new_container; 462 | 463 | if (this.zoomToFit) { 464 | this.fitBounds(); 465 | } 466 | } 467 | 468 | fitBounds() { 469 | if (this.container && this.feature) { 470 | let bounds = this.feature.getBounds(); 471 | if (bounds.isValid()) { 472 | this.container.fitBounds(bounds, { 473 | paddingBottomRight: L.point(0, 32), 474 | animate: false, 475 | }); 476 | } 477 | } 478 | } 479 | } 480 | 481 | // leafy-circle 482 | 483 | class Circle extends Polyline { 484 | static get observedAttributes() { 485 | return ["lat", "lng", "radius", ...Polyline.observedAttributes]; 486 | } 487 | 488 | get lat() { 489 | return this.getAttribute("lat") || 0; 490 | } 491 | get lng() { 492 | return this.getAttribute("lng") || 0; 493 | } 494 | get radius() { 495 | return this.getAttribute("radius") || 0; 496 | } 497 | 498 | attributeChangedCallback(name, oldValue, newValue) { 499 | super.attributeChangedCallback(name, oldValue, newValue); 500 | 501 | switch (name) { 502 | case "lat": 503 | case "lng": 504 | this.feature.setLatLng(L.latLng(this.lat, this.lng)); 505 | break; 506 | 507 | case "radius": 508 | this.feature.setRadius(this.radius); 509 | break; 510 | } 511 | } 512 | 513 | constructor() { 514 | super(); 515 | 516 | this.feature = L.circle( 517 | [this.lat, this.lng], 518 | this.radius, 519 | this.pathOptions 520 | ); 521 | } 522 | } 523 | 524 | customElements.define("leafy-circle", Circle); 525 | 526 | // leafy-geojson 527 | 528 | class Geojson extends Polyline { 529 | static get observedAttributes() { 530 | return ["data", ...Polyline.observedAttributes]; 531 | } 532 | 533 | get data() { 534 | try { 535 | return JSON.parse(this.getAttribute("data")); 536 | } catch (e) { 537 | return null; 538 | } 539 | } 540 | 541 | attributeChangedCallback(name, oldValue, newValue) { 542 | super.attributeChangedCallback(name, oldValue, newValue); 543 | 544 | switch (name) { 545 | case "data": 546 | this.feature = L.geoJSON(this.data, this.pathOptions); 547 | break; 548 | } 549 | } 550 | 551 | constructor() { 552 | super(); 553 | 554 | this.feature = L.geoJSON(this.data, this.pathOptions); 555 | } 556 | } 557 | 558 | customElements.define("leafy-geojson", Geojson); 559 | -------------------------------------------------------------------------------- /example/src/static/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+ocaml+reason */ 3 | /** 4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 5 | * Based on https://github.com/chriskempson/tomorrow-theme 6 | * @author Rose Pritchard 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: #ccc; 12 | background: none; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | font-size: 1em; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"] { 35 | padding: 1em; 36 | margin: .5em 0; 37 | overflow: auto; 38 | } 39 | 40 | :not(pre) > code[class*="language-"], 41 | pre[class*="language-"] { 42 | background: #2d2d2d; 43 | } 44 | 45 | /* Inline code */ 46 | :not(pre) > code[class*="language-"] { 47 | padding: .1em; 48 | border-radius: .3em; 49 | white-space: normal; 50 | } 51 | 52 | .token.comment, 53 | .token.block-comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: #999; 58 | } 59 | 60 | .token.punctuation { 61 | color: #ccc; 62 | } 63 | 64 | .token.tag, 65 | .token.attr-name, 66 | .token.namespace, 67 | .token.deleted { 68 | color: #e2777a; 69 | } 70 | 71 | .token.function-name { 72 | color: #6196cc; 73 | } 74 | 75 | .token.boolean, 76 | .token.number, 77 | .token.function { 78 | color: #f08d49; 79 | } 80 | 81 | .token.property, 82 | .token.class-name, 83 | .token.constant, 84 | .token.symbol { 85 | color: #f8c555; 86 | } 87 | 88 | .token.selector, 89 | .token.important, 90 | .token.atrule, 91 | .token.keyword, 92 | .token.builtin { 93 | color: #cc99cd; 94 | } 95 | 96 | .token.string, 97 | .token.char, 98 | .token.attr-value, 99 | .token.regex, 100 | .token.variable { 101 | color: #7ec699; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url { 107 | color: #67cdcc; 108 | } 109 | 110 | .token.important, 111 | .token.bold { 112 | font-weight: bold; 113 | } 114 | .token.italic { 115 | font-style: italic; 116 | } 117 | 118 | .token.entity { 119 | cursor: help; 120 | } 121 | 122 | .token.inserted { 123 | color: green; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /example/src/static/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=clike+ocaml+reason */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,a=0;var _={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof L?new L(e.type,_.util.encode(e.content),e.alias):Array.isArray(e)?e.map(_.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof L)){if(h&&y!=a.length-1){if(c.lastIndex=v,!(x=c.exec(e)))break;for(var b=x.index+(f&&x[1]?x[1].length:0),w=x.index+x[0].length,A=y,P=v,O=a.length;A"+n.content+""},!u.document)return u.addEventListener&&(_.disableWorkerMessageHandler||u.addEventListener("message",function(e){var a=JSON.parse(e.data),n=a.language,r=a.code,t=a.immediateClose;u.postMessage(_.highlight(r,_.languages[n],n)),t&&u.close()},!1)),_;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();if(e&&(_.filename=e.src,e.hasAttribute("data-manual")&&(_.manual=!0)),!_.manual){function n(){_.manual||_.highlightAll()}"loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n):window.setTimeout(n,16):document.addEventListener("DOMContentLoaded",n)}return _}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 5 | Prism.languages.ocaml={comment:/\(\*[\s\S]*?\*\)/,string:[{pattern:/"(?:\\.|[^\\\r\n"])*"/,greedy:!0},{pattern:/(['`])(?:\\(?:\d+|x[\da-f]+|.)|(?!\1)[^\\\r\n])\1/i,greedy:!0}],number:/\b(?:0x[\da-f][\da-f_]+|(?:0[bo])?\d[\d_]*\.?[\d_]*(?:e[+-]?[\d_]+)?)/i,type:{pattern:/\B['`]\w*/,alias:"variable"},directive:{pattern:/\B#\w+/,alias:"function"},keyword:/\b(?:as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|match|method|module|mutable|new|object|of|open|prefix|private|rec|then|sig|struct|to|try|type|val|value|virtual|where|while|with)\b/,boolean:/\b(?:false|true)\b/,operator:/:=|[=<>@^|&+\-*\/$%!?~][!$%&*+\-.\/:<=>?@^|~]*|\b(?:and|asr|land|lor|lxor|lsl|lsr|mod|nor|or)\b/,punctuation:/[(){}\[\]|_.,:;]/}; 6 | Prism.languages.reason=Prism.languages.extend("clike",{comment:{pattern:/(^|[^\\])\/\*[\s\S]*?\*\//,lookbehind:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},"class-name":/\b[A-Z]\w*/,keyword:/\b(?:and|as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|method|module|mutable|new|nonrec|object|of|open|or|private|rec|sig|struct|switch|then|to|try|type|val|virtual|when|while|with)\b/,operator:/\.{3}|:[:=]|\|>|->|=(?:==?|>)?|<=?|>=?|[|^?'#!~`]|[+\-*\/]\.?|\b(?:mod|land|lor|lxor|lsl|lsr|asr)\b/}),Prism.languages.insertBefore("reason","class-name",{character:{pattern:/'(?:\\x[\da-f]{2}|\\o[0-3][0-7][0-7]|\\\d{3}|\\.|[^'\\\r\n])'/,alias:"string"},constructor:{pattern:/\b[A-Z]\w*\b(?!\s*\.)/,alias:"variable"},label:{pattern:/\b[a-z]\w*(?=::)/,alias:"symbol"}}),delete Prism.languages.reason.function; 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const outputDir = path.join(__dirname, 'build/'); 4 | 5 | const isProd = process.env.NODE_ENV === 'production'; 6 | 7 | module.exports = { 8 | entry: '../_build/default/example/src/App.bc.js', 9 | mode: isProd ? 'production' : 'development', 10 | resolve: { 11 | modules: [path.resolve(__dirname, 'node_modules'), 'node_modules'] 12 | }, 13 | output: { 14 | path: outputDir, 15 | filename: 'Index.js' 16 | }, 17 | node: { 18 | fs: 'empty', 19 | child_process: 'empty' 20 | }, 21 | plugins: [ 22 | new HtmlWebpackPlugin({ 23 | template: 'src/index.html', 24 | inject: false 25 | }) 26 | ], 27 | devServer: { 28 | compress: true, 29 | contentBase: outputDir, 30 | port: process.env.PORT || 8000, 31 | historyApiFallback: true 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.css$/i, 37 | use: ['style-loader', 'css-loader'], 38 | }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /interop.md: -------------------------------------------------------------------------------- 1 | ## js_of_ocaml: Interop with JavaScript 2 | 3 | There are three approaches to interop with JavaScript code in Js_of_ocaml: 4 | 5 | 1. Use the functions available in the [`Js` module](http://ocsigen.org/js_of_ocaml/3.1.0/api/Js) in order to translate the data inside Reason/OCaml files, so it can be used from JavaScript. 6 | 2. Use OCaml `external` statements to define the types, and then do the transformations on the JavaScript side. External functions are the same way that OCaml offers to [interop with C code](https://caml.inria.fr/pub/docs/manual-ocaml/intfc.html) as well. 7 | 3. Use `gen_js_api` and create interface files so that the tool generates all the mapping code for you :) 8 | 9 | The approach being used in `jsoo-react` is the third, as it automates all the conversions needed and doesn't require to think about them 10 | 11 | ### gen_js_api: Comparison with BuckleScript 12 | 13 | Some notes on how interop in BuckleScript and gen_js_api relate: 14 | 15 | | Feature | BuckleScript interop | gen_js_api interface files | 16 | | ---------------------------- | ----------------- | ---------------------------------------- | 17 | | Use JS data types | `array`, `string` | No work needed, conversions will be added in generated code automatically | 18 | | Require JS modules | `[@bs.module]` | Add `require("my-lib")` statement in `.js` file ([example](https://github.com/ml-in-barcelona/jsoo-react/blob/3a69759eaf7a777b8b006422b829c8e0fdcc94cf/lib/ReactJs.js)), include it in Dune ([example](https://github.com/ml-in-barcelona/jsoo-react/blob/3a69759eaf7a777b8b006422b829c8e0fdcc94cf/lib/dune#L5)) | 19 | | Bind to JS values | `[@bs.val]` | `[@js.global]` | 20 | | Variadic params | `[@bs.splice]` | `[@js.variadic]` | 21 | | Raw expressions | `[%bs.raw]` | `Js.Unsafe.js_expr();` | 22 | | Create JS objects | `[%bs.obj]` | `[@js.builder]` | 23 | | Access JS objects properties | `##` | `##.` | 24 | | Nullable values | `Js.nullable` | No work needed, conversions will be added in generated code automatically | 25 | 26 | For more information about `gen_js_api`, check the [project documentation](https://github.com/LexiFi/gen_js_api#documentation). 27 | -------------------------------------------------------------------------------- /jsoo-react.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Bindings to ReactJS for js_of_ocaml, including JSX ppx" 4 | maintainer: ["Javier Chávarri "] 5 | authors: ["Javier Chávarri "] 6 | license: "MIT" 7 | homepage: "https://github.com/ml-in-barcelona/jsoo-react" 8 | bug-reports: "https://github.com/ml-in-barcelona/jsoo-react/issues" 9 | depends: [ 10 | "ocaml" {>= "4.12.0"} 11 | "js_of_ocaml" {>= "4.0.0"} 12 | "gen_js_api" {>= "1.0.8"} 13 | "ppxlib" {>= "0.23.0"} 14 | "webtest" {with-test} 15 | "webtest-js" {with-test} 16 | "js_of_ocaml-ppx" {with-test} 17 | "conf-npm" {with-test} 18 | "ocamlformat" {= "0.21.0" & with-test} 19 | "reason" {= "3.8.2" & with-test} 20 | "ppx_blob" {with-test} 21 | "js_of_ocaml-lwt" {with-test} 22 | "odoc" {with-doc} 23 | ] 24 | build: [ 25 | ["dune" "subst"] {dev} 26 | [ 27 | "dune" 28 | "build" 29 | "-p" 30 | name 31 | "-j" 32 | jobs 33 | "@install" 34 | "@doc" {with-doc} 35 | ] 36 | ] 37 | dev-repo: "git+https://github.com/ml-in-barcelona/jsoo-react.git" 38 | -------------------------------------------------------------------------------- /lib/dom_aria_attributes.ml: -------------------------------------------------------------------------------- 1 | include Dom_dsl_core.Prop 2 | 3 | (* All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/ *) 4 | 5 | (* Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. *) 6 | let ariaActivedescendant = string "aria-activedescendant" 7 | 8 | (* Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. *) 9 | let ariaAtomic = string (* Bool | 'false' | 'true' *) "aria-atomic" 10 | 11 | (* Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be 12 | * presented if they are made. 13 | *) 14 | let ariaAutocomplete = 15 | string (* 'none' | 'inline' | 'list' | 'both' *) "aria-autocomplete" 16 | 17 | (* Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. *) 18 | let ariaBusy = string (* Bool | 'false' | 'true' *) "aria-busy" 19 | 20 | (* Indicates the current "checked" state of checkboxes, radio buttons, and other widgets. 21 | * @see aria-pressed @see aria-selected. 22 | *) 23 | let ariaChecked = string (* Bool | 'false' | 'mixed' | 'true' *) "aria-checked" 24 | 25 | (* Defines the total number of columns in a table, grid, or treegrid. 26 | * @see aria-colindex. 27 | *) 28 | let ariaColcount = int "aria-colcount" 29 | 30 | (* Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid. 31 | * @see aria-colcount @see aria-colspan. 32 | *) 33 | let ariaColindex = int "aria-colindex" 34 | 35 | (* Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid. 36 | * @see aria-colindex @see aria-rowspan. 37 | *) 38 | let ariaColspan = int "aria-colspan" 39 | 40 | (* Identifies the element (or elements) whose contents or presence are controlled by the current element. 41 | * @see aria-owns. 42 | *) 43 | let ariaControls = string "aria-controls" 44 | 45 | (* Indicates the element that represents the current item within a container or set of related elements. *) 46 | let ariaCurrent = string "aria-current" 47 | 48 | (* Bool | 'false' | 'true' | 'page' | 'step' | 'location' | 'date' | 'time' *) 49 | (* Identifies the element (or elements) that describes the object. 50 | * @see aria-labelledby 51 | *) 52 | let ariaDescribedby = string "aria-describedby" 53 | 54 | (* Identifies the element that provides a detailed, extended description for the object. 55 | * @see aria-describedby. 56 | *) 57 | let ariaDetails = string "aria-details" 58 | 59 | (* Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable. 60 | * @see aria-hidden @see aria-readonly. 61 | *) 62 | let ariaDisabled = string (* Bool | 'false' | 'true' *) "aria-disabled" 63 | 64 | (* Indicates what functions can be performed when a dragged object is released on the drop target. 65 | * @deprecated in ARIA 1.1 66 | *) 67 | let ariaDropeffect = string "aria-dropeffect" 68 | 69 | (* 'none' | 'copy' | 'execute' | 'link' | 'move' | 'popup' *) 70 | (* Identifies the element that provides an error message for the object. 71 | * @see aria-invalid @see aria-describedby. 72 | *) 73 | let ariaErrormessage = string "aria-errormessage" 74 | 75 | (* Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. *) 76 | let ariaExpanded = string (* Bool | 'false' | 'true' *) "aria-expanded" 77 | 78 | (* Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion, 79 | * allows assistive technology to override the general default of reading in document source order. 80 | *) 81 | let ariaFlowto = string "aria-flowto" 82 | 83 | (* Indicates an element's "grabbed" state in a drag-and-drop operation. 84 | * @deprecated in ARIA 1.1 85 | *) 86 | let ariaGrabbed = string (* Bool | 'false' | 'true' *) "aria-grabbed" 87 | 88 | (* Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. *) 89 | let ariaHaspopup = 90 | string 91 | (* Bool | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'; *) 92 | "aria-haspopup" 93 | 94 | (* Indicates whether the element is exposed to an accessibility API. 95 | * @see aria-disabled. 96 | *) 97 | let ariaHidden = string (* Bool | 'false' | 'true' *) "aria-hidden" 98 | 99 | (* Indicates the entered value does not conform to the format expected by the application. 100 | * @see aria-errormessage. 101 | *) 102 | let ariaInvalid = 103 | string (* Bool | 'false' | 'true' | 'grammar' | 'spelling'; *) "aria-invalid" 104 | 105 | (* Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. *) 106 | let ariaKeyshortcuts = string "aria-keyshortcuts" 107 | 108 | (* Defines a String value that labels the current element. 109 | * @see aria-labelledby. 110 | *) 111 | let ariaLabel = string "aria-label" 112 | 113 | (* Identifies the element (or elements) that labels the current element. 114 | * @see aria-describedby. 115 | *) 116 | let ariaLabelledby = string "aria-labelledby" 117 | 118 | (* Defines the hierarchical level of an element within a structure. *) 119 | let ariaLevel = int "aria-level" 120 | 121 | (* Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect ;rom the live region. *) 122 | let ariaLive = string (* 'off' | 'assertive' | 'polite' *) "aria-live" 123 | 124 | (* Indicates whether an element is modal when displayed. *) 125 | let ariaModal = string (* Bool | 'false' | 'true' *) "aria-modal" 126 | 127 | (* Indicates whether a text box accepts multiple lines of input or only a single line. *) 128 | let ariaMultiline = string (* Bool | 'false' | 'true' *) "aria-multiline" 129 | 130 | (* Indicates that the user may select more than one item from the current selectable descendants. *) 131 | let ariaMultiselectable = 132 | string (* Bool | 'false' | 'true' *) "aria-multiselectable" 133 | 134 | (* Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. *) 135 | let ariaOrientation = string (* 'horizontal' | 'vertical' *) "aria-orientation" 136 | 137 | (* Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship 138 | * between DOM elements where the DOM hierarchy cannot be used to represent the relationship. 139 | * @see aria-controls. 140 | *) 141 | let ariaOwns = string "aria-owns" 142 | 143 | (* Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value. 144 | * A hint could be a sample value or a brief description of the expected format. 145 | *) 146 | let ariaPlaceholder = string "aria-placeholder" 147 | 148 | (* Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM. 149 | * @see aria-setsize. 150 | *) 151 | let ariaPosinset = int "aria-posinset" 152 | 153 | (* Indicates the current "pressed" state of toggle buttons. 154 | * @see aria-checked @see aria-selected. 155 | *) 156 | let ariaPressed = string (* Bool | 'false' | 'mixed' | 'true' *) "aria-pressed" 157 | 158 | (* Indicates that the element is not editable, but is otherwise operable. 159 | * @see aria-disabled. 160 | *) 161 | let ariaReadonly = string (* Bool | 'false' | 'true' *) "aria-readonly" 162 | 163 | (* Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified. 164 | * @see aria-atomic. 165 | *) 166 | let ariaRelevant = 167 | string 168 | (* 'additions' | 'additions removals' | 'additions text' | 'all' | 'removals' | 'removals additions' | 'removals text' | 'text' | 'text additions' | 'text removals' *) 169 | "aria-relevant" 170 | 171 | (* Indicates that user input is required on the element before a form may be submitted. *) 172 | let ariaRequired = string (* Bool | 'false' | 'true' *) "aria-required" 173 | 174 | (* Defines a human-readable, author-localized description for the role of an element. *) 175 | let ariaRoledescription = string "aria-roledescription" 176 | 177 | (* Defines the total number of rows in a table, grid, or treegrid. 178 | * @see aria-rowindex. 179 | *) 180 | let ariaRowcount = int "aria-rowcount" 181 | 182 | (* Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid. 183 | * @see aria-rowcount @see aria-rowspan. 184 | *) 185 | let ariaRowindex = int "aria-rowindex" 186 | 187 | (* Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid. 188 | * @see aria-rowindex @see aria-colspan. 189 | *) 190 | let ariaRowspan = int "aria-rowspan" 191 | 192 | (* Indicates the current "selected" state of various widgets. 193 | * @see aria-checked @see aria-pressed. 194 | *) 195 | let ariaSelected = string (* Bool | 'false' | 'true' *) "aria-selected" 196 | 197 | (* Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM. 198 | * @see aria-posinset. 199 | *) 200 | let ariaSetsize = int "aria-setsize" 201 | 202 | (* Indicates if items in a table or grid are sorted in ascending or descending order. *) 203 | let ariaSort = 204 | string (* 'none' | 'ascending' | 'descending' | 'other' *) "aria-sort" 205 | 206 | (* Defines the maximum allowed value for a range widget. *) 207 | let ariaValuemax = int "aria-valuemax" 208 | 209 | (* Defines the minimum allowed value for a range widget. *) 210 | let ariaValuemin = int "aria-valuemin" 211 | 212 | (* Defines the current value for a range widget. 213 | * @see aria-valuetext. 214 | *) 215 | let ariaValuenow = int "aria-valuenow" 216 | 217 | (* Defines the human readable text alternative of aria-valuenow for a range widget. *) 218 | let ariaValuetext = string "aria-valuetext" 219 | -------------------------------------------------------------------------------- /lib/dom_aria_attributes.mli: -------------------------------------------------------------------------------- 1 | type t = Dom_dsl_core.Prop.t 2 | 3 | val any : string -> 'a -> t 4 | val string : string -> string -> t 5 | val bool : string -> bool -> t 6 | val int : string -> int -> t 7 | val float_ : string -> float -> t 8 | val event : string -> ('a Event.synthetic -> unit) -> t 9 | val maybe : ('a -> t) -> 'a option -> t 10 | val key : string -> t 11 | val ref_ : Dom.dom_ref -> t 12 | val ariaActivedescendant : string -> t 13 | val ariaAtomic : string -> t 14 | val ariaAutocomplete : string -> t 15 | val ariaBusy : string -> t 16 | val ariaChecked : string -> t 17 | val ariaColcount : int -> t 18 | val ariaColindex : int -> t 19 | val ariaColspan : int -> t 20 | val ariaControls : string -> t 21 | val ariaCurrent : string -> t 22 | val ariaDescribedby : string -> t 23 | val ariaDetails : string -> t 24 | val ariaDisabled : string -> t 25 | val ariaDropeffect : string -> t 26 | val ariaErrormessage : string -> t 27 | val ariaExpanded : string -> t 28 | val ariaFlowto : string -> t 29 | val ariaGrabbed : string -> t 30 | val ariaHaspopup : string -> t 31 | val ariaHidden : string -> t 32 | val ariaInvalid : string -> t 33 | val ariaKeyshortcuts : string -> t 34 | val ariaLabel : string -> t 35 | val ariaLabelledby : string -> t 36 | val ariaLevel : int -> t 37 | val ariaLive : string -> t 38 | val ariaModal : string -> t 39 | val ariaMultiline : string -> t 40 | val ariaMultiselectable : string -> t 41 | val ariaOrientation : string -> t 42 | val ariaOwns : string -> t 43 | val ariaPlaceholder : string -> t 44 | val ariaPosinset : int -> t 45 | val ariaPressed : string -> t 46 | val ariaReadonly : string -> t 47 | val ariaRelevant : string -> t 48 | val ariaRequired : string -> t 49 | val ariaRoledescription : string -> t 50 | val ariaRowcount : int -> t 51 | val ariaRowindex : int -> t 52 | val ariaRowspan : int -> t 53 | val ariaSelected : string -> t 54 | val ariaSetsize : int -> t 55 | val ariaSort : string -> t 56 | val ariaValuemax : int -> t 57 | val ariaValuemin : int -> t 58 | val ariaValuenow : int -> t 59 | val ariaValuetext : string -> t 60 | -------------------------------------------------------------------------------- /lib/dom_dsl_core.ml: -------------------------------------------------------------------------------- 1 | module Prop = struct 2 | type t = string * Js_of_ocaml.Js.Unsafe.any 3 | 4 | let any key value = (key, Js_of_ocaml.Js.Unsafe.inject value) 5 | let string key value = any key (Js_of_ocaml.Js.string value) 6 | let bool key value = any key (Js_of_ocaml.Js.bool value) 7 | let int key (value : int) = any key value 8 | let float_ key (value : float) = any key value 9 | 10 | let event key (f : _ Event.synthetic -> unit) = 11 | any key (Js_of_ocaml.Js.wrap_callback f) 12 | 13 | let maybe prop = function 14 | | Some value -> prop value 15 | | None -> any "" Js_of_ocaml.Js.undefined 16 | 17 | let key = string "key" 18 | let ref_ = (any "ref" : Dom.dom_ref -> t) 19 | end 20 | 21 | module Element = struct 22 | let h name props children = 23 | Dom.create_element name ~props:(Js_of_ocaml.Js.Unsafe.obj props) children 24 | end 25 | 26 | module Common = struct 27 | module Context = struct 28 | module Provider = struct 29 | let make context ~value children = 30 | Core.Context.Provider.make context ~value ~children () 31 | end 32 | end 33 | 34 | let fragment children = Core.Fragment.make ~children () 35 | let strict_mode children = Core.StrictMode.make ~children () 36 | let none = fragment [] 37 | let string = Core.string 38 | let int = Core.int 39 | let float = Core.float 40 | let maybe f = function Some value -> f value | None -> none 41 | end 42 | -------------------------------------------------------------------------------- /lib/dom_dsl_core.mli: -------------------------------------------------------------------------------- 1 | module Prop : sig 2 | type t 3 | 4 | val any : string -> _ -> t 5 | val string : string -> string -> t 6 | val bool : string -> bool -> t 7 | val int : string -> int -> t 8 | val float_ : string -> float -> t 9 | val event : string -> (_ Event.synthetic -> unit) -> t 10 | val maybe : ('a -> t) -> 'a option -> t 11 | val key : string -> t 12 | val ref_ : Dom.dom_ref -> t 13 | end 14 | 15 | module Element : sig 16 | val h : string -> Prop.t array -> Core.element list -> Core.element 17 | end 18 | 19 | module Common : sig 20 | module Context : sig 21 | module Provider : sig 22 | val make : 23 | 'a Core.Context.t -> value:'a -> Core.element list -> Core.element 24 | end 25 | end 26 | 27 | val fragment : Core.element list -> Core.element 28 | val strict_mode : Core.element list -> Core.element 29 | val none : Core.element 30 | val string : string -> Core.element 31 | val int : int -> Core.element 32 | val float : float -> Core.element 33 | val maybe : ('a -> Core.element) -> 'a option -> Core.element 34 | end 35 | -------------------------------------------------------------------------------- /lib/dom_svg.ml: -------------------------------------------------------------------------------- 1 | module Prop = struct 2 | include Dom_dsl_core.Prop 3 | include Dom_aria_attributes 4 | 5 | open struct 6 | let string_prop = string 7 | end 8 | 9 | let accentHeight = string_prop "accentHeight" 10 | let accumulate = string_prop "accumulate" 11 | let additive = string_prop "additive" 12 | let alignmentBaseline = string_prop "alignmentBaseline" 13 | let allowReorder = string_prop "allowReorder" 14 | let alphabetic = string_prop "alphabetic" 15 | let amplitude = string_prop "amplitude" 16 | let arabicForm = string_prop "arabicForm" 17 | let ascent = string_prop "ascent" 18 | let attributeName = string_prop "attributeName" 19 | let attributeType = string_prop "attributeType" 20 | let autoReverse = string_prop "autoReverse" 21 | let azimuth = string_prop "azimuth" 22 | let baseFrequency = string_prop "baseFrequency" 23 | let baseProfile = string_prop "baseProfile" 24 | let baselineShift = string_prop "baselineShift" 25 | let bbox = string_prop "bbox" 26 | let begin_ = string_prop "begin" (* reserved keyword *) 27 | let bias = string_prop "bias" 28 | let by = string_prop "by" 29 | let calcMode = string_prop "calcMode" 30 | let capHeight = string_prop "capHeight" 31 | let className = string_prop "className" (* substitute for "class" *) 32 | let classNames names = className (String.concat " " names) 33 | let clip = string_prop "clip" 34 | let clipPath = string_prop "clipPath" 35 | let clipPathUnits = string_prop "clipPathUnits" 36 | let clipRule = string_prop "clipRule" 37 | let colorInterpolation = string_prop "colorInterpolation" 38 | let colorInterpolationFilters = string_prop "colorInterpolationFilters" 39 | let colorProfile = string_prop "colorProfile" 40 | let colorRendering = string_prop "colorRendering" 41 | let contentScriptType = string_prop "contentScriptType" 42 | let contentStyleType = string_prop "contentStyleType" 43 | let cursor = string_prop "cursor" 44 | let cx = string_prop "cx" 45 | let cy = string_prop "cy" 46 | let d = string_prop "d" 47 | let decelerate = string_prop "decelerate" 48 | let descent = string_prop "descent" 49 | let diffuseConstant = string_prop "diffuseConstant" 50 | let direction = string_prop "direction" 51 | let display = string_prop "display" 52 | let divisor = string_prop "divisor" 53 | let dominantBaseline = string_prop "dominantBaseline" 54 | let dur = string_prop "dur" 55 | let dx = string_prop "dx" 56 | let dy = string_prop "dy" 57 | let edgeMode = string_prop "edgeMode" 58 | let elevation = string_prop "elevation" 59 | let enableBackground = string_prop "enableBackground" 60 | let end_ = string_prop "end" (* reserved keyword *) 61 | let exponent = string_prop "exponent" 62 | let externalResourcesRequired = string_prop "externalResourcesRequired" 63 | let fill = string_prop "fill" 64 | let fillOpacity = string_prop "fillOpacity" 65 | let fillRule = string_prop "fillRule" 66 | let filter = string_prop "filter" 67 | let filterRes = string_prop "filterRes" 68 | let filterUnits = string_prop "filterUnits" 69 | let floodColor = string_prop "floodColor" 70 | let floodOpacity = string_prop "floodOpacity" 71 | let focusable = string_prop "focusable" 72 | let fontFamily = string_prop "fontFamily" 73 | let fontSize = string_prop "fontSize" 74 | let fontSizeAdjust = string_prop "fontSizeAdjust" 75 | let fontStretch = string_prop "fontStretch" 76 | let fontStyle = string_prop "fontStyle" 77 | let fontVariant = string_prop "fontVariant" 78 | let fontWeight = string_prop "fontWeight" 79 | let fomat = string_prop "fomat" 80 | let from = string_prop "from" 81 | let fx = string_prop "fx" 82 | let fy = string_prop "fy" 83 | let g1 = string_prop "g1" 84 | let g2 = string_prop "g2" 85 | let glyphName = string_prop "glyphName" 86 | let glyphOrientationHorizontal = string_prop "glyphOrientationHorizontal" 87 | let glyphOrientationVertical = string_prop "glyphOrientationVertical" 88 | let glyphRef = string_prop "glyphRef" 89 | let gradientTransform = string_prop "gradientTransform" 90 | let gradientUnits = string_prop "gradientUnits" 91 | let hanging = string_prop "hanging" 92 | let height = string_prop "height" 93 | let horizAdvX = string_prop "horizAdvX" 94 | let horizOriginX = string_prop "horizOriginX" 95 | let href = string_prop "href" 96 | let id = string_prop "id" 97 | let ideographic = string_prop "ideographic" 98 | let imageRendering = string_prop "imageRendering" 99 | let in_ = string_prop "in" (* reserved keyword *) 100 | let in2 = string_prop "in2" 101 | let intercept = string_prop "intercept" 102 | let k = string_prop "k" 103 | let k1 = string_prop "k1" 104 | let k2 = string_prop "k2" 105 | let k3 = string_prop "k3" 106 | let k4 = string_prop "k4" 107 | let kernelMatrix = string_prop "kernelMatrix" 108 | let kernelUnitLength = string_prop "kernelUnitLength" 109 | let kerning = string_prop "kerning" 110 | let keyPoints = string_prop "keyPoints" 111 | let keySplines = string_prop "keySplines" 112 | let keyTimes = string_prop "keyTimes" 113 | let lang = string_prop "lang" 114 | let lengthAdjust = string_prop "lengthAdjust" 115 | let letterSpacing = string_prop "letterSpacing" 116 | let lightingColor = string_prop "lightingColor" 117 | let limitingConeAngle = string_prop "limitingConeAngle" 118 | let local = string_prop "local" 119 | let markerEnd = string_prop "markerEnd" 120 | let markerHeight = string_prop "markerHeight" 121 | let markerMid = string_prop "markerMid" 122 | let markerStart = string_prop "markerStart" 123 | let markerUnits = string_prop "markerUnits" 124 | let markerWidth = string_prop "markerWidth" 125 | let mask = string_prop "mask" 126 | let maskContentUnits = string_prop "maskContentUnits" 127 | let maskUnits = string_prop "maskUnits" 128 | let mathematical = string_prop "mathematical" 129 | let mode = string_prop "mode" 130 | let numOctaves = string_prop "numOctaves" 131 | let offset = string_prop "offset" 132 | let opacity = string_prop "opacity" 133 | let operator = string_prop "operator" 134 | let order = string_prop "order" 135 | let orient = string_prop "orient" 136 | let orientation = string_prop "orientation" 137 | let origin = string_prop "origin" 138 | let overflow = string_prop "overflow" 139 | let overflowX = string_prop "overflowX" 140 | let overflowY = string_prop "overflowY" 141 | let overlinePosition = string_prop "overlinePosition" 142 | let overlineThickness = string_prop "overlineThickness" 143 | let paintOrder = string_prop "paintOrder" 144 | let panose1 = string_prop "panose1" 145 | let pathLength = string_prop "pathLength" 146 | let patternContentUnits = string_prop "patternContentUnits" 147 | let patternTransform = string_prop "patternTransform" 148 | let patternUnits = string_prop "patternUnits" 149 | let pointerEvents = string_prop "pointerEvents" 150 | let points = string_prop "points" 151 | let pointsAtX = string_prop "pointsAtX" 152 | let pointsAtY = string_prop "pointsAtY" 153 | let pointsAtZ = string_prop "pointsAtZ" 154 | let preserveAlpha = string_prop "preserveAlpha" 155 | let preserveAspectRatio = string_prop "preserveAspectRatio" 156 | let primitiveUnits = string_prop "primitiveUnits" 157 | let r = string_prop "r" 158 | let radius = string_prop "radius" 159 | let refX = string_prop "refX" 160 | let refY = string_prop "refY" 161 | let renderingIntent = string_prop "renderingIntent" 162 | let repeatCount = string_prop "repeatCount" 163 | let repeatDur = string_prop "repeatDur" 164 | let requiredExtensions = string_prop "requiredExtensions" 165 | let requiredFeatures = string_prop "requiredFeatures" 166 | let restart = string_prop "restart" 167 | let result = string_prop "result" 168 | let rotate = string_prop "rotate" 169 | let rx = string_prop "rx" 170 | let ry = string_prop "ry" 171 | let scale = string_prop "scale" 172 | let seed = string_prop "seed" 173 | let shapeRendering = string_prop "shapeRendering" 174 | let slope = string_prop "slope" 175 | let spacing = string_prop "spacing" 176 | let specularConstant = string_prop "specularConstant" 177 | let specularExponent = string_prop "specularExponent" 178 | let speed = string_prop "speed" 179 | let spreadMethod = string_prop "spreadMethod" 180 | let startOffset = string_prop "startOffset" 181 | let stdDeviation = string_prop "stdDeviation" 182 | let stemh = string_prop "stemh" 183 | let stemv = string_prop "stemv" 184 | let stitchTiles = string_prop "stitchTiles" 185 | let stopColor = string_prop "stopColor" 186 | let stopOpacity = string_prop "stopOpacity" 187 | let strikethroughPosition = string_prop "strikethroughPosition" 188 | let strikethroughThickness = string_prop "strikethroughThickness" 189 | let string = string_prop "string" 190 | let stroke = string_prop "stroke" 191 | let strokeDasharray = string_prop "strokeDasharray" 192 | let strokeDashoffset = string_prop "strokeDashoffset" 193 | let strokeLinecap = string_prop "strokeLinecap" 194 | let strokeLinejoin = string_prop "strokeLinejoin" 195 | let strokeMiterlimit = string_prop "strokeMiterlimit" 196 | let strokeOpacity = string_prop "strokeOpacity" 197 | let strokeWidth = string_prop "strokeWidth" 198 | let style = (any "style" : Dom.Style.t -> t) 199 | let surfaceScale = string_prop "surfaceScale" 200 | let systemLanguage = string_prop "systemLanguage" 201 | let tabIndex = int "tabIndex" 202 | let tableValues = string_prop "tableValues" 203 | let targetX = string_prop "targetX" 204 | let targetY = string_prop "targetY" 205 | let textAnchor = string_prop "textAnchor" 206 | let textDecoration = string_prop "textDecoration" 207 | let textLength = string_prop "textLength" 208 | let textRendering = string_prop "textRendering" 209 | let to_ = string_prop "to" (* reserved keyword *) 210 | let transform = string_prop "transform" 211 | let u1 = string_prop "u1" 212 | let u2 = string_prop "u2" 213 | let underlinePosition = string_prop "underlinePosition" 214 | let underlineThickness = string_prop "underlineThickness" 215 | let unicode = string_prop "unicode" 216 | let unicodeBidi = string_prop "unicodeBidi" 217 | let unicodeRange = string_prop "unicodeRange" 218 | let unitsPerEm = string_prop "unitsPerEm" 219 | let vAlphabetic = string_prop "vAlphabetic" 220 | let vHanging = string_prop "vHanging" 221 | let vIdeographic = string_prop "vIdeographic" 222 | let vMathematical = string_prop "vMathematical" 223 | let values = string_prop "values" 224 | let vectorEffect = string_prop "vectorEffect" 225 | let version = string_prop "version" 226 | let vertAdvX = string_prop "vertAdvX" 227 | let vertAdvY = string_prop "vertAdvY" 228 | let vertOriginX = string_prop "vertOriginX" 229 | let vertOriginY = string_prop "vertOriginY" 230 | let viewBox = string_prop "viewBox" 231 | let viewTarget = string_prop "viewTarget" 232 | let visibility = string_prop "visibility" 233 | let width = string_prop "width" 234 | let widths = string_prop "widths" 235 | let wordSpacing = string_prop "wordSpacing" 236 | let writingMode = string_prop "writingMode" 237 | let x = string_prop "x" 238 | let x1 = string_prop "x1" 239 | let x2 = string_prop "x2" 240 | let xChannelSelector = string_prop "xChannelSelector" 241 | let xHeight = string_prop "xHeight" 242 | let xlinkActuate = string_prop "xlinkActuate" 243 | let xlinkArcrole = string_prop "xlinkArcrole" 244 | let xlinkHref = string_prop "xlinkHref" 245 | let xlinkRole = string_prop "xlinkRole" 246 | let xlinkShow = string_prop "xlinkShow" 247 | let xlinkTitle = string_prop "xlinkTitle" 248 | let xlinkType = string_prop "xlinkType" 249 | let xmlns = string_prop "xmlns" 250 | let xmlnsXlink = string_prop "xmlnsXlink" 251 | let xmlBase = string_prop "xmlBase" 252 | let xmlLang = string_prop "xmlLang" 253 | let xmlSpace = string_prop "xmlSpace" 254 | let y = string_prop "y" 255 | let y1 = string_prop "y1" 256 | let y2 = string_prop "y2" 257 | let yChannelSelector = string_prop "yChannelSelector" 258 | let z = string_prop "z" 259 | let zoomAndPan = string_prop "zoomAndPan" 260 | end 261 | 262 | include Prop 263 | include Dom_dsl_core.Element 264 | include Dom_dsl_core.Common 265 | 266 | let a = h "a" 267 | let animate = h "animate" 268 | let animateMotion = h "animateMotion" 269 | let animateTransform = h "animateTransform" 270 | let circle = h "circle" 271 | let clipPath = h "clipPath" 272 | let defs = h "defs" 273 | let desc = h "desc" 274 | let discard = h "discard" 275 | let ellipse = h "ellipse" 276 | let feBlend = h "feBlend" 277 | let feColorMatrix = h "feColorMatrix" 278 | let feComponentTransfer = h "feComponentTransfer" 279 | let feComposite = h "feComposite" 280 | let feConvolveMatrix = h "feConvolveMatrix" 281 | let feDiffuseLighting = h "feDiffuseLighting" 282 | let feDisplacementMap = h "feDisplacementMap" 283 | let feDistantLight = h "feDistantLight" 284 | let feDropShadow = h "feDropShadow" 285 | let feFlood = h "feFlood" 286 | let feFuncA = h "feFuncA" 287 | let feFuncB = h "feFuncB" 288 | let feFuncG = h "feFuncG" 289 | let feFuncR = h "feFuncR" 290 | let feGaussianBlur = h "feGaussianBlur" 291 | let feImage = h "feImage" 292 | let feMerge = h "feMerge" 293 | let feMergeNode = h "feMergeNode" 294 | let feMorphology = h "feMorphology" 295 | let feOffset = h "feOffset" 296 | let fePointLight = h "fePointLight" 297 | let feSpecularLighting = h "feSpecularLighting" 298 | let feSpotLight = h "feSpotLight" 299 | let feTile = h "feTile" 300 | let feTurbulence = h "feTurbulence" 301 | let filter = h "filter" 302 | let foreignObject = h "foreignObject" 303 | let g = h "g" 304 | let hatch = h "hatch" 305 | let hatchpath = h "hatchpath" 306 | let image = h "image" 307 | let line = h "line" 308 | let linearGradient = h "linearGradient" 309 | let marker = h "marker" 310 | let mask = h "mask" 311 | let metadata = h "metadata" 312 | let mpath = h "mpath" 313 | let path = h "path" 314 | let pattern = h "pattern" 315 | let polygon = h "polygon" 316 | let polyline = h "polyline" 317 | let radialGradient = h "radialGradient" 318 | let rect = h "rect" 319 | let script = h "script" 320 | let set = h "set" 321 | let stop = h "stop" 322 | let style = h "style" 323 | let svg = h "svg" 324 | let switch = h "switch" 325 | let symbol = h "symbol" 326 | let text = h "text" 327 | let textPath = h "textPath" 328 | let title = h "title" 329 | let tspan = h "tspan" 330 | let use = h "use" 331 | let view = h "view" 332 | -------------------------------------------------------------------------------- /lib/dom_svg.mli: -------------------------------------------------------------------------------- 1 | module Prop : sig 2 | include module type of Dom_aria_attributes 3 | 4 | type t = Dom_dsl_core.Prop.t 5 | 6 | (** Common *) 7 | 8 | val any : string -> 'a -> t 9 | val bool : string -> bool -> t 10 | val int : string -> int -> t 11 | val float_ : string -> float -> t 12 | val event : string -> ('a Event.synthetic -> unit) -> t 13 | val key : string -> t 14 | val ref_ : Dom.dom_ref -> t 15 | 16 | (** Modifiers *) 17 | 18 | val maybe : ('a -> t) -> 'a option -> t 19 | 20 | (** SVG props *) 21 | 22 | val accentHeight : string -> t 23 | val accumulate : string -> t 24 | val additive : string -> t 25 | val alignmentBaseline : string -> t 26 | val allowReorder : string -> t 27 | val alphabetic : string -> t 28 | val amplitude : string -> t 29 | val arabicForm : string -> t 30 | val ascent : string -> t 31 | val attributeName : string -> t 32 | val attributeType : string -> t 33 | val autoReverse : string -> t 34 | val azimuth : string -> t 35 | val baseFrequency : string -> t 36 | val baseProfile : string -> t 37 | val baselineShift : string -> t 38 | val bbox : string -> t 39 | val begin_ : string -> t 40 | val bias : string -> t 41 | val by : string -> t 42 | val calcMode : string -> t 43 | val capHeight : string -> t 44 | val className : string -> t 45 | val classNames : string list -> t 46 | val clip : string -> t 47 | val clipPath : string -> t 48 | val clipPathUnits : string -> t 49 | val clipRule : string -> t 50 | val colorInterpolation : string -> t 51 | val colorInterpolationFilters : string -> t 52 | val colorProfile : string -> t 53 | val colorRendering : string -> t 54 | val contentScriptType : string -> t 55 | val contentStyleType : string -> t 56 | val cursor : string -> t 57 | val cx : string -> t 58 | val cy : string -> t 59 | val d : string -> t 60 | val decelerate : string -> t 61 | val descent : string -> t 62 | val diffuseConstant : string -> t 63 | val direction : string -> t 64 | val display : string -> t 65 | val divisor : string -> t 66 | val dominantBaseline : string -> t 67 | val dur : string -> t 68 | val dx : string -> t 69 | val dy : string -> t 70 | val edgeMode : string -> t 71 | val elevation : string -> t 72 | val enableBackground : string -> t 73 | val end_ : string -> t 74 | val exponent : string -> t 75 | val externalResourcesRequired : string -> t 76 | val fill : string -> t 77 | val fillOpacity : string -> t 78 | val fillRule : string -> t 79 | val filter : string -> t 80 | val filterRes : string -> t 81 | val filterUnits : string -> t 82 | val floodColor : string -> t 83 | val floodOpacity : string -> t 84 | val focusable : string -> t 85 | val fontFamily : string -> t 86 | val fontSize : string -> t 87 | val fontSizeAdjust : string -> t 88 | val fontStretch : string -> t 89 | val fontStyle : string -> t 90 | val fontVariant : string -> t 91 | val fontWeight : string -> t 92 | val fomat : string -> t 93 | val from : string -> t 94 | val fx : string -> t 95 | val fy : string -> t 96 | val g1 : string -> t 97 | val g2 : string -> t 98 | val glyphName : string -> t 99 | val glyphOrientationHorizontal : string -> t 100 | val glyphOrientationVertical : string -> t 101 | val glyphRef : string -> t 102 | val gradientTransform : string -> t 103 | val gradientUnits : string -> t 104 | val hanging : string -> t 105 | val height : string -> t 106 | val horizAdvX : string -> t 107 | val horizOriginX : string -> t 108 | val href : string -> t 109 | val id : string -> t 110 | val ideographic : string -> t 111 | val imageRendering : string -> t 112 | val in_ : string -> t 113 | val in2 : string -> t 114 | val intercept : string -> t 115 | val k : string -> t 116 | val k1 : string -> t 117 | val k2 : string -> t 118 | val k3 : string -> t 119 | val k4 : string -> t 120 | val kernelMatrix : string -> t 121 | val kernelUnitLength : string -> t 122 | val kerning : string -> t 123 | val keyPoints : string -> t 124 | val keySplines : string -> t 125 | val keyTimes : string -> t 126 | val lang : string -> t 127 | val lengthAdjust : string -> t 128 | val letterSpacing : string -> t 129 | val lightingColor : string -> t 130 | val limitingConeAngle : string -> t 131 | val local : string -> t 132 | val markerEnd : string -> t 133 | val markerHeight : string -> t 134 | val markerMid : string -> t 135 | val markerStart : string -> t 136 | val markerUnits : string -> t 137 | val markerWidth : string -> t 138 | val mask : string -> t 139 | val maskContentUnits : string -> t 140 | val maskUnits : string -> t 141 | val mathematical : string -> t 142 | val mode : string -> t 143 | val numOctaves : string -> t 144 | val offset : string -> t 145 | val opacity : string -> t 146 | val operator : string -> t 147 | val order : string -> t 148 | val orient : string -> t 149 | val orientation : string -> t 150 | val origin : string -> t 151 | val overflow : string -> t 152 | val overflowX : string -> t 153 | val overflowY : string -> t 154 | val overlinePosition : string -> t 155 | val overlineThickness : string -> t 156 | val paintOrder : string -> t 157 | val panose1 : string -> t 158 | val pathLength : string -> t 159 | val patternContentUnits : string -> t 160 | val patternTransform : string -> t 161 | val patternUnits : string -> t 162 | val pointerEvents : string -> t 163 | val points : string -> t 164 | val pointsAtX : string -> t 165 | val pointsAtY : string -> t 166 | val pointsAtZ : string -> t 167 | val preserveAlpha : string -> t 168 | val preserveAspectRatio : string -> t 169 | val primitiveUnits : string -> t 170 | val r : string -> t 171 | val radius : string -> t 172 | val refX : string -> t 173 | val refY : string -> t 174 | val renderingIntent : string -> t 175 | val repeatCount : string -> t 176 | val repeatDur : string -> t 177 | val requiredExtensions : string -> t 178 | val requiredFeatures : string -> t 179 | val restart : string -> t 180 | val result : string -> t 181 | val rotate : string -> t 182 | val rx : string -> t 183 | val ry : string -> t 184 | val scale : string -> t 185 | val seed : string -> t 186 | val shapeRendering : string -> t 187 | val slope : string -> t 188 | val spacing : string -> t 189 | val specularConstant : string -> t 190 | val specularExponent : string -> t 191 | val speed : string -> t 192 | val spreadMethod : string -> t 193 | val startOffset : string -> t 194 | val stdDeviation : string -> t 195 | val stemh : string -> t 196 | val stemv : string -> t 197 | val stitchTiles : string -> t 198 | val stopColor : string -> t 199 | val stopOpacity : string -> t 200 | val strikethroughPosition : string -> t 201 | val strikethroughThickness : string -> t 202 | val string : string -> t 203 | val stroke : string -> t 204 | val strokeDasharray : string -> t 205 | val strokeDashoffset : string -> t 206 | val strokeLinecap : string -> t 207 | val strokeLinejoin : string -> t 208 | val strokeMiterlimit : string -> t 209 | val strokeOpacity : string -> t 210 | val strokeWidth : string -> t 211 | val style : Dom.Style.t -> t 212 | val surfaceScale : string -> t 213 | val systemLanguage : string -> t 214 | val tabIndex : int -> t 215 | val tableValues : string -> t 216 | val targetX : string -> t 217 | val targetY : string -> t 218 | val textAnchor : string -> t 219 | val textDecoration : string -> t 220 | val textLength : string -> t 221 | val textRendering : string -> t 222 | val to_ : string -> t 223 | val transform : string -> t 224 | val u1 : string -> t 225 | val u2 : string -> t 226 | val underlinePosition : string -> t 227 | val underlineThickness : string -> t 228 | val unicode : string -> t 229 | val unicodeBidi : string -> t 230 | val unicodeRange : string -> t 231 | val unitsPerEm : string -> t 232 | val vAlphabetic : string -> t 233 | val vHanging : string -> t 234 | val vIdeographic : string -> t 235 | val vMathematical : string -> t 236 | val values : string -> t 237 | val vectorEffect : string -> t 238 | val version : string -> t 239 | val vertAdvX : string -> t 240 | val vertAdvY : string -> t 241 | val vertOriginX : string -> t 242 | val vertOriginY : string -> t 243 | val viewBox : string -> t 244 | val viewTarget : string -> t 245 | val visibility : string -> t 246 | val width : string -> t 247 | val widths : string -> t 248 | val wordSpacing : string -> t 249 | val writingMode : string -> t 250 | val x : string -> t 251 | val x1 : string -> t 252 | val x2 : string -> t 253 | val xChannelSelector : string -> t 254 | val xHeight : string -> t 255 | val xlinkActuate : string -> t 256 | val xlinkArcrole : string -> t 257 | val xlinkHref : string -> t 258 | val xlinkRole : string -> t 259 | val xlinkShow : string -> t 260 | val xlinkTitle : string -> t 261 | val xlinkType : string -> t 262 | val xmlns : string -> t 263 | val xmlnsXlink : string -> t 264 | val xmlBase : string -> t 265 | val xmlLang : string -> t 266 | val xmlSpace : string -> t 267 | val y : string -> t 268 | val y1 : string -> t 269 | val y2 : string -> t 270 | val yChannelSelector : string -> t 271 | val z : string -> t 272 | val zoomAndPan : string -> t 273 | end 274 | 275 | include module type of Prop 276 | 277 | (** Common *) 278 | 279 | module Context : sig 280 | module Provider : sig 281 | val make : 282 | 'a Core.Context.t -> value:'a -> Core.element list -> Core.element 283 | end 284 | end 285 | 286 | val fragment : Core.element list -> Core.element 287 | val strict_mode : Core.element list -> Core.element 288 | val none : Core.element 289 | val string : string -> Core.element 290 | val int : int -> Core.element 291 | val float : float -> Core.element 292 | val maybe : ('a -> Core.element) -> 'a option -> Core.element 293 | val h : string -> Prop.t array -> Core.element list -> Core.element 294 | 295 | (** SVG elements *) 296 | 297 | val a : Prop.t array -> Core.element list -> Core.element 298 | val animate : Prop.t array -> Core.element list -> Core.element 299 | val animateMotion : Prop.t array -> Core.element list -> Core.element 300 | val animateTransform : Prop.t array -> Core.element list -> Core.element 301 | val circle : Prop.t array -> Core.element list -> Core.element 302 | val clipPath : Prop.t array -> Core.element list -> Core.element 303 | val defs : Prop.t array -> Core.element list -> Core.element 304 | val desc : Prop.t array -> Core.element list -> Core.element 305 | val discard : Prop.t array -> Core.element list -> Core.element 306 | val ellipse : Prop.t array -> Core.element list -> Core.element 307 | val feBlend : Prop.t array -> Core.element list -> Core.element 308 | val feColorMatrix : Prop.t array -> Core.element list -> Core.element 309 | val feComponentTransfer : Prop.t array -> Core.element list -> Core.element 310 | val feComposite : Prop.t array -> Core.element list -> Core.element 311 | val feConvolveMatrix : Prop.t array -> Core.element list -> Core.element 312 | val feDiffuseLighting : Prop.t array -> Core.element list -> Core.element 313 | val feDisplacementMap : Prop.t array -> Core.element list -> Core.element 314 | val feDistantLight : Prop.t array -> Core.element list -> Core.element 315 | val feDropShadow : Prop.t array -> Core.element list -> Core.element 316 | val feFlood : Prop.t array -> Core.element list -> Core.element 317 | val feFuncA : Prop.t array -> Core.element list -> Core.element 318 | val feFuncB : Prop.t array -> Core.element list -> Core.element 319 | val feFuncG : Prop.t array -> Core.element list -> Core.element 320 | val feFuncR : Prop.t array -> Core.element list -> Core.element 321 | val feGaussianBlur : Prop.t array -> Core.element list -> Core.element 322 | val feImage : Prop.t array -> Core.element list -> Core.element 323 | val feMerge : Prop.t array -> Core.element list -> Core.element 324 | val feMergeNode : Prop.t array -> Core.element list -> Core.element 325 | val feMorphology : Prop.t array -> Core.element list -> Core.element 326 | val feOffset : Prop.t array -> Core.element list -> Core.element 327 | val fePointLight : Prop.t array -> Core.element list -> Core.element 328 | val feSpecularLighting : Prop.t array -> Core.element list -> Core.element 329 | val feSpotLight : Prop.t array -> Core.element list -> Core.element 330 | val feTile : Prop.t array -> Core.element list -> Core.element 331 | val feTurbulence : Prop.t array -> Core.element list -> Core.element 332 | val filter : Prop.t array -> Core.element list -> Core.element 333 | val foreignObject : Prop.t array -> Core.element list -> Core.element 334 | val g : Prop.t array -> Core.element list -> Core.element 335 | val hatch : Prop.t array -> Core.element list -> Core.element 336 | val hatchpath : Prop.t array -> Core.element list -> Core.element 337 | val image : Prop.t array -> Core.element list -> Core.element 338 | val line : Prop.t array -> Core.element list -> Core.element 339 | val linearGradient : Prop.t array -> Core.element list -> Core.element 340 | val marker : Prop.t array -> Core.element list -> Core.element 341 | val mask : Prop.t array -> Core.element list -> Core.element 342 | val metadata : Prop.t array -> Core.element list -> Core.element 343 | val mpath : Prop.t array -> Core.element list -> Core.element 344 | val path : Prop.t array -> Core.element list -> Core.element 345 | val pattern : Prop.t array -> Core.element list -> Core.element 346 | val polygon : Prop.t array -> Core.element list -> Core.element 347 | val polyline : Prop.t array -> Core.element list -> Core.element 348 | val radialGradient : Prop.t array -> Core.element list -> Core.element 349 | val rect : Prop.t array -> Core.element list -> Core.element 350 | val script : Prop.t array -> Core.element list -> Core.element 351 | val set : Prop.t array -> Core.element list -> Core.element 352 | val stop : Prop.t array -> Core.element list -> Core.element 353 | val style : Prop.t array -> Core.element list -> Core.element 354 | val svg : Prop.t array -> Core.element list -> Core.element 355 | val switch : Prop.t array -> Core.element list -> Core.element 356 | val symbol : Prop.t array -> Core.element list -> Core.element 357 | val text : Prop.t array -> Core.element list -> Core.element 358 | val textPath : Prop.t array -> Core.element list -> Core.element 359 | val title : Prop.t array -> Core.element list -> Core.element 360 | val tspan : Prop.t array -> Core.element list -> Core.element 361 | val use : Prop.t array -> Core.element list -> Core.element 362 | val view : Prop.t array -> Core.element list -> Core.element 363 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name react) 3 | (modes byte) 4 | (public_name jsoo-react.lib) 5 | (preprocess 6 | (pps gen_js_api.ppx)) 7 | (libraries js_of_ocaml gen_js_api)) 8 | 9 | (rule 10 | (targets core.ml) 11 | (deps core.mli) 12 | (action 13 | (run %{bin:gen_js_api} -o %{targets} %{deps}))) 14 | 15 | (rule 16 | (targets dom.ml) 17 | (deps dom.mli) 18 | (action 19 | (run %{bin:gen_js_api} -o %{targets} %{deps}))) 20 | -------------------------------------------------------------------------------- /lib/event.ml: -------------------------------------------------------------------------------- 1 | (* Adapted from reason-react ReactEvent.re, commit 0f73a307 *) 2 | type 'a synthetic = Ojs.t 3 | 4 | module CommonApi = struct 5 | include 6 | [%js: 7 | type tag = Ojs.t 8 | type t = Ojs.t 9 | 10 | val t_of_js : Ojs.t -> t 11 | val t_to_js : t -> Ojs.t 12 | val bubbles : Ojs.t -> bool [@@js.get "bubbles"] 13 | val cancelable : t -> bool [@@js.get "cancelable"] 14 | val current_target : t -> Ojs.t [@@js.get "currentTarget"] 15 | (* Should return Dom.eventTarget *) 16 | 17 | val default_prevented : t -> bool [@@js.get "defaultPrevented"] 18 | val event_phase : t -> int [@@js.get "eventPhase"] 19 | val is_trusted : t -> bool [@@js.get "isTrusted"] 20 | val native_event : t -> Ojs.t [@@js.get "nativeEvent"] 21 | (* Should return Dom.event *) 22 | 23 | val prevent_default : t -> unit [@@js.call "preventDefault"] 24 | val is_default_prevented : t -> bool [@@js.call "isDefaultPrevented"] 25 | val stop_propagation : t -> unit [@@js.call "stopPropagation"] 26 | val is_propagation_stopped : t -> bool [@@js.call "isPropagationStopped"] 27 | val target : t -> Ojs.t [@@js.get "target"] 28 | (* Should return Dom.eventTarget *) 29 | 30 | val time_stamp : t -> float [@@js.get "timeStamp"] 31 | val type_ : t -> string [@@js.get "type"] 32 | val persist : t -> unit [@@js.call "persist"]] 33 | end 34 | 35 | module Synthetic = CommonApi 36 | 37 | (* Cast any event type to the general synthetic type. This is safe, since synthetic is more general *) 38 | external to_synthetic_event : 'a synthetic -> Synthetic.t = "%identity" 39 | 40 | module Clipboard = struct 41 | include CommonApi 42 | include [%js: val clipboard_data : t -> Ojs.t [@@js.get "clipboardData"]] 43 | 44 | (* Should return Dom.dataTransfer *) 45 | end 46 | 47 | module Composition = struct 48 | include CommonApi 49 | include [%js: val data : t -> string [@@js.get "data"]] 50 | end 51 | 52 | module Keyboard = struct 53 | include CommonApi 54 | 55 | include 56 | [%js: 57 | val alt_key : t -> bool [@@js.get "altKey"] 58 | val char_code : t -> int [@@js.get "charCode"] 59 | val ctrl_key : t -> bool [@@js.get "ctrlKey"] 60 | val get_modifier_state : t -> string -> bool [@@js.call "getModifierState"] 61 | val key : t -> string [@@js.get "key"] 62 | val key_code : t -> int [@@js.get "keyCode"] 63 | val locale : t -> string [@@js.get "locale"] 64 | val location : t -> int [@@js.get "location"] 65 | val meta_key : t -> bool [@@js.get "metaKey"] 66 | val repeat : t -> bool [@@js.get "repeat"] 67 | val shift_key : t -> bool [@@js.get "shiftKey"] 68 | val which : t -> int [@@js.get "which"]] 69 | end 70 | 71 | module Focus = struct 72 | include CommonApi 73 | 74 | include 75 | [%js: 76 | val related_target : t -> Ojs.t option [@@js.get "relatedTarget"]] 77 | 78 | (* Should return Dom.eventTarget *) 79 | end 80 | 81 | module Form = struct 82 | include CommonApi 83 | end 84 | 85 | module Mouse = struct 86 | include CommonApi 87 | 88 | include 89 | [%js: 90 | val alt_key : t -> bool [@@js.get "altKey"] 91 | val button : t -> int [@@js.get "button"] 92 | val buttons : t -> int [@@js.get "buttons"] 93 | val client_x : t -> int [@@js.get "clientX"] 94 | val client_y : t -> int [@@js.get "clientY"] 95 | val ctrl_key : t -> bool [@@js.get "ctrlKey"] 96 | val get_modifier_state : t -> string -> bool [@@js.call "getModifierState"] 97 | val meta_key : t -> bool [@@js.get "metaKey"] 98 | val page_x : t -> int [@@js.get "pageX"] 99 | val page_y : t -> int [@@js.get "pageY"] 100 | val related_target : t -> Ojs.t option [@@js.get "relatedTarget"] 101 | 102 | (* Should return Dom.eventTarget *) 103 | 104 | val screen_x : t -> int [@@js.get "screenX"] 105 | val screen_y : t -> int [@@js.get "screenY"] 106 | val shift_key : t -> bool [@@js.get "shiftKey"]] 107 | end 108 | 109 | module Selection = struct 110 | include CommonApi 111 | end 112 | 113 | module Touch = struct 114 | include CommonApi 115 | 116 | include 117 | [%js: 118 | val alt_key : t -> bool [@@js.get "altKey"] 119 | val changed_touches : t -> Ojs.t [@@js.get "changedTouches"] 120 | (* Should return Dom.touchList *) 121 | 122 | val ctrl_key : t -> bool [@@js.get "ctrlKey"] 123 | val get_modifier_state : t -> string -> bool [@@js.call "getModifierState"] 124 | val meta_key : t -> bool [@@js.get "metaKey"] 125 | val shift_key : t -> bool [@@js.get "shiftKey"] 126 | val target_touches : t -> Ojs.t [@@js.get "targetTouches"] 127 | (* Should return Dom.touchList *) 128 | 129 | val touches : t -> Ojs.t [@@js.get "touches"]] 130 | 131 | (* Should return Dom.touchList *) 132 | end 133 | 134 | module Pointer = struct 135 | include Mouse 136 | 137 | include 138 | [%js: 139 | val pointerId : t -> int [@@js.get] 140 | val pressure : t -> float [@@js.get] 141 | val tangentialPressure : t -> float [@@js.get] 142 | val tiltX : t -> float [@@js.get] 143 | val tiltY : t -> float [@@js.get] 144 | val twist : t -> float [@@js.get] 145 | val width : t -> float [@@js.get] 146 | val height : t -> float [@@js.get] 147 | val pointerType : t -> string [@@js.get] 148 | val isPrimary : t -> bool [@@js.get]] 149 | end 150 | 151 | type window = Js_of_ocaml.Dom_html.window 152 | 153 | module UI = struct 154 | external window_of_js : Ojs.t -> Js_of_ocaml.Dom_html.window = "%identity" 155 | 156 | include CommonApi 157 | 158 | include 159 | [%js: 160 | val detail : t -> int [@@js.get "detail"] 161 | val view : t -> window [@@js.get "view"]] 162 | 163 | (* Should return DOMAbstractView/WindowProxy *) 164 | end 165 | 166 | module Wheel = struct 167 | include CommonApi 168 | 169 | include 170 | [%js: 171 | val delta_mode : t -> int [@@js.get "deltaMode"] 172 | val delta_x : t -> float [@@js.get "deltaX"] 173 | val delta_y : t -> float [@@js.get "deltaY"] 174 | val delta_z : t -> float [@@js.get "deltaZ"]] 175 | end 176 | 177 | module Media = struct 178 | include CommonApi 179 | end 180 | 181 | module Image = struct 182 | include CommonApi 183 | end 184 | 185 | module Animation = struct 186 | include CommonApi 187 | 188 | include 189 | [%js: 190 | val animation_name : t -> string [@@js.get "animationName"] 191 | val pseudo_element : t -> string [@@js.get "pseudoElement"] 192 | val elapsed_time : t -> float [@@js.get "elapsedTime"]] 193 | end 194 | 195 | module Transition = struct 196 | include CommonApi 197 | 198 | include 199 | [%js: 200 | val property_name : t -> string [@@js.get "propertyName"] 201 | val pseudo_element : t -> string [@@js.get "pseudoElement"] 202 | val elapsed_time : t -> float [@@js.get "elapsedTime"]] 203 | end 204 | -------------------------------------------------------------------------------- /lib/event.mli: -------------------------------------------------------------------------------- 1 | (* Adapted from reason-react ReactEvent.re, commit 0f73a307type 'a synthetic *) 2 | type 'a synthetic 3 | 4 | module Synthetic : sig 5 | type tag 6 | type t = tag synthetic 7 | 8 | val bubbles : 'a synthetic -> bool 9 | val cancelable : 'a synthetic -> bool 10 | val current_target : 'a synthetic -> Ojs.t 11 | val default_prevented : 'a synthetic -> bool 12 | val event_phase : 'a synthetic -> int 13 | val is_trusted : 'a synthetic -> bool 14 | val native_event : 'a synthetic -> Ojs.t 15 | val prevent_default : 'a synthetic -> unit 16 | val is_default_prevented : 'a synthetic -> bool 17 | val stop_propagation : 'a synthetic -> unit 18 | val is_propagation_stopped : 'a synthetic -> bool 19 | val target : 'a synthetic -> Ojs.t 20 | val time_stamp : 'a synthetic -> float 21 | val type_ : 'a synthetic -> string 22 | val persist : 'a synthetic -> unit 23 | end 24 | 25 | (* Cast any event type to the general synthetic type. This is safe, since synthetic is more general *) 26 | val to_synthetic_event : 'a synthetic -> Synthetic.t 27 | 28 | module Clipboard : sig 29 | type tag 30 | type t = tag synthetic 31 | 32 | val t_of_js : Ojs.t -> t 33 | val t_to_js : t -> Ojs.t 34 | val bubbles : t -> bool 35 | val cancelable : t -> bool 36 | val current_target : t -> Ojs.t 37 | val default_prevented : t -> bool 38 | val event_phase : t -> int 39 | val is_trusted : t -> bool 40 | val native_event : t -> Ojs.t 41 | val prevent_default : t -> unit 42 | val is_default_prevented : t -> bool 43 | val stop_propagation : t -> unit 44 | val is_propagation_stopped : t -> bool 45 | val target : t -> Ojs.t 46 | val time_stamp : t -> float 47 | val type_ : t -> string 48 | val persist : t -> unit 49 | val clipboard_data : t -> Ojs.t 50 | end 51 | 52 | module Composition : sig 53 | type tag 54 | type t = tag synthetic 55 | 56 | val t_of_js : Ojs.t -> t 57 | val t_to_js : t -> Ojs.t 58 | val bubbles : t -> bool 59 | val cancelable : t -> bool 60 | val current_target : t -> Ojs.t 61 | val default_prevented : t -> bool 62 | val event_phase : t -> int 63 | val is_trusted : t -> bool 64 | val native_event : t -> Ojs.t 65 | val prevent_default : t -> unit 66 | val is_default_prevented : t -> bool 67 | val stop_propagation : t -> unit 68 | val is_propagation_stopped : t -> bool 69 | val target : t -> Ojs.t 70 | val time_stamp : t -> float 71 | val type_ : t -> string 72 | val persist : t -> unit 73 | val data : t -> string 74 | end 75 | 76 | module Keyboard : sig 77 | type tag 78 | type t = tag synthetic 79 | 80 | val t_of_js : Ojs.t -> t 81 | val t_to_js : t -> Ojs.t 82 | val bubbles : t -> bool 83 | val cancelable : t -> bool 84 | val current_target : t -> Ojs.t 85 | val default_prevented : t -> bool 86 | val event_phase : t -> int 87 | val is_trusted : t -> bool 88 | val native_event : t -> Ojs.t 89 | val prevent_default : t -> unit 90 | val is_default_prevented : t -> bool 91 | val stop_propagation : t -> unit 92 | val is_propagation_stopped : t -> bool 93 | val target : t -> Ojs.t 94 | val time_stamp : t -> float 95 | val type_ : t -> string 96 | val persist : t -> unit 97 | val alt_key : t -> bool 98 | val char_code : t -> int 99 | val ctrl_key : t -> bool 100 | val get_modifier_state : t -> string -> bool 101 | val key : t -> string 102 | val key_code : t -> int 103 | val locale : t -> string 104 | val location : t -> int 105 | val meta_key : t -> bool 106 | val repeat : t -> bool 107 | val shift_key : t -> bool 108 | val which : t -> int 109 | end 110 | 111 | module Focus : sig 112 | type tag 113 | type t = tag synthetic 114 | 115 | val t_of_js : Ojs.t -> t 116 | val t_to_js : t -> Ojs.t 117 | val bubbles : t -> bool 118 | val cancelable : t -> bool 119 | val current_target : t -> Ojs.t 120 | val default_prevented : t -> bool 121 | val event_phase : t -> int 122 | val is_trusted : t -> bool 123 | val native_event : t -> Ojs.t 124 | val prevent_default : t -> unit 125 | val is_default_prevented : t -> bool 126 | val stop_propagation : t -> unit 127 | val is_propagation_stopped : t -> bool 128 | val target : t -> Ojs.t 129 | val time_stamp : t -> float 130 | val type_ : t -> string 131 | val persist : t -> unit 132 | val related_target : t -> Ojs.t option 133 | end 134 | 135 | module Form : sig 136 | type tag 137 | type t = tag synthetic 138 | 139 | val t_of_js : Ojs.t -> t 140 | val t_to_js : t -> Ojs.t 141 | val bubbles : t -> bool 142 | val cancelable : t -> bool 143 | val current_target : t -> Ojs.t 144 | val default_prevented : t -> bool 145 | val event_phase : t -> int 146 | val is_trusted : t -> bool 147 | val native_event : t -> Ojs.t 148 | val prevent_default : t -> unit 149 | val is_default_prevented : t -> bool 150 | val stop_propagation : t -> unit 151 | val is_propagation_stopped : t -> bool 152 | val target : t -> Ojs.t 153 | val time_stamp : t -> float 154 | val type_ : t -> string 155 | val persist : t -> unit 156 | end 157 | 158 | module Mouse : sig 159 | type tag 160 | type t = tag synthetic 161 | 162 | val t_of_js : Ojs.t -> t 163 | val t_to_js : t -> Ojs.t 164 | val bubbles : t -> bool 165 | val cancelable : t -> bool 166 | val current_target : t -> Ojs.t 167 | val default_prevented : t -> bool 168 | val event_phase : t -> int 169 | val is_trusted : t -> bool 170 | val native_event : t -> Ojs.t 171 | val prevent_default : t -> unit 172 | val is_default_prevented : t -> bool 173 | val stop_propagation : t -> unit 174 | val is_propagation_stopped : t -> bool 175 | val target : t -> Ojs.t 176 | val time_stamp : t -> float 177 | val type_ : t -> string 178 | val persist : t -> unit 179 | val alt_key : t -> bool 180 | val button : t -> int 181 | val buttons : t -> int 182 | val client_x : t -> int 183 | val client_y : t -> int 184 | val ctrl_key : t -> bool 185 | val get_modifier_state : t -> string -> bool 186 | val meta_key : t -> bool 187 | val page_x : t -> int 188 | val page_y : t -> int 189 | val related_target : t -> Ojs.t option 190 | val screen_x : t -> int 191 | val screen_y : t -> int 192 | val shift_key : t -> bool 193 | end 194 | 195 | module Selection : sig 196 | type tag 197 | type t = tag synthetic 198 | 199 | val t_of_js : Ojs.t -> t 200 | val t_to_js : t -> Ojs.t 201 | val bubbles : t -> bool 202 | val cancelable : t -> bool 203 | val current_target : t -> Ojs.t 204 | val default_prevented : t -> bool 205 | val event_phase : t -> int 206 | val is_trusted : t -> bool 207 | val native_event : t -> Ojs.t 208 | val prevent_default : t -> unit 209 | val is_default_prevented : t -> bool 210 | val stop_propagation : t -> unit 211 | val is_propagation_stopped : t -> bool 212 | val target : t -> Ojs.t 213 | val time_stamp : t -> float 214 | val type_ : t -> string 215 | val persist : t -> unit 216 | end 217 | 218 | module Touch : sig 219 | type tag 220 | type t = tag synthetic 221 | 222 | val t_of_js : Ojs.t -> t 223 | val t_to_js : t -> Ojs.t 224 | val bubbles : t -> bool 225 | val cancelable : t -> bool 226 | val current_target : t -> Ojs.t 227 | val default_prevented : t -> bool 228 | val event_phase : t -> int 229 | val is_trusted : t -> bool 230 | val native_event : t -> Ojs.t 231 | val prevent_default : t -> unit 232 | val is_default_prevented : t -> bool 233 | val stop_propagation : t -> unit 234 | val is_propagation_stopped : t -> bool 235 | val target : t -> Ojs.t 236 | val time_stamp : t -> float 237 | val type_ : t -> string 238 | val persist : t -> unit 239 | val alt_key : t -> bool 240 | val changed_touches : t -> Ojs.t 241 | val ctrl_key : t -> bool 242 | val get_modifier_state : t -> string -> bool 243 | val meta_key : t -> bool 244 | val shift_key : t -> bool 245 | val target_touches : t -> Ojs.t 246 | val touches : t -> Ojs.t 247 | end 248 | 249 | module Pointer : sig 250 | type tag 251 | type t = tag synthetic 252 | 253 | val t_of_js : Ojs.t -> t 254 | val t_to_js : t -> Ojs.t 255 | val pointerId : t -> int 256 | val pressure : t -> float 257 | val tangentialPressure : t -> float 258 | val tiltX : t -> float 259 | val tiltY : t -> float 260 | val twist : t -> float 261 | val width : t -> float 262 | val height : t -> float 263 | val pointerType : t -> string (* 'mouse' | 'pen' | 'touch' *) 264 | val isPrimary : t -> bool 265 | 266 | (** Inherited from Mouse *) 267 | 268 | val bubbles : t -> bool 269 | val cancelable : t -> bool 270 | val current_target : t -> Ojs.t 271 | val default_prevented : t -> bool 272 | val event_phase : t -> int 273 | val is_trusted : t -> bool 274 | val native_event : t -> Ojs.t 275 | val prevent_default : t -> unit 276 | val is_default_prevented : t -> bool 277 | val stop_propagation : t -> unit 278 | val is_propagation_stopped : t -> bool 279 | val target : t -> Ojs.t 280 | val time_stamp : t -> float 281 | val type_ : t -> string 282 | val persist : t -> unit 283 | val alt_key : t -> bool 284 | val button : t -> int 285 | val buttons : t -> int 286 | val client_x : t -> int 287 | val client_y : t -> int 288 | val ctrl_key : t -> bool 289 | val get_modifier_state : t -> string -> bool 290 | val meta_key : t -> bool 291 | val page_x : t -> int 292 | val page_y : t -> int 293 | val related_target : t -> Ojs.t option 294 | val screen_x : t -> int 295 | val screen_y : t -> int 296 | val shift_key : t -> bool 297 | end 298 | 299 | module UI : sig 300 | type tag 301 | type t = tag synthetic 302 | 303 | val t_of_js : Ojs.t -> t 304 | val t_to_js : t -> Ojs.t 305 | val bubbles : t -> bool 306 | val cancelable : t -> bool 307 | val current_target : t -> Ojs.t 308 | val default_prevented : t -> bool 309 | val event_phase : t -> int 310 | val is_trusted : t -> bool 311 | val native_event : t -> Ojs.t 312 | val prevent_default : t -> unit 313 | val is_default_prevented : t -> bool 314 | val stop_propagation : t -> unit 315 | val is_propagation_stopped : t -> bool 316 | val target : t -> Ojs.t 317 | val time_stamp : t -> float 318 | val type_ : t -> string 319 | val persist : t -> unit 320 | val detail : t -> int 321 | val view : t -> Js_of_ocaml.Dom_html.window 322 | end 323 | 324 | module Wheel : sig 325 | type tag 326 | type t = tag synthetic 327 | 328 | val t_of_js : Ojs.t -> t 329 | val t_to_js : t -> Ojs.t 330 | val bubbles : t -> bool 331 | val cancelable : t -> bool 332 | val current_target : t -> Ojs.t 333 | val default_prevented : t -> bool 334 | val event_phase : t -> int 335 | val is_trusted : t -> bool 336 | val native_event : t -> Ojs.t 337 | val prevent_default : t -> unit 338 | val is_default_prevented : t -> bool 339 | val stop_propagation : t -> unit 340 | val is_propagation_stopped : t -> bool 341 | val target : t -> Ojs.t 342 | val time_stamp : t -> float 343 | val type_ : t -> string 344 | val persist : t -> unit 345 | val delta_mode : t -> int 346 | val delta_x : t -> float 347 | val delta_y : t -> float 348 | val delta_z : t -> float 349 | end 350 | 351 | module Media : sig 352 | type tag 353 | type t = tag synthetic 354 | 355 | val t_of_js : Ojs.t -> t 356 | val t_to_js : t -> Ojs.t 357 | val bubbles : t -> bool 358 | val cancelable : t -> bool 359 | val current_target : t -> Ojs.t 360 | val default_prevented : t -> bool 361 | val event_phase : t -> int 362 | val is_trusted : t -> bool 363 | val native_event : t -> Ojs.t 364 | val prevent_default : t -> unit 365 | val is_default_prevented : t -> bool 366 | val stop_propagation : t -> unit 367 | val is_propagation_stopped : t -> bool 368 | val target : t -> Ojs.t 369 | val time_stamp : t -> float 370 | val type_ : t -> string 371 | val persist : t -> unit 372 | end 373 | 374 | module Image : sig 375 | type tag 376 | type t = tag synthetic 377 | 378 | val t_of_js : Ojs.t -> t 379 | val t_to_js : t -> Ojs.t 380 | val bubbles : t -> bool 381 | val cancelable : t -> bool 382 | val current_target : t -> Ojs.t 383 | val default_prevented : t -> bool 384 | val event_phase : t -> int 385 | val is_trusted : t -> bool 386 | val native_event : t -> Ojs.t 387 | val prevent_default : t -> unit 388 | val is_default_prevented : t -> bool 389 | val stop_propagation : t -> unit 390 | val is_propagation_stopped : t -> bool 391 | val target : t -> Ojs.t 392 | val time_stamp : t -> float 393 | val type_ : t -> string 394 | val persist : t -> unit 395 | end 396 | 397 | module Animation : sig 398 | type tag 399 | type t = tag synthetic 400 | 401 | val t_of_js : Ojs.t -> t 402 | val t_to_js : t -> Ojs.t 403 | val bubbles : t -> bool 404 | val cancelable : t -> bool 405 | val current_target : t -> Ojs.t 406 | val default_prevented : t -> bool 407 | val event_phase : t -> int 408 | val is_trusted : t -> bool 409 | val native_event : t -> Ojs.t 410 | val prevent_default : t -> unit 411 | val is_default_prevented : t -> bool 412 | val stop_propagation : t -> unit 413 | val is_propagation_stopped : t -> bool 414 | val target : t -> Ojs.t 415 | val time_stamp : t -> float 416 | val type_ : t -> string 417 | val persist : t -> unit 418 | val animation_name : t -> string 419 | val pseudo_element : t -> string 420 | val elapsed_time : t -> float 421 | end 422 | 423 | module Transition : sig 424 | type tag 425 | type t = tag synthetic 426 | 427 | val t_of_js : Ojs.t -> t 428 | val t_to_js : t -> Ojs.t 429 | val bubbles : t -> bool 430 | val cancelable : t -> bool 431 | val current_target : t -> Ojs.t 432 | val default_prevented : t -> bool 433 | val event_phase : t -> int 434 | val is_trusted : t -> bool 435 | val native_event : t -> Ojs.t 436 | val prevent_default : t -> unit 437 | val is_default_prevented : t -> bool 438 | val stop_propagation : t -> unit 439 | val is_propagation_stopped : t -> bool 440 | val target : t -> Ojs.t 441 | val time_stamp : t -> float 442 | val type_ : t -> string 443 | val persist : t -> unit 444 | val property_name : t -> string 445 | val pseudo_element : t -> string 446 | val elapsed_time : t -> float 447 | end 448 | -------------------------------------------------------------------------------- /lib/hooks.ml: -------------------------------------------------------------------------------- 1 | let use_ref initial = 2 | let state, _ = Core.use_state (fun () -> ref initial) in 3 | state 4 | 5 | let use_ref_lazy init = 6 | let state, _ = Core.use_state (fun () -> ref (init ())) in 7 | state 8 | 9 | let use_state initial = Core.use_state (fun () -> initial) 10 | let use_state_lazy init = Core.use_state init 11 | let use_reducer = Core.use_reducer 12 | 13 | let use_resource ~on:deps ?(equal = ( = )) ?before_render ~release acquire = 14 | let last_deps = use_ref deps in 15 | let resource = use_ref None in 16 | 17 | let release () = Option.iter release !resource in 18 | let acquire () = resource := Some (acquire ()) in 19 | 20 | let init () = 21 | acquire (); 22 | Some release 23 | in 24 | let update () = 25 | if not (equal deps !last_deps) then begin 26 | last_deps := deps; 27 | release (); 28 | acquire () 29 | end; 30 | None 31 | in 32 | 33 | Core.use_effect_once ?before_render init; 34 | Core.use_effect_always ?before_render update 35 | 36 | let use_effect ~on ?equal ?before_render ?(cleanup = fun () -> ()) f = 37 | use_resource ?before_render ~on ?equal ~release:cleanup f 38 | 39 | let use_effect_once ?before_render ?cleanup f = 40 | Core.use_effect_once ?before_render (fun () -> 41 | f (); 42 | cleanup) 43 | 44 | let use_effect_always ?before_render ?cleanup f = 45 | Core.use_effect_always ?before_render (fun () -> 46 | f (); 47 | cleanup) 48 | 49 | let use_memo ~on:deps ?(equal = ( = )) f = 50 | let last_deps = use_ref deps in 51 | let value = use_ref_lazy (fun () -> f ()) in 52 | 53 | if not (equal deps !last_deps) then begin 54 | last_deps := deps; 55 | value := f () 56 | end; 57 | 58 | !value 59 | 60 | let use_context = Core.use_context 61 | -------------------------------------------------------------------------------- /lib/hooks.mli: -------------------------------------------------------------------------------- 1 | val use_ref : 'value -> 'value ref 2 | val use_ref_lazy : (unit -> 'value) -> 'value ref 3 | val use_state : 'state -> 'state * (('state -> 'state) -> unit) 4 | val use_state_lazy : (unit -> 'state) -> 'state * (('state -> 'state) -> unit) 5 | 6 | val use_reducer : 7 | init:(unit -> 'state) 8 | -> ('state -> 'action -> 'state) 9 | -> 'state * ('action -> unit) 10 | 11 | val use_effect : 12 | on:'deps 13 | -> ?equal:('deps -> 'deps -> bool) 14 | -> ?before_render:bool 15 | -> ?cleanup:(unit -> unit) 16 | -> (unit -> unit) 17 | -> unit 18 | 19 | val use_effect_once : 20 | ?before_render:bool -> ?cleanup:(unit -> unit) -> (unit -> unit) -> unit 21 | 22 | val use_effect_always : 23 | ?before_render:bool -> ?cleanup:(unit -> unit) -> (unit -> unit) -> unit 24 | 25 | val use_resource : 26 | on:'deps 27 | -> ?equal:('deps -> 'deps -> bool) 28 | -> ?before_render:bool 29 | -> release:('resource -> unit) 30 | -> (unit -> 'resource) 31 | -> unit 32 | 33 | val use_memo : 34 | on:'deps -> ?equal:('deps -> 'deps -> bool) -> (unit -> 'value) -> 'value 35 | 36 | val use_context : 'value Core.Context.t -> 'value 37 | -------------------------------------------------------------------------------- /lib/imports.ml: -------------------------------------------------------------------------------- 1 | type react = Ojs.t 2 | type react_dom = Ojs.t 3 | 4 | let react_to_js v = v 5 | let react_dom_to_js v = v 6 | let react : react = Js_of_ocaml.Js.Unsafe.js_expr {|require("react")|} 7 | 8 | let react_dom : react_dom = 9 | Js_of_ocaml.Js.Unsafe.js_expr {|require("react-dom")|} 10 | -------------------------------------------------------------------------------- /lib/react.ml: -------------------------------------------------------------------------------- 1 | include Core 2 | module Hooks = Hooks 3 | 4 | module Dom = struct 5 | include Dom 6 | 7 | module Dsl = struct 8 | module Html = Dom_html 9 | module Svg = Dom_svg 10 | end 11 | end 12 | 13 | module Event = Event 14 | module Router = Router 15 | -------------------------------------------------------------------------------- /lib/router.ml: -------------------------------------------------------------------------------- 1 | (* Adapted from reason-react: https://reasonml.github.io/reason-react/docs/en/router *) 2 | 3 | module Browser = 4 | [%js: 5 | type history 6 | type window 7 | 8 | val window_to_js : window -> Ojs.t 9 | 10 | type location 11 | 12 | val history : history option [@@js.global "history"] 13 | val window : window option [@@js.global "window"] 14 | val location : window -> location [@@js.get] 15 | val pathname : location -> string [@@js.get] 16 | val hash : location -> string [@@js.get] 17 | val search : location -> string [@@js.get] 18 | 19 | val push_state : history -> Ojs.t -> string -> href:string -> unit 20 | [@@js.call "pushState"] 21 | 22 | val replace_state : history -> Ojs.t -> string -> href:string -> unit 23 | [@@js.call "replaceState"]] 24 | 25 | module Event = 26 | [%js: 27 | type t 28 | 29 | val event : t [@@js.global "Event"] 30 | val make_event_ie11_compatible : string -> t [@@js.new "Event"] 31 | val create_event_non_ie8 : string -> t [@@js.global "document.createEvent"] 32 | 33 | val init_event_non_ie8 : t -> string -> bool -> bool -> unit 34 | [@@js.call "initEvent"] 35 | 36 | (* The cb is t => unit, but access is restricted for now *) 37 | val add_event_listener : Browser.window -> string -> (unit -> unit) -> unit 38 | [@@js.call "addEventListener"] 39 | 40 | val remove_event_listener : Browser.window -> string -> (unit -> unit) -> unit 41 | [@@js.call "removeEventListener"] 42 | 43 | val dispatch_event : Browser.window -> t -> unit [@@js.call "dispatchEvent"]] 44 | 45 | let safe_make_event eventName = 46 | if 47 | let open Js_of_ocaml in 48 | Js.typeof (Js.Unsafe.inject Event.event) = Js.string "function" 49 | then Event.make_event_ie11_compatible eventName 50 | else 51 | let event = Event.create_event_non_ie8 "Event" in 52 | Event.init_event_non_ie8 event eventName true true; 53 | Event.event 54 | 55 | let slice_to_end s = 56 | match s = "" with true -> "" | false -> String.sub s 1 (String.length s - 1) 57 | 58 | (* if we ever roll our own parser in the future, make sure you test all url combinations 59 | e.g. foo.com/?#bar 60 | *) 61 | (* URLSearchParams doesn't work on IE11, edge16, etc. *) 62 | (* The library doesn't provide search for now. Users can roll their own solution/data structure.*) 63 | let path () = 64 | match Browser.window with 65 | | None -> [] 66 | | Some w -> ( 67 | match 68 | let open Browser in 69 | w |> location |> pathname 70 | with 71 | | "" | "/" -> [] 72 | | raw -> 73 | let raw = slice_to_end raw in 74 | let raw = 75 | let n = String.length raw in 76 | match n > 0 && raw.[n - 1] = '/' with 77 | | true -> String.sub raw 0 (n - 1) 78 | | false -> raw 79 | in 80 | raw |> String.split_on_char '/') 81 | 82 | let hash () = 83 | match Browser.window with 84 | | None -> "" 85 | | Some w -> ( 86 | match 87 | let open Browser in 88 | w |> location |> hash 89 | with 90 | | "" | "#" -> "" 91 | | raw -> 92 | (* remove the preceeding #, which every hash seems to have. *) 93 | slice_to_end raw) 94 | 95 | let search () = 96 | match Browser.window with 97 | | None -> "" 98 | | Some w -> ( 99 | match 100 | let open Browser in 101 | w |> location |> search 102 | with 103 | | "" | "?" -> "" 104 | | raw -> 105 | (* remove the preceeding ?, which every search seems to have. *) 106 | slice_to_end raw) 107 | 108 | let push path = 109 | match 110 | let open Browser in 111 | (history, window) 112 | with 113 | | None, _ | _, None -> () 114 | | Some history, Some window -> 115 | Browser.push_state history Ojs.null "" ~href:path; 116 | Event.dispatch_event window (safe_make_event "popstate") 117 | 118 | let replace path = 119 | match 120 | let open Browser in 121 | (history, window) 122 | with 123 | | None, _ | _, None -> () 124 | | Some history, Some window -> 125 | Browser.replace_state history Ojs.null "" ~href:path; 126 | Event.dispatch_event window (safe_make_event "popstate") 127 | 128 | type url = 129 | { path : string list 130 | ; hash : string 131 | ; search : string 132 | } 133 | 134 | let url_not_equal a b = 135 | let rec list_not_equal xs ys = 136 | match (xs, ys) with 137 | | [], [] -> false 138 | | [], _ :: _ | _ :: _, [] -> true 139 | | x :: xs, y :: ys -> if x != y then true else list_not_equal xs ys 140 | in 141 | a.hash != b.hash || a.search != b.search || list_not_equal a.path b.path 142 | 143 | type watcher_id = unit -> unit 144 | 145 | let url () = { path = path (); hash = hash (); search = search () } 146 | 147 | (* alias exposed publicly *) 148 | let dangerously_get_initial_url = url 149 | 150 | let watch_url callback = 151 | match Browser.window with 152 | | None -> fun () -> () 153 | | Some window -> 154 | let watcher_id () = callback (url ()) in 155 | Event.add_event_listener window "popstate" watcher_id; 156 | watcher_id 157 | 158 | let unwatch_url watcher_id = 159 | match Browser.window with 160 | | None -> () 161 | | Some window -> Event.remove_event_listener window "popstate" watcher_id 162 | 163 | let use_url ?server_url () = 164 | let url, set_url = 165 | Core.use_state (fun () -> 166 | match server_url with 167 | | Some url -> url 168 | | None -> dangerously_get_initial_url ()) 169 | in 170 | Core.use_effect_once (fun () -> 171 | let watcher_id = watch_url (fun url -> set_url (fun _ -> url)) in 172 | (* check for updates that may have occured between the initial state and the subscribe above *) 173 | let new_url = dangerously_get_initial_url () in 174 | if url_not_equal new_url url then set_url (fun _ -> new_url); 175 | Some (fun () -> unwatch_url watcher_id)); 176 | url 177 | -------------------------------------------------------------------------------- /lib/router.mli: -------------------------------------------------------------------------------- 1 | (* Adapted from reason-react: https://reasonml.github.io/reason-react/docs/en/router *) 2 | 3 | val push : string -> unit 4 | (** update the url with the string path. Example: `push("/book/1")`, `push("/books#title")` *) 5 | 6 | val replace : string -> unit 7 | (** update the url with the string path. modifies the current history entry instead of creating a new one. Example: `replace("/book/1")`, `replace("/books#title")` *) 8 | 9 | type watcher_id 10 | 11 | type url = 12 | { (* path takes window.location.path, like "/book/title/edit" and turns it into `["book", "title", "edit"]` *) 13 | path : string list 14 | ; (* the url's hash, if any. The # symbol is stripped out for you *) 15 | hash : string 16 | ; (* the url's query params, if any. The ? symbol is stripped out for you *) 17 | search : string 18 | } 19 | 20 | val watch_url : (url -> unit) -> watcher_id 21 | (** start watching for URL changes. Returns a subscription token. Upon url change, calls the callback and passes it the url record *) 22 | 23 | val unwatch_url : watcher_id -> unit 24 | (** stop watching for URL changes *) 25 | 26 | val dangerously_get_initial_url : unit -> url 27 | (** this is marked as "dangerous" because you technically shouldn't be accessing the URL outside of watchUrl's callback; 28 | you'd read a potentially stale url, instead of the fresh one inside watchUrl. 29 | But this helper is sometimes needed, if you'd like to initialize a page whose display/state depends on the URL, 30 | instead of reading from it in watchUrl's callback, which you'd probably have put inside didMount (aka too late, 31 | the page's already rendered). 32 | So, the correct (and idiomatic) usage of this helper is to only use it in a component that's also subscribed to 33 | watchUrl. Please see https://github.com/reasonml-community/reason-react-example/blob/master/src/todomvc/TodoItem.re 34 | for an example. 35 | *) 36 | 37 | val use_url : ?server_url:url -> unit -> url 38 | (** hook for watching url changes. 39 | * serverUrl is used for ssr. it allows you to specify the url without relying on browser apis existing/working as expected 40 | *) 41 | -------------------------------------------------------------------------------- /lib/test_utils/ReactDOMTestUtils.js: -------------------------------------------------------------------------------- 1 | joo_global_object.__LIB__reactDOMTestUtils = require('react-dom/test-utils'); 2 | -------------------------------------------------------------------------------- /lib/test_utils/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name jsoo_react_test) 3 | (modes byte) 4 | (public_name jsoo-react.test) 5 | (wrapped false) 6 | (js_of_ocaml 7 | (javascript_files ReactDOMTestUtils.js)) 8 | (libraries js_of_ocaml gen_js_api)) 9 | 10 | (rule 11 | (targets reactDOMTestUtils.ml) 12 | (deps reactDOMTestUtils.mli) 13 | (action 14 | (run %{bin:gen_js_api} -o %{targets} %{deps}))) 15 | -------------------------------------------------------------------------------- /lib/test_utils/reactDOMTestUtils.mli: -------------------------------------------------------------------------------- 1 | [@@@js.stop] 2 | 3 | type undefined 4 | type element 5 | type 'a node_list = 'a Js_of_ocaml.Dom.nodeList 6 | 7 | val node_list_of_js : (Ojs.t -> 'value) -> Ojs.t -> 'value node_list 8 | val node_list_to_js : ('value -> Ojs.t) -> 'value node_list -> Ojs.t 9 | val unsafe_to_element : 'a Js_of_ocaml.Js.t -> element 10 | 11 | [@@@js.start] 12 | 13 | [@@@js.implem 14 | type undefined = Ojs.t 15 | 16 | let undefined = Ojs.variable "undefined" 17 | let undefined_to_js x = x 18 | let undefined_of_js x = x 19 | 20 | type element = Js_of_ocaml.Dom_html.element 21 | 22 | external element_of_js : Ojs.t -> Js_of_ocaml.Dom_html.element = "%identity" 23 | external element_to_js : Js_of_ocaml.Dom_html.element -> Ojs.t = "%identity" 24 | 25 | type 'a node_list = 'a Js_of_ocaml.Dom.nodeList 26 | 27 | external node_list_of_ojs : Ojs.t -> 'value node_list = "%identity" 28 | external node_list_to_js : 'value node_list -> Ojs.t = "%identity" 29 | 30 | let node_list_of_js _f x = node_list_of_ojs x 31 | let node_list_to_js _f x = node_list_to_js x 32 | 33 | external unsafe_to_element : 'a Js_of_ocaml.Js.t -> element = "%identity"] 34 | 35 | val act : (unit -> unit) -> unit 36 | [@@js.custom 37 | val act : (unit -> undefined) -> unit 38 | [@@js.global "__LIB__reactDOMTestUtils.act"] 39 | 40 | let act f = 41 | act (fun () -> 42 | f (); 43 | undefined)] 44 | 45 | module Simulate : sig 46 | val click : element -> unit 47 | [@@js.global "__LIB__reactDOMTestUtils.Simulate.click"] 48 | end 49 | 50 | val querySelectorAll : element -> string -> element array 51 | [@@js.custom 52 | val querySelectorAll : element -> string -> element node_list [@@js.call] 53 | val arrayFrom : element node_list -> element array [@@js.global "Array.from"] 54 | 55 | let querySelectorAll element selector = 56 | arrayFrom (querySelectorAll element selector)] 57 | 58 | module DOM : sig 59 | val findBySelectorAndPartialTextContent : 60 | element -> string -> string -> element 61 | [@@js.custom 62 | let find f a = 63 | let rec find a f n = if f a.(n) then a.(n) else find a f (n + 1) in 64 | find a f 0 65 | 66 | val textContent : element -> string [@@js.get] 67 | val includes : string -> string -> bool [@@js.call] 68 | 69 | let findBySelectorAndPartialTextContent element selector content = 70 | querySelectorAll element selector 71 | |> find (fun node -> textContent node |> includes content)] 72 | end 73 | -------------------------------------------------------------------------------- /ppx/dune: -------------------------------------------------------------------------------- 1 | (env 2 | (dev 3 | (flags 4 | (:standard -w -9)))) 5 | 6 | (library 7 | (name jsoo_react_ppx) 8 | (public_name jsoo-react.ppx) 9 | (libraries compiler-libs.common ppxlib ppxlib.astlib) 10 | (preprocess 11 | (pps ppxlib.metaquot)) 12 | (kind ppx_rewriter)) 13 | -------------------------------------------------------------------------------- /ppx/html.mli: -------------------------------------------------------------------------------- 1 | type attributeType = 2 | | String 3 | | Int 4 | | Float 5 | | Bool 6 | | Style 7 | | Ref 8 | | InnerHtml 9 | 10 | type eventType = 11 | | Clipboard 12 | | Composition 13 | | Keyboard 14 | | Focus 15 | | Form 16 | | Mouse 17 | | Selection 18 | | Touch 19 | | UI 20 | | Wheel 21 | | Media 22 | | Image 23 | | Animation 24 | | Transition 25 | | Pointer 26 | | Drag 27 | 28 | type attribute = 29 | { type_ : attributeType 30 | ; name : string 31 | ; jsxName : string 32 | } 33 | 34 | type event = 35 | { type_ : eventType 36 | ; name : string 37 | } 38 | 39 | type prop = 40 | | Attribute of attribute 41 | | Event of event 42 | 43 | type errors = 44 | [ `ElementNotFound 45 | | `AttributeNotFound 46 | ] 47 | 48 | val getJSXName : prop -> string 49 | val findByName : string -> string -> (prop, errors) result 50 | -------------------------------------------------------------------------------- /ppx/test/.ocamlformat-ignore: -------------------------------------------------------------------------------- 1 | test.ml 2 | -------------------------------------------------------------------------------- /ppx/test/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (libraries reason ppxlib jsoo_react_ppx)) 4 | 5 | (rule 6 | (targets pp_reason.result) 7 | (deps test_reason.ml) 8 | (action 9 | (run ./main.exe --impl %{deps} -o %{targets}))) 10 | 11 | (rule 12 | (targets test_reason.ml) 13 | (deps input_reason.re) 14 | (action 15 | (with-stdout-to 16 | %{targets} 17 | (run refmt --parse=re --print=ml %{deps})))) 18 | 19 | (rule 20 | (targets pp_errors_01_external_component_multi_prim.actual) 21 | (deps 22 | (:pp main.exe) 23 | (:input input_errors_01_external_component_multi_prim.ml)) 24 | (action 25 | (setenv 26 | "OCAML_COLOR" 27 | "never" 28 | (with-stderr-to 29 | %{targets} 30 | (bash "! ./%{pp} --impl %{input}"))))) 31 | 32 | (rule 33 | (targets pp_errors_02_external_component_unlabelled_arg.actual) 34 | (deps 35 | (:pp main.exe) 36 | (:input input_errors_02_external_component_unlabelled_arg.ml)) 37 | (action 38 | (setenv 39 | "OCAML_COLOR" 40 | "never" 41 | (with-stderr-to 42 | %{targets} 43 | (bash "! ./%{pp} --impl %{input}"))))) 44 | 45 | (rule 46 | (targets pp_ocaml.result) 47 | (deps input_ocaml.ml) 48 | (action 49 | (run ./main.exe --impl %{deps} -o %{targets}))) 50 | 51 | (rule 52 | (targets pp_ocaml_dev.result) 53 | (deps input_ocaml.ml) 54 | (action 55 | (setenv 56 | "JSOO_REACT_DEV" 57 | "1" 58 | (run ./main.exe --impl %{deps} -o %{targets})))) 59 | 60 | (rule 61 | (alias runtest) 62 | (action 63 | (progn 64 | (diff pp_errors_01_external_component_multi_prim.expected 65 | pp_errors_01_external_component_multi_prim.actual) 66 | (diff pp_errors_02_external_component_unlabelled_arg.expected 67 | pp_errors_02_external_component_unlabelled_arg.actual) 68 | (diff pp_reason.expected pp_reason.result) 69 | (diff pp_ocaml.expected pp_ocaml.result) 70 | (diff pp_ocaml_dev.expected pp_ocaml_dev.result)))) 71 | -------------------------------------------------------------------------------- /ppx/test/input_errors_01_external_component_multi_prim.ml: -------------------------------------------------------------------------------- 1 | external%component make : name:Js.js_string Js.t -> React.element = "foo" "bar" 2 | -------------------------------------------------------------------------------- /ppx/test/input_errors_02_external_component_unlabelled_arg.ml: -------------------------------------------------------------------------------- 1 | external%component make : Js.js_string Js.t -> React.element = "foo" 2 | -------------------------------------------------------------------------------- /ppx/test/input_ocaml.ml: -------------------------------------------------------------------------------- 1 | (* ppx should not transform this *) 2 | let rec pp_user = 2 3 | 4 | (* ppx should not error on unrelated primitives with [] as pval_prim *) 5 | type t = string [@@foo val f : int] 6 | 7 | (* let%component *) 8 | 9 | let%component make ?(name = "") = div [||] [ React.string ("Hello " ^ name) ] 10 | let%component make ~children:(first, second) = div [||] [ first; second ] 11 | let%component make ~children:kids = div [||] kids 12 | let%component make ~children:(first, second) () = div [||] [ first; second ] 13 | let%component make ?(name = "") = div [||] [ name ] 14 | let%component make () = div [||] [] 15 | let%component make (type a) ~(foo : a) : _ = div [||] [] 16 | let%component make : type a. foo:a -> _ = fun ~foo:_ -> div [||] [] 17 | 18 | let%component make ~(bar : int option) = 19 | div [||] [ React.string (string_of_int (Option.value ~default:0 bar)) ] () 20 | 21 | external%component make : 22 | name:Js.js_string Js.t -> React.element 23 | = "require(\"my-react-library\").MyReactComponent" 24 | 25 | external%component make : 26 | ?name:Js.js_string Js.t -> React.element 27 | = "require(\"my-react-library\").MyReactComponent" 28 | 29 | external%component make : 30 | names:string array -> React.element 31 | = "require(\"my-react-library\").MyReactComponent" 32 | -------------------------------------------------------------------------------- /ppx/test/input_reason.re: -------------------------------------------------------------------------------- 1 | // Components 2 | 3 | [@react.component] 4 | let make = (~name="") => { 5 | <> 6 |
{React.string("Hello " ++ name)}
7 | {React.string("Hello " ++ name)} 8 | ; 9 | }; 10 | 11 | [@react.component] 12 | let make = (~a, ~b, _) => { 13 | print_endline("This function should be named `Test`"); 14 |
; 15 | }; 16 | 17 | module External = { 18 | [@react.component] [@otherAttribute "bla"] 19 | external component: (~a: int, ~b: string) => React.element = 20 | {|require("my-react-library").MyReactComponent|}; 21 | }; 22 | 23 | module Bar = { 24 | [@react.component] 25 | let make = (~a, ~b, _) => { 26 | print_endline("This function should be named `Test$Bar`"); 27 |
; 28 | }; 29 | [@react.component] 30 | let component = (~a, ~b, _) => { 31 | print_endline("This function should be named `Test$Bar$component`"); 32 |
; 33 | }; 34 | }; 35 | 36 | module type X_int = { 37 | let x: int; 38 | }; 39 | 40 | module Func = (M: X_int) => { 41 | let x = M.x + 1; 42 | [@react.component] 43 | let make = (~a, ~b, _) => { 44 | print_endline("This function should be named `Test$Func`", M.x); 45 |
; 46 | }; 47 | }; 48 | 49 | module ForwardRef = { 50 | [@react.component] 51 | let make = 52 | React.forward_ref(theRef => 53 |
Js_of_ocaml.Js.Opt.to_option}> 54 | {React.string("ForwardRef")} 55 |
56 | ); 57 | }; 58 | 59 | module Memo = { 60 | [@react.component] 61 | let make = 62 | React.memo((~a) => { 63 |
{Printf.sprintf("`a` is %s", a) |> React.string}
64 | }); 65 | }; 66 | 67 | module MemoCustomCompareProps = { 68 | [@react.component] 69 | let make = 70 | React.memo( 71 | ~compare=(prevPros, nextProps) => false, 72 | (~a) => { 73 |
{Printf.sprintf("`a` is %d", a) |> React.string}
74 | }, 75 | ); 76 | }; 77 | 78 | let fragment = foo => [@bla] <> foo ; 79 | 80 | let polyChildrenFragment = (foo, bar) => <> foo bar ; 81 | 82 | let nestedFragment = (foo, bar, baz) => <> foo <> bar baz ; 83 | 84 | let upper = ; 85 | 86 | let upperWithProp = ; 87 | 88 | let upperWithChild = foo => foo ; 89 | 90 | let upperWithChildren = (foo, bar) => foo bar ; 91 | 92 | let lower =
; 93 | 94 | let lowerWithChildAndProps = foo => 95 | foo ; 96 | 97 | let lowerWithChildren = (foo, bar) => foo bar ; 98 | 99 | let lowerWithChildrenComplex = 100 |
101 |
102 |

{"jsoo-react" |> s}

103 | 121 |
122 |
; 123 | 124 | let lowerWithChildrenComplex2 = 125 |
126 |
127 | {let example = 128 | examples 129 | |> List.find_opt(e => { 130 | e.path 131 | == (List.nth_opt(url.path, 0) |> Option.value(~default="")) 132 | }) 133 | |> Option.value(~default=firstExample); 134 |
135 |

{example.title |> s}

136 |

{"Rendered component" |> s}

137 | {example.element} 138 |

{"Code" |> s}

139 | {example.code} 140 |
} 141 |
142 |
; 143 | 144 | let nestedElement = ; 145 | 146 | let innerHTML = 147 |
")} 149 | />; 150 | // TODO: fix this test (are these components deprecated??) 151 | // let nestedElementCustomName = ; 152 | 153 | [@react.component] 154 | let make = (~title, ~children) => { 155 |
...{[ {title |> s} , ...children]}
; 156 | }; 157 | 158 | let t =
; 159 | 160 | let t = ; 161 | 162 | [@react.component] 163 | let make = 164 | React.Dom.forward_ref((~children, ref) => { 165 | 166 | }); 167 | 168 | let testAttributes = 169 |
170 | 171 | test picture/img.png 172 | 173 | 174 | 175 |
; 176 | 177 | let randomElement = ; 178 | 179 | [@react.component] 180 | let make = (~name, ~isDisabled=?, ~onClick=?) => { 181 | ; 131 | ``` 132 | 133 | At present, the PPX produces the following output for the code above: 134 | 135 | ```ocaml 136 | let makeProps : 137 | name:'name 138 | -> ?key:string 139 | -> unit 140 | -> < name: 'name Js_of_ocaml.Js.readonly_prop > Js_of_ocaml.Js.t = 141 | fun ~name ?key _ -> 142 | let open Js_of_ocaml.Js.Unsafe in 143 | obj 144 | ( [| Option.map (fun raw -> ("key", inject (Js_of_ocaml.Js.string raw))) key 145 | ; Some ("name", inject name) |] 146 | |> Array.to_list 147 | |> List.filter_map (fun x -> x) 148 | |> Array.of_list ) 149 | 150 | let make ~name = 151 | React.Dom.createDOMElementVariadic "button" 152 | ~props:(Js_of_ocaml.Js.Unsafe.obj [||]) 153 | [ReasonReact.string ("Hello " ^ name ^ "!")] 154 | 155 | let make = 156 | let (Test 157 | (Props : < name: 'name Js_of_ocaml.Js.readonly_prop > Js_of_ocaml.Js.t) 158 | ) = 159 | make 160 | ~name: 161 | (fun (type res a0) (a0 : a0 Js_of_ocaml.Js.t) 162 | (_ : a0 -> < get: res ; .. > Js_of_ocaml.Js.gen_prop) -> 163 | (Js_of_ocaml.Js.Unsafe.get a0 "name" : res) 164 | (Props : < .. > Js_of_ocaml.Js.t) 165 | (fun x -> x#name) ) 166 | in 167 | Test 168 | ``` 169 | 170 | The PPX would need to add to the output an extra function at the end of the component implementation, that just calls `React.createElement` and pass `make` and `makeProps` defined before: 171 | 172 | ```ocaml 173 | let makeProps ... (* same as before *) 174 | 175 | let make ~name = ... (* same *) 176 | 177 | let make = ... (* same *) 178 | 179 | let make ~name () = React.createElement make (makeProps ~name ()) 180 | ``` 181 | 182 | - **New input** 183 | 184 | After the change above, we can create component elements with just a function call from OCaml syntax: 185 | 186 | ```ocaml 187 | let my_element = Greeting.make ~name:"John" () 188 | ``` 189 | 190 | ### Implementation of components 191 | 192 | - **Current input** 193 | 194 | Right now, components are defined with an attribute attached to the component value binding, e.g.: 195 | 196 | ```ocaml 197 | module Greeting = struct 198 | let make () = React.string "Hello world from OCaml" [@@react.component] 199 | end 200 | ``` 201 | 202 | - **Proposal** 203 | 204 | For OCaml syntax, it would be much friendlier to use infix syntax for component implementation, like it's done in [brisk-reconciler](https://github.com/briskml/brisk-reconciler/). 205 | 206 | The example above would look like: 207 | 208 | ```ocaml 209 | module Greeting = struct 210 | let%component make () = React.string "Hello world from OCaml" 211 | end 212 | ``` 213 | 214 | ### Expose API in snake_case 215 | 216 | All APIs should be aliased to make them available in snake case, e.g. functions like `React.use_state`, or props like `on_click`. 217 | 218 | ### Summary 219 | 220 | Here is a component with some jsoo-react functionality to see how all the different parts would look like together: 221 | 222 | ```ocaml 223 | [@@@react.dom] 224 | 225 | open Bindings 226 | 227 | type action = Increment | Decrement 228 | 229 | let reducer state action = 230 | match action with Increment -> state + 1 | Decrement -> state - 1 231 | 232 | let s = React.string 233 | 234 | let space = " " |> s 235 | 236 | let%component make ?(name = "buddy") ?children = 237 | let count, set_count = React.use_state (fun () -> 0) in 238 | let state, dispatch = React.use_reducer reducer 0 in 239 | div 240 | ~id:"counter" 241 | ~children: 242 | [ p ~children:["Hello " ^ name ^ "!" |> s] () 243 | ; button 244 | ~on_click:(fun _ -> 245 | Console.log "Click!" ; 246 | set_count (fun c -> c + 1) ) 247 | ~children:["Count: " ^ string_of_int count |> s] 248 | () 249 | ; space 250 | ; button 251 | ~on_click:(fun _ -> dispatch @@ Decrement) 252 | ~children:["Dec" |> s] () 253 | ; space 254 | ; span ~children:[string_of_int state |> s] () 255 | ; space 256 | ; button 257 | ~on_click:(fun _ -> dispatch @@ Increment) 258 | ~children:["Inc" |> s] () 259 | ; space 260 | ; ( match children with 261 | | Some c -> 262 | div ~children:[c] () 263 | | None -> 264 | React.null ) ] 265 | () 266 | ``` 267 | 268 | 269 | # Drawbacks 270 | 271 | - Performance: some of the points above like "1. Creating DOM elements" will involve more work from the PPX, as it has to check every `Pexp_apply` expression and then compare its `Pexp_ident` with all DOM elements `p`, `div`,... tag strings. 272 | - Confusion and bad user experience: the proposal "1. Creating DOM elements" will lead to the PPX processing _every_ function with a name like DOM elements (`p`, `div`). This can lead to opaque compilation errors and confusion if the user redefines any of these functions for different purposes. 273 | - Maintenance burden: the additions proposed will involve more maintenance work, and more use cases to support. 274 | - Backwards compatibility: at some point we might decide to drop support for old ways to implement components using `[@react.component]`. This would mean that it'd not be as straight forward to migrate existing [ReasonReact](https://reasonml.github.io/reason-react) apps to jsoo-react. 275 | 276 | # Unresolved questions 277 | 278 | - Is there a better way to deal with DOM elements than using floating attributes? -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name test_jsoo_react) 3 | (libraries webtest webtest-js gen_js_api react jsoo_react_test js_of_ocaml) 4 | (preprocess 5 | (pps jsoo-react.ppx js_of_ocaml-ppx)) 6 | (js_of_ocaml 7 | (javascript_files react-requires.js jsdom.js)) 8 | (modes js)) 9 | 10 | (rule 11 | (targets jsdom.ml) 12 | (deps jsdom.mli) 13 | (action 14 | (run %{bin:gen_js_api} -o %{targets} %{deps}))) 15 | 16 | (rule 17 | (alias runtest) 18 | (deps 19 | external.js 20 | test_jsoo_react.bc.js 21 | (source_tree node_modules)) 22 | (action 23 | (run node test_jsoo_react.bc.js))) 24 | 25 | (data_only_dirs node_modules) 26 | -------------------------------------------------------------------------------- /test/external.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | 3 | function Greeting({ 4 | name, 5 | strong, 6 | }) { 7 | var name = strong === true 8 | ? React.createElement("strong", null, name) 9 | : name; 10 | 11 | return React.createElement("span", null, "Hey ", name); 12 | } 13 | 14 | exports.Greeting = Greeting; 15 | 16 | function GreetingChildren({ 17 | children 18 | }) { 19 | return React.createElement("span", null, "Hey ", children); 20 | } 21 | 22 | exports.GreetingChildren = GreetingChildren; 23 | 24 | function Greetings({ 25 | names 26 | }) { 27 | return React.createElement("span", null, "Hey ", names.join(", ")); 28 | } 29 | 30 | exports.Greetings = Greetings; 31 | 32 | // React.forwardRef is a non-function wrapper component, at least at time of writing 33 | exports.NonFunctionGreeting = React.forwardRef(function Greeting({ 34 | name, 35 | strong, 36 | }, ref) { 37 | var name = strong === true 38 | ? React.createElement("strong", null, name) 39 | : name; 40 | 41 | return React.createElement("span", { ref: ref }, "Hey ", name); 42 | }); 43 | -------------------------------------------------------------------------------- /test/jsdom.js: -------------------------------------------------------------------------------- 1 | joo_global_object.__LIB__jsdom = require('jsdom').JSDOM; 2 | -------------------------------------------------------------------------------- /test/jsdom.mli: -------------------------------------------------------------------------------- 1 | type t = private Ojs.t 2 | 3 | val t_of_js : Ojs.t -> t 4 | val t_to_js : t -> Ojs.t 5 | val make : html:string -> t [@@js.new "__LIB__jsdom"] 6 | 7 | [@@@js.stop] 8 | 9 | type window = Js_of_ocaml.Dom_html.window Js_of_ocaml.Js.t 10 | 11 | [@@@js.start] 12 | 13 | [@@@js.implem 14 | type window = Js_of_ocaml.Dom_html.window Js_of_ocaml.Js.t 15 | 16 | external window_of_js : t -> window = "%identity" 17 | external window_to_js : window -> t = "%identity"] 18 | 19 | val window : t -> window [@@js.get] 20 | 21 | [@@@js.stop] 22 | 23 | type document = Js_of_ocaml.Dom_html.document Js_of_ocaml.Js.t 24 | 25 | [@@@js.start] 26 | 27 | [@@@js.implem 28 | type document = Js_of_ocaml.Dom_html.document Js_of_ocaml.Js.t 29 | 30 | external document_of_js : t -> document = "%identity" 31 | external document_to_js : document -> t = "%identity"] 32 | 33 | val document : window -> document [@@js.get] 34 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "jsdom": "^16.4.0", 8 | "react": "^18.1.0", 9 | "react-dom": "^18.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/react-requires.js: -------------------------------------------------------------------------------- 1 | joo_global_object.React = require('react'); 2 | joo_global_object.ReactDOM = require("react-dom"); 3 | -------------------------------------------------------------------------------- /test/test_jsoo_react.ml: -------------------------------------------------------------------------------- 1 | open Webtest.Suite 2 | 3 | let suite = "baseSuite" >::: [ Test_reason.suite; Test_ml.suite ] 4 | let () = Webtest_js.Runner.run suite 5 | --------------------------------------------------------------------------------