├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .munit ├── README.md ├── changes.md ├── doc ├── compilation-flags.md ├── control-structures.md ├── custom-meta.md ├── defines.json ├── metadata.json ├── react-next-roadmap.md ├── react-next.md ├── react-refs-api.md ├── static-components.md ├── typing-props.md └── wrapping-with-hoc.md ├── extraParams.hxml ├── haxelib.json ├── mdk └── info.json ├── releaseHaxelib.sh ├── res └── gen │ ├── GenHtmlEntities.hx │ └── Main.hx ├── samples ├── docs │ ├── .gitignore │ ├── README.md │ ├── build-static.hxml │ ├── build.hxml │ ├── common.hxml │ ├── install.hxml │ ├── makefile │ ├── package.json │ ├── res │ │ └── base.css │ └── src │ │ ├── AppContext.hx │ │ ├── Main.hx │ │ ├── StaticGenerator.hx │ │ ├── comp │ │ ├── App.hx │ │ ├── Article.hx │ │ ├── SidePanel.hx │ │ ├── html │ │ │ └── ExternalLink.hx │ │ ├── import.hx │ │ └── layout │ │ │ ├── MainContent.hx │ │ │ ├── MainContentContainer.hx │ │ │ ├── MainWrapper.hx │ │ │ ├── StaticBorderBottom.hx │ │ │ └── StaticBorderTop.hx │ │ └── data │ │ └── DocChapter.hx └── todoapp │ ├── bin │ ├── index.html │ └── styles.css │ ├── build.hxml │ ├── install.hxml │ ├── readme.md │ └── src │ ├── Main.hx │ ├── store │ ├── TodoActions.hx │ ├── TodoItem.hx │ └── TodoStore.hx │ └── view │ ├── TodoApp.hx │ └── TodoList.hx ├── src └── lib │ └── react │ ├── BaseProps.hx │ ├── Empty.hx │ ├── Fragment.hx │ ├── Partial.hx │ ├── PureComponent.hx │ ├── React.hx │ ├── ReactClipboardEvent.hx │ ├── ReactComponent.hx │ ├── ReactComponentMacro.hx │ ├── ReactContext.hx │ ├── ReactDOM.hx │ ├── ReactDOMServer.hx │ ├── ReactEvent.hx │ ├── ReactFocusEvent.hx │ ├── ReactKeyboardEvent.hx │ ├── ReactMacro.hx │ ├── ReactMouseEvent.hx │ ├── ReactNode.hx │ ├── ReactPropTypes.hx │ ├── ReactRef.hx │ ├── ReactSharedInternals.hx │ ├── ReactTestUtils.hx │ ├── ReactTouchEvent.hx │ ├── ReactType.hx │ ├── ReactUIEvent.hx │ ├── ReactUtil.hx │ ├── ReactWheelEvent.hx │ ├── StrictMode.hx │ ├── Suspense.hx │ ├── jsx │ ├── AriaAttributes.hx │ ├── JsxLiteral.hx │ ├── JsxMacro.hx │ ├── JsxPropsBuilder.hx │ └── JsxStaticMacro.hx │ └── macro │ ├── ContextMacro.hx │ ├── MacroUtil.hx │ ├── PropsValidator.hx │ ├── PureComponentMacro.hx │ ├── ReactComponentMacro.hx │ ├── ReactDebugMacro.hx │ ├── ReactMeta.hx │ ├── ReactTypeMacro.hx │ └── ReactWrapperMacro.hx ├── tagNext.sh ├── tagRelease.sh ├── test.hxml └── test ├── src ├── AssertTools.hx ├── ReactMacroTest.hx ├── TestMain.hx ├── TestSuite.hx ├── react │ ├── Fragment.hx │ ├── React.hx │ └── ReactComponent.hx └── support │ └── sub │ ├── CompExternModule.hx │ └── CompModule.hx └── test.hxml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [*.{json,yml}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, repository_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | haxe: [4.3.2, latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Setup haxe ${{ matrix.haxe }} 18 | uses: krdlab/setup-haxe@v1 19 | with: 20 | haxe-version: ${{ matrix.haxe }} 21 | 22 | - name: Install libs 23 | run: | 24 | git config --global url."https://github.com/".insteadOf "git@github.com:" 25 | haxelib newrepo 26 | haxelib install munit 27 | haxelib install hxnodejs 28 | haxelib git tink_hxx git@github.com:kLabz/tink_hxx.git 29 | haxelib git tink_anon git@github.com:haxetink/tink_anon.git 30 | 31 | - name: Run tests 32 | run: | 33 | haxelib run munit test -js 34 | 35 | - name: "[Samples] SSR sample : react-next docs website" 36 | run: | 37 | cd samples/docs 38 | make setup 39 | make server 40 | make static 41 | 42 | - name: "[Samples] TODO App" 43 | run: | 44 | cd samples/todoapp 45 | haxelib newrepo 46 | haxelib dev react-next ../.. 47 | haxelib install --skip-dependencies --always install.hxml 48 | haxe build.hxml 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example-minimal/node_modules 3 | example-commentbox/node_modules 4 | example-filterproducts/node_modules 5 | /.idea 6 | *.iml 7 | *.hxproj 8 | *.map 9 | test/report/ 10 | test/build/ 11 | haxe-react.zip 12 | .haxelib 13 | -------------------------------------------------------------------------------- /.munit: -------------------------------------------------------------------------------- 1 | version=munit 2 | src=test/src 3 | bin=test/build 4 | report=test/report 5 | hxml=test/test.hxml 6 | classPaths=src 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haxe React [#next](./doc/react-next.md) temp fork (See [Roadmap](./doc/react-next-roadmap.md)) 2 | 3 | [![TravisCI Build Status](https://travis-ci.org/kLabz/haxe-react.svg?branch=next)](https://travis-ci.org/kLabz/haxe-react) 4 | [![Haxelib Version](https://img.shields.io/github/tag/kLabz/haxe-react.svg?label=haxelib)](http://lib.haxe.org/p/react-next) 5 | [![Join the chat at https://gitter.im/haxe-react](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/haxe-react/Lobby) 6 | 7 | A Haxe library offering externs and tool functions leveraging Haxe's excellent type system and 8 | compile time macros to offer a strongly typed language to work with the increasingly popular 9 | [React](https://facebook.github.io/react/) library. 10 | 11 | haxelib install react-next 12 | 13 | ### ⚠️ Warning: README.md outdated 14 | 15 | This readme needs many updates. See [changes in #next](./doc/react-next.md); other docs available 16 | in [`doc` folder](./doc/). 17 | 18 | ### What's included / not included 19 | 20 | This library covers React core and ReactDOM. 21 | It does NOT cover: ReactAddOns, react-router or React Native. 22 | 23 | We recommend looking into / contributing to the following efforts: 24 | 25 | - https://github.com/haxe-react (React native and others libs) 26 | - https://github.com/tokomlabs/haxe-react-addons (various externs to pick) 27 | 28 | 29 | ### Application architecture examples 30 | 31 | React doesn't enforce any specific application architecture; here are a few approaches: 32 | 33 | **Redux**, a very popular new approach of Model-View-Intent architecture: global state object, 34 | following immutability principles, but the wiring has been re-imagined to use the best of Haxe: 35 | 36 | - https://github.com/elsassph/haxe-react-redux 37 | 38 | **MMVC**, classic, battle tested, Model-View-Mediator with state of the art Dependency Injection: 39 | 40 | - https://github.com/elsassph/haxe-react-mmvc 41 | 42 | **Flux**, inspired by Facebook's suggested architecture for React; this is a very quick PoC 43 | which probably won't scale well to complex apps, but it shows a good range of React features: 44 | 45 | - https://github.com/massiveinteractive/haxe-react/tree/master/samples/todoapp 46 | 47 | 48 | ### Support / discussions 49 | 50 | If you have questions / issues, join [haxe-react on Gitter.im](https://gitter.im/haxe-react/Lobby) 51 | 52 | 53 | ## API 54 | 55 | Most of the regular React API is integrated (non-JSX example): 56 | 57 | ```haxe 58 | import react.React; 59 | import react.ReactDOM; 60 | 61 | class App extends ReactComponent { 62 | 63 | static public function main() { 64 | ReactDOM.render(React.createElement(App), Browser.document.getElementById('app')); 65 | } 66 | 67 | public function new() { 68 | super(); 69 | } 70 | 71 | override function render() { 72 | var cname = 'foo'; 73 | return React.createElement('div', {className:cname}, [/*children*/]); 74 | } 75 | } 76 | ``` 77 | 78 | Note that `React.createElement` strictly expects either a `String`, a `Function`, or a class 79 | extending `ReactComponent`. It includes when writing externs for 3rd party JS libraries you 80 | must specify `extends`: 81 | 82 | ```haxe 83 | @:jsRequire('react-redux', 'Provider') 84 | extern class Provider extends react.ReactComponent { } 85 | ``` 86 | 87 | ## JSX 88 | 89 | The Haxe compiler (and editors) doesn't allow to use exactly the JSX XML DSL, 90 | so we had to compromise a bit... 91 | 92 | This library's take on JSX is to use a compile-time macro to parse JSX as a string to generate 93 | the same kind of code that Facebook's JSX, Babel and Typescript will generate. 94 | 95 | Both classic JSX `{}` binding and Haxe string interpolation `$var` / `${expression}` / `<$Comp>` 96 | are allowed. The advantage of string interpolation is Haxe editor supports for completion and 97 | code navigation. 98 | 99 | Spread operator and complex expressions within curly braces are supported. 100 | 101 | ```haxe 102 | import react.React; 103 | import react.ReactDOM; 104 | import react.ReactMacro.jsx; 105 | 106 | class App extends ReactComponent { 107 | 108 | static public function main() { 109 | ReactDOM.render(jsx(''), Browser.document.getElementById('app')); 110 | } 111 | 112 | public function new() { 113 | super(); 114 | } 115 | 116 | override function render() { 117 | var cname = 'foo'; 118 | return jsx(' 119 |
120 | 121 | ${/*children*/} 122 |
123 | '); 124 | } 125 | 126 | static function statelessComponent(props:Dynamic) { 127 | return jsx('
'); 128 | } 129 | } 130 | ``` 131 | 132 | ### JSX Fragments 133 | 134 | [Fragments](https://reactjs.org/docs/fragments.html) (React 16.2+) let you group 135 | a list of children without adding extra nodes to the DOM. 136 | 137 | Two syntaxes are supported: 138 | ```jsx 139 | 140 | Text 141 | more text 142 | Still more text 143 | 144 | 145 | // or short syntax: 146 | <> 147 | Text 148 | more text 149 | Still more text 150 | 151 | ``` 152 | 153 | ### JSX gotchas 154 | 155 | 1. JSX is not String magic! **Do not concatenate Strings** to construct the JSX expression 156 | 157 | 2. Haxe's JSX parser is not "re-entrant" 158 | 159 | In JavaScript you can nest JSX inside curly-brace expressions: 160 | ```javascript 161 | return ( 162 |
{ isA ? : }
163 | ); 164 | ``` 165 | 166 | However this isn't allowed in Haxe, so you must extract nested JSX into variables: 167 | ```haxe 168 | var content = isA ? jsx('') : jsx(''); 169 | return jsx('
{content}
'); 170 | ``` 171 | 172 | ## Components strict typing 173 | 174 | The default `ReactComponent` type is a shorthand for `ReactComponentOf`, 175 | a fully untyped component. 176 | 177 | To fully benefit from Haxe's strict typing you should look into extending a stricter base class: 178 | 179 | ```haxe 180 | typedef ReactComponentOfProps = ReactComponentOf; 181 | typedef ReactComponentOfState = ReactComponentOf; 182 | typedef ReactComponentOfPropsAndState = ReactComponentOf; 183 | ``` 184 | 185 | ## React JS dependency 186 | 187 | There are 2 ways to link the React JS library: 188 | 189 | ### Require method (default) 190 | 191 | By default the library uses `require('react')` to reference React JS. 192 | 193 | This means you are expected to use `npm` to install this dependency: 194 | 195 | npm install react 196 | 197 | and a second build step to generate the final JS file, for instance using `browserify`: 198 | 199 | npm install browserify 200 | browserify haxe-output.js -o final-output.js 201 | 202 | (note that you can use `watchify` to automatically run this build step) 203 | 204 | ### Global JS 205 | 206 | The other common method is to download or reference the CDN files of React JS in your HTML page: 207 | 208 | ```html 209 | 210 | 211 | ``` 212 | 213 | and don't forget to add the following Haxe define to your build command: 214 | 215 | -D react_global 216 | 217 | Look at `samples/todoapp` for an example of this approach. 218 | 219 | 220 | ## JSX Optimizing Compiler 221 | 222 | ### Inline ReactElements 223 | 224 | By default, when building for release (eg. without `-debug`), calls to `React.createElement` are replaced by inline JS objects (if possible). 225 | 226 | See: https://github.com/facebook/react/issues/3228 227 | 228 | ```javascript 229 | // regular 230 | return React.createElement('div', {key:'bar', className:'foo'}); 231 | 232 | // inlined (simplified) 233 | return {$$typeof:Symbol.for('react.element'), type:'div', props:{className:'foo'}, key:'bar'} 234 | ``` 235 | 236 | This behaviour can be **disabled** using `-D react_no_inline`. 237 | 238 | ## Optimization tools 239 | 240 | ### Avoidable renders warning 241 | 242 | Setting `-D react_render_warning` will enable runtime warnings for avoidable renders. 243 | 244 | This will add a `componentDidUpdate` (or update the existing one) where a **shallowCompare** is done on current and previous props and state. If both did not change, a warning will be displayed in the console. 245 | 246 | False positives can happen if your props are not flat, due to the shallowCompare. 247 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | ### 1.4.0 4 | 5 | - Generate `displayName` for `@:jsxStatic` components #86 6 | - React 16.2: added Fragments support #87: https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html 7 | - User overloads instead of `EitherType` for `setState` #91 8 | - Added utility function `ReactUtil.copyWithout` #96 9 | - ReactComponent macros refactoring #97 10 | - Travis CI 11 | 12 | ### 1.3.0 13 | 14 | - React 16 support; React 15 is still compatible but won't support new APIs (`componentDidCatch`, `createPortal`) 15 | - added missing `ReactDOM.hydrate` method (server-side rendering) 16 | - added `@:jsxStatic` optional meta 17 | - breaking: `react.ReactPropTypes` now requires the NPM `prop-types` module 18 | 19 | ### 1.2.1 20 | 21 | - fixed auto-complete issue on `this.state` caused by the `1.2.0` changes 22 | 23 | ### 1.2.0 24 | 25 | - `setState` now accepts `Partial`; where `T` is a `typedef`, `Partial` is `T` will all the fields made optional 26 | - `react.React.PropTypes` removed in favor of `react.ReactPropTypes` 27 | - added `-D react_render_warning` option 28 | -------------------------------------------------------------------------------- /doc/compilation-flags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom compilation flags 3 | --- 4 | 5 | # Custom compilation flags used by haxe-react 6 | 7 | ## `-debug` / `-D debug` 8 | 9 | This is not a custom compilation flag, but it enables/disables some features in 10 | haxe-react. Basically, you'll want to compile with it when using dev version of 11 | react, and remove it when using prod version. 12 | 13 | ### Disabled features 14 | 15 | #### Inline ReactElements 16 | 17 | By default, when building for release (eg. without `-debug`), calls to 18 | `React.createElement` are replaced by inline JS objects (if possible). This can 19 | also be disabled with `-D react_no_inline`. 20 | 21 | See: https://github.com/facebook/react/issues/3228 22 | 23 | ```javascript 24 | // regular 25 | return React.createElement('div', {key:'bar', className:'foo'}); 26 | 27 | // inlined (simplified) 28 | return {$$typeof:Symbol.for('react.element'), type:'div', props:{className:'foo'}, key:'bar'} 29 | ``` 30 | 31 | ### Enabled features 32 | 33 | #### Display names 34 | 35 | When compiling in debug mode, display names are added for your components, for 36 | use with your browser's react developer tools. 37 | 38 | #### React shared internals 39 | 40 | Some react shared internals data are only available when compiling in debug mode 41 | (and using dev version of react), for advanced debugging. 42 | 43 | #### React runtime warnings 44 | 45 | Haxe-react adds some runtime warnings when enabled with both debug mode and 46 | `-D react_runtime_warnings`. There is still work to be done here, especially 47 | with the re-render warnings that have some false positives atm. 48 | 49 | This warnings include: 50 | 51 | * Runtime warnings about avoidable re-renders, unless you also compile with 52 | `-D react_runtime_warnings_ignore_rerender` **or** for a specific component if 53 | you add a `@:ignoreRenderWarning` meta to it. This feature can be disabled 54 | because it can have false positives when dealing with legacy context API or 55 | hot reloading. 56 | 57 | * Runtime errors when a stateful component does not initialize its state inside 58 | its constructor. There would be errors thrown by react later in the component 59 | lifecycle, but without the source cause. 60 | 61 | ```haxe 62 | js.Browser.console.error( 63 | 'Warning: component ${inClass.name} is stateful but its ' 64 | + '`state` is not initialized inside its constructor.\n\n' 65 | 66 | + 'Either add a `state = { ... }` statement to its constructor ' 67 | + 'or define this component as a `ReactComponentOfProps` ' 68 | + 'if it is only using `props`.\n\n' 69 | 70 | + 'If it is using neither `props` nor `state`, you might ' 71 | + 'consider using `@:jsxStatic` to avoid unneeded lifecycle. ' 72 | + 'See https://github.com/kLabz/haxe-react/blob/next/doc/static-components.md ' 73 | + 'for more information on static components.' 74 | ); 75 | ``` 76 | 77 | #### Runtime errors for `null` nodes 78 | 79 | `ReactType` adds a runtime error when a node ends up with a `null` value, which 80 | is usually due to an extern component not resolving to its real value. 81 | 82 | ## `-D react_global` 83 | 84 | Use this compilation flag when you are loading react by embedding react js files 85 | in your HTML page (instead of using `require()` from modular or webpack). 86 | 87 | ## `-D react_hot` 88 | 89 | Adds some data needed for react hot reloading with modular / webpack. See 90 | [haxe-modular documentation](https://github.com/elsassph/haxe-modular/blob/master/doc/hmr-usage.md). 91 | 92 | ## `-D react_deprecated_context` 93 | 94 | The `context` field on `ReactComponent` has been removed because it has been 95 | deprecated in react and its use as a class field is discouraged. You can add it 96 | back with this flag if needed. 97 | 98 | ## `-D react_ignore_failed_props_inference` 99 | 100 | Jsx parser currently cannot apply type checker on some components (see 101 | [#7](https://github.com/kLabz/haxe-react/issues/7)) and will produce warnings. 102 | 103 | This compilation flags disable these warnings. 104 | 105 | ## `-D react_wrap_strict` 106 | 107 | Enable strict mode for `@:wrap` HOC wrapping. This will ensure you define the 108 | public props type of your component wrapped with `@:wrap` so that jsx type 109 | checking can be done. 110 | See [Wrapping your components in HOCs](./wrapping-with-hoc.md). 111 | 112 | ## `-D react_check_jsxstatic_type` 113 | 114 | Enable prototype type checker for `@:jsxStatic` expressions. 115 | 116 | ## `-D react_jsx_no_aria` 117 | 118 | Since [`affd6e4a`][affd6e4a], `aria-*` props are type-checked against their 119 | [specification][aria-specs], and enums are provided where necessary (see 120 | `react.jsx.AriaAttributes`). They are enabled by default for both html elements 121 | and react components. You can disable support for `aria-*` props entirely with 122 | `-D react_jsx_no_aria`, or only disable it for react components with 123 | `-D react_jsx_no_aria_for_components`. 124 | 125 | ## `-D react_jsx_no_data_for_components` 126 | 127 | Since [`affd6e4a`][affd6e4a], `data-*` props are enabled by default for react 128 | components (all unknown props are already accepted for html elements), with a 129 | type of `Dynamic`. This behavior can be disabled with 130 | `-D react_jsx_no_data_for_components`, meaning that `data-*` props need to be 131 | explicitely accepted by the components (which is currently not that easy in 132 | Haxe). 133 | 134 | ## `-D react_auto_jsx` (haxe 4 only) 135 | 136 | Haxe 4 introduces inline xml-like markup, allowing us to drop the strings in our 137 | `jsx(...)` expressions. 138 | 139 | With this option, `react-next` goes one step further and automatically wraps all 140 | xml markup in `render` (and also `renderSomething`) functions in classes 141 | extending `ReactComponent` in a call to `ReactMacro.jsx()`, as well as in the 142 | methods used in `@:jsxStatic(myMethod)`. 143 | 144 | Note that this feature has been reported to be incompatible with `cococonut`. 145 | 146 | [affd6e4a]: https://github.com/kLabz/haxe-react/commit/affd6e4a 147 | [aria-specs]: https://www.w3.org/TR/wai-aria-1.1/#state_prop_def 148 | -------------------------------------------------------------------------------- /doc/control-structures.md: -------------------------------------------------------------------------------- 1 | ## Control structures 2 | 3 | React next supports `tink_hxx` control structures. This doc comes directly from 4 | `tink_hxx`'s README with minor adaptations for use with Haxe React. 5 | 6 | HXX has support for a few control structures. Their main reason for existence 7 | is that implementing a reentrant parser with autocompletion support proved 8 | rather problematic in Haxe 3. 9 | 10 | ### If 11 | 12 | This is what conditionals look like: 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ``` 28 | 29 | Note that `else` (as well as `elseif`) is optional and that both `elseif` and 30 | `else if` will work. 31 | 32 | ### Switch 33 | 34 | Switch statements are also supported, including guards but without `default` 35 | branches (just use a catch-all `case` instead). The above example for 36 | conditionals would look like this: 37 | 38 | ```html 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ``` 61 | 62 | ### For 63 | 64 | For loops are pretty straight forward: 65 | 66 | ```html 67 | 68 | 69 | 70 | ``` 71 | 72 | ### Let 73 | 74 | You can define variables with `` and access them within the tag. 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /doc/custom-meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom metadata 3 | --- 4 | 5 | # Custom meta used by haxe-react 6 | 7 | ## `@:wrap(hoc)` 8 | 9 | Wrap current component (must extend `ReactComponent`) in a HOC. 10 | 11 | See [Wrapping your components in HOCs](./wrapping-with-hoc.md) for more 12 | information about this meta and the following related meta: 13 | 14 | #### `@:publicProps(TProps)` 15 | 16 | Set public props type to ensure jsx type checking. 17 | 18 | #### `@:noPublicProps` 19 | 20 | Disallow public props for this component when used in jsx. 21 | 22 | #### `@:wrapped_by_macro` 23 | 24 | This special meta is added by the `@:wrap` macro for internal use, do not set it 25 | if you don't want to break the functionality. 26 | 27 | ## `@:jsxStatic(method)` 28 | 29 | Create a static component that you can use in jsx (and where "real" components 30 | are expected) from a static function from a class (**not** a `ReactComponent` 31 | class). 32 | 33 | See [Static components](./static-components.md). 34 | 35 | ## `@:ignoreEmptyRender` 36 | 37 | There is a compile-time check for an override of the `render` function in your 38 | components. This helps catching following runtime warning sooner: 39 | 40 | Warning: Index(...): No `render` method found on the returned component 41 | instance: you may have forgotten to define `render`. 42 | 43 | Catching it at compile-time also ensures it does not happen to a component only 44 | visible for a few specific application state. 45 | 46 | You can disable this with the `-D react_ignore_empty_render` compilation flag, 47 | or for a specific component by adding `@:ignoreEmptyRender` meta to it. 48 | 49 | ## `@:pureComponent` 50 | 51 | TODO: Documentation for macro implementation of pure components. 52 | 53 | ## `@:ignoreRenderWarning` 54 | 55 | TODO: Documentation for runtime warnings. 56 | 57 | ## `@:acceptsMoreProps` 58 | 59 | Some components accept specific props, but also any number of additional props 60 | that are usually passed down to an unknown child component. 61 | 62 | This is not the safest pattern out there, but you might have to write or use 63 | externs for this kind of components. Haxe React jsx parser being very strict 64 | with the props, this meta was needed to define this behavior. 65 | 66 | ```haxe 67 | typedef Props = { /* define your props here */ } 68 | 69 | @:acceptsMoreProps 70 | class MyComponent extends ReactComponentOfProps {} 71 | ``` 72 | 73 | ### Custom props validators with `@:acceptsMoreProps('validator_key')` 74 | 75 | (New in react-next 1.105.0) 76 | 77 | Sometimes, especially when dealing with externs, you want to be able to validate 78 | props in a way that is not really possible with haxe type system. 79 | 80 | You can register custom props validator (at macro level) for your component with 81 | an initialization macro in your `.hxml`: 82 | 83 | ``` 84 | --macro pack.InitMacro.registerValidator() 85 | ``` 86 | 87 | This macro will look like this: 88 | 89 | ```haxe 90 | package pack; 91 | 92 | import haxe.macro.Expr; 93 | import react.macro.PropsValidator; 94 | 95 | class InitMacro { 96 | // Initialization macro doing the registration 97 | public static function registerValidator() { 98 | PropsValidator.register('my_very_unique_key', validator); 99 | } 100 | 101 | // The actual validator 102 | public static function validator(name:String, expr:Expr):Null { 103 | if (some_condition) { 104 | // Ok, I recognize this prop! 105 | // Add an `ECheckType` around the expr to validate the props typing 106 | // Note: just return `expr` if you don't want to check its type 107 | var expectedType:ComplexType = macro :ExpectedType; 108 | return macro @:pos(expr.pos) (${expr}:$expectedType); 109 | } 110 | 111 | // This prop isn't known by the validator, let jsx throw the usual error 112 | return null; 113 | } 114 | } 115 | ``` 116 | 117 | Your component can the use `@:acceptsMoreProps` to tell the jsx macro how to 118 | validate extra props: 119 | 120 | ```haxe 121 | private typedef Props = { 122 | var normalProp:String; 123 | @:optional var normalOptionalProp:Int; 124 | } 125 | 126 | @:acceptsMoreProps('my_very_unique_key') 127 | class MyComponent extends ReactComponentOfProps { 128 | // ... 129 | } 130 | ``` 131 | 132 | Note that you will have to avoid validator key conflict yourself, so make sure 133 | your keys will likely be unique (by namespacing, for example), especially if you 134 | use this feature in a library. 135 | -------------------------------------------------------------------------------- /doc/defines.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "define": "react_no_inline", 4 | "doc": "Skip React.createElement inlining optimization." 5 | }, 6 | { 7 | "define": "react_global", 8 | "doc": "Use window.React instead of require() from modular/webpack." 9 | }, 10 | { 11 | "define": "react_hot", 12 | "doc": "Adds some data needed for react hot reloading with modular/webpack." 13 | }, 14 | { 15 | "define": "react_runtime_warnings", 16 | "doc": "Enable some runtime warnings for debug purpose." 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /doc/metadata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "metadata": ":wrap", 4 | "doc": "Wrap current class component in a HOC.", 5 | "params": ["HOC"], 6 | "targets": ["TClass"] 7 | }, 8 | { 9 | "metadata": ":publicProps", 10 | "doc": "Set public props type to ensure jsx type checking.", 11 | "params": ["TProps"], 12 | "targets": ["TClass"] 13 | }, 14 | { 15 | "metadata": ":noPublicProps", 16 | "doc": "Disallow public props for this component when used in jsx.", 17 | "targets": ["TClass"] 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /doc/react-next-roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | title: React "next" Roadmap 4 | --- 5 | 6 | # Haxe React #next: roadmap 7 | 8 | There's still work to be done being this fork is ready for a haxe-react 2.0.0 9 | candidate. This file will be updated with new bugs and new feature ideas that 10 | should be included in the release. 11 | 12 | Done tasks will be deleted from here, if you are looking for differences with 13 | upstream haxe-react, see [React #next doc](./react-next.md). 14 | 15 | PRs are welcome for helping with any of these, but you may want to get in touch 16 | (via gitter for example) before doing so, as some features/fix are already 17 | started and sometimes almost finished. 18 | 19 | If you have wishes or suggestions, come to gitter or open issues here; I'll be 20 | happy to consider them. 21 | 22 | ## Features needing improvements / new features 23 | 24 | * Jsx macro performances improvement to reduce compilation time 25 | * Implement missing React APIs (see #11) 26 | * Update react events handling 27 | 28 | ### Some more things that **may** be added too 29 | 30 | * Inline `@:jsxStatic` proxy field to help with performance and file size 31 | * Some helpers to create HOCs (which can then be used with `@:wrap`) 32 | * More stability with both runtime warnings and hot reloading enabled 33 | * Some improvements for `Partial` 34 | * Generate `propTypes` from components' `TProps` when compiling with `-debug` 35 | and `-D react-generate-proptypes` 36 | 37 | ## Documentation 38 | 39 | * PureComponent 40 | * Runtime warnings documentation 41 | * Update README.md 42 | * Proper migration guide (based off [react #next doc](./react-next.md)) 43 | * Test (and potentially update) samples 44 | * Add more samples for new APIs 45 | -------------------------------------------------------------------------------- /doc/react-refs-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Refs API (React 16.3) 3 | --- 4 | 5 | # New React Refs API (React 16.3) 6 | 7 | Externs have been added for [`React.createRef()`][createRef] and 8 | [`React.forwardRef()`][forwardRef] from the new refs API introduced in React 9 | `16.3`. 10 | 11 | ## Example usage 12 | 13 | ```haxe 14 | import js.html.InputElement; 15 | import react.React; 16 | import react.ReactRef; 17 | import react.ReactComponent; 18 | import react.ReactMacro.jsx; 19 | 20 | class TestComponent extends ReactComponentOfState<{message: String}> { 21 | var inputRef:ReactRef = React.createRef(); 22 | 23 | public function new(props) { 24 | super(props); 25 | 26 | state = {message: null}; 27 | } 28 | 29 | override public function render() { 30 | return jsx(' 31 | <> 32 | ${state.message} 33 | 34 | 35 | 36 | '); 37 | } 38 | 39 | function updateMessage() { 40 | setState({message: inputRef.current.value}); 41 | } 42 | } 43 | ``` 44 | 45 | We can also use `var inputRef = React.createRef();` but I personally prefer to 46 | type my refs to the underlying element. 47 | 48 | [createRef]: https://reactjs.org/docs/react-api.html#reactcreateref 49 | [forwardRef]: https://reactjs.org/docs/react-api.html#reactforwardref 50 | -------------------------------------------------------------------------------- /doc/static-components.md: -------------------------------------------------------------------------------- 1 | # Static components 2 | 3 | Static/functional components (sometimes called presentational or dumb 4 | components) are lightweight components that only rely on their props to render. 5 | 6 | Not being real components means that they are not subject to react component 7 | lifecycle, and so are more lightweight than standard components. 8 | 9 | They serve a different purpose than `PureComponent`: their render function will 10 | still get called everytime their parent updates, regardless of the static 11 | component's props. Static components should have simple render functions, 12 | allowing them to be faster than pure components even if they do not support 13 | `shouldComponentUpdate`. 14 | 15 | Static components should be avoided when their parent updates often and the 16 | static component's props mostly stays the same. Use `PureComponent` for this use 17 | case. 18 | 19 | Static components can be expressed as static functions: 20 | ```haxe 21 | class MyComponents { 22 | public static function heading(props:{children:String}) { 23 | return jsx(' 24 |

${props.content}

25 | '); 26 | } 27 | } 28 | ``` 29 | 30 | And used in your jsx like this: 31 | ```haxe 32 | jsx(' 33 |
34 | <$MyComponents.heading>Hello world! 35 | 36 | ... 37 |
38 | '); 39 | ``` 40 | 41 | But sometimes you want these components to blend in, and be able to call them 42 | just like any other component (especially when you start with a "normal" 43 | component and only then change it into a static component for performance). 44 | 45 | ## `@:jsxStatic` components 46 | 47 | Since haxe-react `1.3.0`, you can use a special meta on any class to transform 48 | it into a static component in the eyes of the jsx parser: 49 | 50 | ```haxe 51 | private typedef Props = { 52 | var children:ReactFragment; 53 | } 54 | 55 | @:jsxStatic(myRenderFunction) 56 | class Heading { 57 | public static function myRenderFunction(props:Props) { 58 | return jsx(' 59 |

${props.content}

60 | '); 61 | } 62 | } 63 | ``` 64 | 65 | Which can be used in jsx just like any other component: 66 | ```haxe 67 | jsx(' 68 |
69 | <$Heading>Hello world! 70 | 71 | ... 72 |
73 | '); 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /doc/typing-props.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typing your props 3 | --- 4 | 5 | # Typing your props with haxe-react 6 | 7 | One of the points of using Haxe to develop javascript applications is to benefit 8 | from a statically typed language. So, naturally, you'll want your components to 9 | be able to define the type of props they are going to get and are able to use. 10 | 11 | ## Using `PropTypes`? 12 | 13 | In React, this is usually done using [`PropTypes`][react-proptypes], which uses 14 | an object with validators for each prop. These validators are runtime ones (they 15 | can also be used by flow or typescript at compile-time), and are not types but 16 | descriptors of types. 17 | 18 | A simplified example from the documentation: 19 | 20 | ```javascript 21 | MyComponent.propTypes = { 22 | optionalArray: PropTypes.array, 23 | optionalBool: PropTypes.bool, 24 | optionalFunc: PropTypes.func, 25 | optionalNumber: PropTypes.number, 26 | requiredFunc: PropTypes.func.isRequired 27 | }; 28 | ``` 29 | 30 | You can declare them with haxe-react by using `react.ReactPropTypes`: 31 | 32 | ```haxe 33 | import react.ReactComponent; 34 | import react.ReactPropTypes as PropTypes; 35 | 36 | class MyComponent extends ReactComponent { 37 | static var propTypes = { 38 | optionalArray: PropTypes.array, 39 | optionalBool: PropTypes.bool, 40 | optionalFunc: PropTypes.func, 41 | optionalNumber: PropTypes.number, 42 | requiredFunc: PropTypes.func.isRequired 43 | }; 44 | 45 | // ... 46 | } 47 | ``` 48 | 49 | Note that this won't do any compile-time check, though, as it is not the primary 50 | typing system for props in haxe-react. 51 | 52 | ## Use real static typing with `TProps` 53 | 54 | In Haxe React, props typing is usually done using `TProps`, a typedef typing the 55 | props your component can use and should be called with. 56 | 57 | These types are enforced at compile times when using jsx (haxe-react #next only) 58 | 59 | The above example would be implemented with something like that: 60 | 61 | ```haxe 62 | import react.ReactComponent; 63 | 64 | typedef MyComponentProps = { 65 | var requiredFunc:String->Void; 66 | @:optional var optionalArray:Array; 67 | @:optional var optionalBool:Bool; 68 | @:optional var optionalFunc:Int->String; 69 | @:optional var optionalNumber:Int; 70 | } 71 | 72 | class MyComponent extends ReactComponentOfProps { 73 | // ... 74 | } 75 | ``` 76 | 77 | 78 | [react-proptypes]: https://reactjs.org/docs/typechecking-with-proptypes.html 79 | -------------------------------------------------------------------------------- /doc/wrapping-with-hoc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wrap with HOC 3 | --- 4 | 5 | # Wrapping your components in HOCs 6 | 7 | You can use HOCs with your components (unless they are `@:jsxStatic` components) 8 | by adding `@:wrap` meta. 9 | 10 | ```haxe 11 | import react.ReactComponent; 12 | import react.ReactMacro.jsx; 13 | import react.router.ReactRouter; 14 | import react.router.Route.RouteRenderProps; 15 | 16 | @:wrap(ReactRouter.withRouter) 17 | class MyComponent extends ReactComponentOfProps { 18 | override public function render() { 19 | return jsx('

Current path is ${props.location.pathname}

'); 20 | } 21 | } 22 | ``` 23 | 24 | You can also combine HOCs for a single component by simply adding more `@:wrap` 25 | meta: 26 | 27 | ```haxe 28 | import react.ReactComponent; 29 | import react.ReactMacro.jsx; 30 | import react.ReactType; 31 | import react.router.ReactRouter; 32 | import react.router.Route.RouteRenderProps; 33 | 34 | private typedef Props = { 35 | > RouteRenderProps, 36 | var answer:Int; 37 | } 38 | 39 | @:wrap(ReactRouter.withRouter) 40 | @:wrap(uselessHoc(42)) 41 | class MyComponent extends ReactComponentOfProps { 42 | static function uselessHoc(value:Int):ReactType->ReactType { 43 | return function(Comp:ReactType) { 44 | return function(props:Any) { 45 | return jsx('<$Comp {...props} answer=${value} />'); 46 | }; 47 | }; 48 | } 49 | 50 | override public function render() { 51 | return jsx(' 52 |

53 | Current path is ${props.location.pathname} and the answer is ${props.answer} 54 |

55 | '); 56 | } 57 | } 58 | ``` 59 | 60 | ## `@:publicProps(TProps)` 61 | 62 | One thing to note, though: you will loose props type checking in jsx. You can 63 | get this back by separating your component's public and final props: 64 | 65 | ```haxe 66 | // Final props 67 | private typedef Props = { 68 | > PublicProps, 69 | > RouteRenderProps, 70 | } 71 | 72 | // Public props, which need to be provided via jsx 73 | private typedef PublicProps = { 74 | var path:String; 75 | } 76 | ``` 77 | 78 | You can then tell `@:wrap` what public props you are expecting by adding a 79 | `@:publicProps` meta, and get back all the props typing (including missing and 80 | extra props warnings/errors) like a "normal" component: 81 | 82 | ```haxe 83 | @:publicProps(PublicProps) 84 | @:wrap(ReactRouter.withRouter) 85 | class MyComponent extends ReactComponentOfProps { 86 | override public function render() { 87 | if (props.path != props.location.pathname) return null; 88 | 89 | return jsx('

Welcome to ${props.path}!

'); 90 | } 91 | } 92 | ``` 93 | 94 | ## `@:noPublicProps` 95 | 96 | However, sometimes your component doesn't have any public prop. You can then use 97 | `@:noPublicProps` meta to get errors when sending extra props to this component: 98 | 99 | ```haxe 100 | import react.ReactComponent; 101 | import react.ReactMacro.jsx; 102 | import react.router.ReactRouter; 103 | import react.router.Route.RouteRenderProps; 104 | 105 | @:noPublicProps 106 | @:wrap(ReactRouter.withRouter) 107 | class MyComponent extends ReactComponentOfProps { 108 | override public function render() { 109 | return jsx('

Current path is ${props.location.pathname}

'); 110 | } 111 | } 112 | ``` 113 | 114 | ## `-D react_wrap_strict` 115 | 116 | You may want to enforce using either `@:publicProps(MyPublicProps)` or 117 | `@:noPublicProps` for all your `@:wrap`-ed components to make sure your jsx 118 | calls are all safe. 119 | 120 | By adding the `react_wrap_strict` compilation flag, you will get compilation 121 | warnings when a `@:wrap`-ed component does not expose its public props type. 122 | 123 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro react.jsx.JsxStaticMacro.addHook() 2 | --macro addGlobalMetadata('', '@:build(react.jsx.JsxStaticMacro.build())') 3 | 4 | -D react=1.122.0 5 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-next", 3 | "license": "MIT", 4 | "tags": [], 5 | "description": "Many breaking changes for react, aiming for a future 2.0.0 release", 6 | "contributors": [ 7 | "klabz" 8 | ], 9 | "releasenote": "ReactTypeOf improvements, and prepare for haxe 4.2", 10 | "version": "1.122.0", 11 | "url": "https://github.com/kLabz/haxe-react", 12 | "classPath": "src/lib", 13 | "documentation": { 14 | "url": "https://github.com/kLabz/haxe-react/tree/next/doc", 15 | "metadata": "doc/metadata.json", 16 | "defines": "doc/defines.json" 17 | }, 18 | "dependencies": { 19 | "tink_hxx": "", 20 | "js-object": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /mdk/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-react", 3 | "version": "1.122.0" 4 | } 5 | -------------------------------------------------------------------------------- /releaseHaxelib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f haxe-react.zip 3 | zip -r haxe-react.zip src haxelib.json README.md changes.md doc extraParams.hxml 4 | # haxelib submit haxe-react.zip $1 $2 --always 5 | haxelib submit haxe-react.zip 6 | -------------------------------------------------------------------------------- /res/gen/GenHtmlEntities.hx: -------------------------------------------------------------------------------- 1 | import haxe.Json; 2 | import haxe.macro.Context; 3 | import haxe.macro.Expr; 4 | import haxe.macro.ExprTools; 5 | import haxe.macro.Printer; 6 | import haxe.macro.TypeTools; 7 | import sys.io.File; 8 | 9 | class GenHtmlEntities 10 | { 11 | static public function main() 12 | { 13 | generate(); 14 | } 15 | 16 | macro static function generate() 17 | { 18 | var src = File.getContent('entities.json'); 19 | src = src.split('\\u').join('\\\\u'); 20 | var json = Json.parse(src); 21 | 22 | var map:Map = new Map(); 23 | for (key in Reflect.fields(json)) 24 | { 25 | map.set(key, Reflect.field(json, key).characters); 26 | } 27 | createClass(map); 28 | 29 | return macro Sys.println('Done.'); 30 | } 31 | 32 | static function createClass(map:Map) 33 | { 34 | var mapExpr = [for (name in map.keys()) macro $v{name} => $v{map.get(name)}]; 35 | 36 | var cl = macro class HtmlEntities { }; 37 | cl.fields = [{ 38 | name: 'map', 39 | access: [Access.AStatic, Access.APublic], 40 | kind: FieldType.FVar(null, macro $a{mapExpr}), 41 | pos: Context.currentPos() 42 | } 43 | ]; 44 | 45 | var printer = new Printer(); 46 | var src = '/* GENERATED, DO NOT EDIT */\npackage react.jsx;\n#if macro\n' 47 | + printer.printTypeDefinition(cl) + '\n#end'; 48 | File.saveContent('../../src/lib/react/jsx/HtmlEntities.hx', src); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /res/gen/Main.hx: -------------------------------------------------------------------------------- 1 | import haxe.Http; 2 | import haxe.Json; 3 | import sys.FileSystem; 4 | import sys.io.File; 5 | 6 | /** 7 | Generates react/jsx/HtmlEntities.hx 8 | 9 | Usage: 10 | haxe -lib hxssl -x Main 11 | **/ 12 | class Main 13 | { 14 | static public function main() 15 | { 16 | Sys.println('Downloading entities...'); 17 | var url = 'https://html.spec.whatwg.org/entities.json'; 18 | var loader = new Http(url); 19 | loader.onData = onData; 20 | loader.request(); 21 | } 22 | 23 | static private function onData(src:String) 24 | { 25 | var json = Json.parse(src); // validate 26 | File.saveContent('entities.json', src); 27 | 28 | Sys.println('Generating sourcecode...'); 29 | Sys.command('haxe', ['-x', 'GenHtmlEntities']); 30 | 31 | FileSystem.deleteFile('Main.n'); 32 | FileSystem.deleteFile('GenHtmlEntities.n'); 33 | FileSystem.deleteFile('entities.json'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/docs/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .haxelib 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /samples/docs/README.md: -------------------------------------------------------------------------------- 1 | # Example project: Server side rendering (no hydration) 2 | 3 | TODO: add some documentation here, or in `docs` and link here. 4 | 5 | Run `make setup` to install dependencies. 6 | 7 | Run `make server` to build and then `make start-server` to start the server on 8 | port `8042`. 9 | 10 | To generate static html pages instead, run `make static`, and then 11 | `make serve-static` to start a http server serving html files on port `8043`. 12 | -------------------------------------------------------------------------------- /samples/docs/build-static.hxml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env haxe 2 | 3 | common.hxml 4 | 5 | -main StaticGenerator 6 | -js bin/static-gen.js 7 | -------------------------------------------------------------------------------- /samples/docs/build.hxml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env haxe 2 | 3 | common.hxml 4 | 5 | -main Main 6 | -js bin/server.js 7 | -------------------------------------------------------------------------------- /samples/docs/common.hxml: -------------------------------------------------------------------------------- 1 | -L react-next 2 | -L react-css 3 | -L css-types 4 | -L classnames 5 | -L datetime 6 | -L markdown 7 | -L event-types 8 | -L html-entities 9 | -L tink_domspec 10 | -L hxnodejs 11 | -L yaml 12 | 13 | -cp src 14 | 15 | # -dce full 16 | -D js-es=6 17 | # -D analyzer-optimize 18 | -D message.reporting=pretty 19 | 20 | -w -WDeprecatedEnumAbstract 21 | 22 | # Configure react-css 23 | -D react.css.out=bin/styles.css 24 | -D react.css.base=res/base.css 25 | # -D react.css.sourcemap=styles.css.map 26 | 27 | # Configure react-next 28 | -D react-wrap-strict 29 | -D react-check-jsxstatic-type 30 | -D react-disable-dynamic-components 31 | -------------------------------------------------------------------------------- /samples/docs/install.hxml: -------------------------------------------------------------------------------- 1 | -lib HtmlParser:3.3.2 2 | -lib js-object:0.0.7 3 | -lib datetime:3.1.4 4 | -lib html-entities:1.0.0 5 | 6 | -lib tink_macro:1.0.1 7 | -lib tink_parse:0.4.1 8 | -lib tink_core:2.1.0 9 | -lib tink_domspec:0.5.0 10 | -lib tink_svgspec:0.0.1 11 | -lib tink_anon:git:git@github.com:haxetink/tink_anon.git#0277e6e 12 | -lib tink_hxx:git:git@github.com:kLabz/tink_hxx.git#5a01af1 13 | 14 | -lib hxnodejs:git:git@github.com:HaxeFoundation/hxnodejs.git#d2d871c 15 | -lib css-types:git:git@github.com:kLabz/haxe-css-types.git#5f5d2c6 16 | -lib event-types:git:git@github.com:kLabz/event-types.git#df271a9 17 | -lib react-css:git:git@github.com:kLabz/haxe-react-css.git#5595611 18 | -lib classnames:git:git@github.com:kLabz/haxe-classnames.git#30e7436 19 | -lib yaml:git:git@github.com:kLabz/hx-yaml.git#c61ca1f 20 | -lib markdown:git:git@github.com:kLabz/haxe-markdown.git#8503130 21 | -------------------------------------------------------------------------------- /samples/docs/makefile: -------------------------------------------------------------------------------- 1 | .SILENT: 2 | 3 | setup: 4 | haxelib newrepo 5 | haxelib dev react-next ../.. 6 | haxelib install --skip-dependencies --always install.hxml 7 | npm i 8 | 9 | server: 10 | haxe build.hxml 11 | 12 | start-server: 13 | node bin/server.js 14 | 15 | static: 16 | haxe build-static.hxml 17 | node bin/static-gen.js 18 | 19 | serve-static: 20 | cd bin/static && python -m http.server 8043 21 | -------------------------------------------------------------------------------- /samples/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jsdom": "19.0.0", 4 | "react": "18.2.0", 5 | "react-dom": "18.2.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/docs/res/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Custom colors */ 3 | --orange: #f37321; 4 | --orange-light: #f3c29b; 5 | --orange-dark: #c74f03; 6 | --yellow: #f8b218; 7 | --bg: #111111; 8 | --fg: #dedbd4; 9 | - --dull: #f9edd4; 10 | --dark: #2d2b28; 11 | --darker: #1e1d1c; 12 | --deep: #736a5d; 13 | --highlight: #ffffff; 14 | 15 | --font-color: var(--fg); 16 | --bg-color: var(--bg); 17 | 18 | --link-color: var(--orange); 19 | --link-state-color: var(--yellow); 20 | --link-state-border-color: var(--yellow); 21 | 22 | --thead-bg-color: var(--dark); 23 | --table-border-color: var(--dark); 24 | 25 | --pre-color: var(--fg); 26 | --pre-bg-color: #1a1a1a; 27 | 28 | /* --bq-color: var(--deep); */ 29 | --hr-color: var(--dark); 30 | 31 | --post-info-color: var(--deep); 32 | --pagination-bg-color: var(--orange-dark); 33 | --pagination-link-color: var(--bg); 34 | 35 | --mainBorderWidth: .625rem; 36 | --borderPosition: 3em; 37 | --footerHeight: calc(4.5rem + var(--mainBorderWidth)); 38 | --footerWidth: calc(3.5rem + var(--mainBorderWidth)); 39 | } 40 | 41 | ::selection { 42 | background: var(--yellow); 43 | color: var(--bg); 44 | -webkit-text-fill-color: var(--bg); 45 | } 46 | 47 | ::-moz-selection { 48 | background: var(--yellow); 49 | color: var(--bg); 50 | } 51 | 52 | @keyframes errorAnimation_bgcolor { 53 | 0% { 54 | background-color: var(--highlight); 55 | } 56 | 57 | 15% { 58 | background-color: var(--orange-dark); 59 | } 60 | 61 | 35% { 62 | background-color: var(--orange-dark); 63 | } 64 | 65 | 80% { 66 | background-color: var(--highlight); 67 | } 68 | 69 | 100% { 70 | background-color: var(--highlight); 71 | } 72 | } 73 | 74 | @keyframes errorAnimation_bordercolor { 75 | 0% { 76 | border-left-color: var(--highlight); 77 | } 78 | 79 | 15% { 80 | border-left-color: var(--orange-dark); 81 | } 82 | 83 | 35% { 84 | border-left-color: var(--orange-dark); 85 | } 86 | 87 | 80% { 88 | border-left-color: var(--highlight); 89 | } 90 | 91 | 100% { 92 | border-left-color: var(--highlight); 93 | } 94 | } 95 | 96 | html { 97 | font-family: "DejaVu Sans", Helvetica, Arial, sans-serif; 98 | -ms-text-size-adjust: 100%; 99 | -webkit-text-size-adjust: 100%; 100 | height: 100vh; 101 | width: 100vw; 102 | color: var(--fg); 103 | overflow: hidden; 104 | } 105 | 106 | body { 107 | position: relative; 108 | height: 100vh; 109 | width: 100vw; 110 | padding: 0; 111 | margin: 0; 112 | font-size: 20px; 113 | line-height: 1.5; 114 | color: var(--font-color); 115 | background: var(--highlight); 116 | /* background: var(--bg-color); */ 117 | overflow: hidden; 118 | -webkit-font-smoothing: antialiased; 119 | } 120 | 121 | body.error, 122 | body.error .static-border-top, 123 | body.error .static-border-bottom { 124 | animation: errorAnimation_bgcolor 2s linear infinite; 125 | } 126 | 127 | body.error .static-border-bottom > footer::before { 128 | animation: errorAnimation_bordercolor 2s linear infinite; 129 | } 130 | 131 | article, 132 | aside, 133 | details, 134 | figcaption, 135 | footer, 136 | header, 137 | hgroup, 138 | main, 139 | menu, 140 | nav, 141 | section, 142 | summary { 143 | display: block; 144 | } 145 | 146 | figure { 147 | display: flex; 148 | flex-direction: column; 149 | } 150 | 151 | figure > figcaption { 152 | order: -1; 153 | } 154 | 155 | summary { 156 | cursor: pointer; 157 | background-color: transparent; 158 | } 159 | 160 | summary:hover { 161 | background-color: var(--darker); 162 | } 163 | 164 | article img, article video { 165 | max-width: 100%; 166 | display: block; 167 | height: auto; 168 | margin: 0 auto .5em; 169 | } 170 | 171 | /* Headings */ 172 | 173 | h1, h2, h3, h4, h5, h6, 174 | article header h1 a { 175 | color: var(--yellow); 176 | } 177 | 178 | h1 { 179 | font-size: 1.35em; 180 | } 181 | 182 | h2 { 183 | font-size: 1.2em; 184 | } 185 | 186 | h3 { 187 | font-size: 1.1em; 188 | } 189 | 190 | h1, main h1, article header h1 { 191 | font-size: 1.6em; 192 | font-weight: bold; 193 | line-height: 1.25em; 194 | width: 100%; 195 | margin: 30px 0; 196 | padding: 0; 197 | } 198 | 199 | /* Links */ 200 | 201 | a { 202 | color: var(--link-color); 203 | text-decoration: none; 204 | } 205 | 206 | a:hover, 207 | a:focus, 208 | a:active { 209 | color: var(--link-state-color); 210 | border-bottom: 1px solid var(--link-state-border-color); 211 | } 212 | 213 | a:active, 214 | a:hover { 215 | outline: 0; 216 | } 217 | 218 | a:active { 219 | opacity: 0.9; 220 | } 221 | 222 | /* Lists */ 223 | 224 | ul { 225 | list-style: none; 226 | } 227 | 228 | ul li::before { 229 | content: "\2022"; 230 | color: var(--yellow); 231 | font-weight: bold; 232 | display: inline-block; 233 | width: 1em; 234 | margin-left: -1em; 235 | } 236 | 237 | ol { 238 | list-style: none; 239 | counter-reset: li; 240 | } 241 | 242 | ol li { 243 | counter-increment: li; 244 | } 245 | 246 | ol li::before { 247 | content: counter(li) "."; 248 | color: var(--yellow); 249 | display: inline-block; 250 | width: 1.25em; 251 | margin-left: -1.25em; 252 | } 253 | 254 | li > p:first-child { 255 | margin-top: 0; 256 | display: inline; 257 | margin-left: -0.25em; 258 | } 259 | 260 | /* Table */ 261 | 262 | thead { 263 | background: var(--thead-bg-color); 264 | } 265 | 266 | .table-wrapper { 267 | overflow-x: auto; 268 | } 269 | 270 | table { 271 | max-width: 100%; 272 | border-spacing: 0; 273 | } 274 | 275 | th, td { 276 | padding: 0.5em 1em; 277 | border: 1px double var(--table-border-color); 278 | } 279 | 280 | /* Code */ 281 | 282 | code { 283 | white-space: nowrap; 284 | font-size: 0.8em; 285 | padding: .25em .5em; 286 | color: var(--orange-light); 287 | background-color: var(--pre-bg-color); 288 | } 289 | 290 | pre { 291 | position: relative; 292 | padding: 1em; 293 | font-size: 0.9rem; 294 | max-width: 100%; 295 | overflow: auto; 296 | color: var(--pre-color); 297 | background-color: var(--pre-bg-color); 298 | scrollbar-color: var(--orange) var(--dark); 299 | } 300 | 301 | code, pre, kbd { 302 | font-family: "DejaVu Sans Mono", monospace, monospace; 303 | line-height: 154%; 304 | tab-size: 4; 305 | -moz-tab-size: 4; 306 | } 307 | 308 | .highlight { 309 | position: relative; 310 | } 311 | 312 | pre > code::before { 313 | display: block; 314 | position: absolute; 315 | left: 0; 316 | right: 0; 317 | top: 0; 318 | content: attr(data-lang); 319 | background: var(--fg); 320 | color: var(--bg); 321 | line-height: 1.5em; 322 | padding: 0 0.25em; 323 | } 324 | 325 | pre > code { 326 | white-space: pre; 327 | font-size: inherit; 328 | color: inherit; 329 | background: none; 330 | padding: 0; 331 | } 332 | 333 | pre.chroma { 334 | /* position: relative; */ 335 | margin-bottom: 0; 336 | } 337 | 338 | .chroma pre { 339 | padding-left: 0; 340 | padding-right: 0; 341 | } 342 | 343 | .chroma td.lntd:last-child { width: 100%; } 344 | 345 | .highlight > .chroma { 346 | border-bottom: 4px solid var(--fg); 347 | } 348 | 349 | /* Note: see chroma.css for syntax highlighting */ 350 | 351 | blockquote { 352 | border-left: 4px solid var(--fg); 353 | background: var(--dark); 354 | padding: 0.1em 1em; 355 | margin-left: 0.75em; 356 | margin-right: 1em; 357 | } 358 | 359 | p { 360 | margin-top: 0.5em; 361 | margin-bottom: 0.5em; 362 | } 363 | 364 | em, i, strong, b { 365 | color: var(--highlight); 366 | } 367 | 368 | strike { 369 | color: var(--deep); 370 | } 371 | 372 | mark { 373 | color: var(--yellow); 374 | background-color: transparent; 375 | /* font-weight: bold; */ 376 | } 377 | 378 | hr { 379 | color: var(--hr-color); 380 | background-color: var(--hr-color); 381 | border: none; 382 | height: 1px; 383 | } 384 | 385 | @media (max-width: 1079px) { 386 | :root { 387 | --borderPosition: 6px; 388 | } 389 | 390 | body { 391 | font-size: 18px; 392 | } 393 | 394 | code, pre, kbd { 395 | tab-size: 2; 396 | -moz-tab-size: 2; 397 | } 398 | } 399 | 400 | @media (max-width: 799px) { 401 | :root { 402 | --mainBorderWidth: 6px; 403 | } 404 | } 405 | 406 | .disabled { 407 | visibility: hidden; 408 | } 409 | 410 | .hidden { 411 | display: none!important; 412 | } 413 | -------------------------------------------------------------------------------- /samples/docs/src/AppContext.hx: -------------------------------------------------------------------------------- 1 | import react.React; 2 | import react.ReactComponent; 3 | import react.ReactContext; 4 | import react.ReactMacro.jsx; 5 | import react.ReactType; 6 | 7 | typedef AppContextData = { 8 | var staticSite:Bool; 9 | } 10 | 11 | typedef AppContextProviderProps = { 12 | var value:AppContextData; 13 | } 14 | 15 | typedef AppContextConsumerProps = { 16 | var children:AppContextData->ReactFragment; 17 | } 18 | 19 | class AppContext { 20 | public static var Context(get, null):ReactContext; 21 | public static var Provider(get, null):ReactTypeOf; 22 | public static var Consumer(get, null):ReactTypeOf; 23 | 24 | static function get_Context() {ensureReady(); return Context;} 25 | static function get_Provider() {ensureReady(); return Provider;} 26 | static function get_Consumer() {ensureReady(); return Consumer;} 27 | 28 | static function ensureReady() @:bypassAccessor { 29 | if (Context == null) { 30 | Context = React.createContext(); 31 | Context.displayName = "AppContext"; 32 | Consumer = Context.Consumer; 33 | Provider = Context.Provider; 34 | } 35 | } 36 | 37 | public static function wrap(Comp:ReactType):ReactType { 38 | return function (props:{}) { 39 | return jsx( 40 | 41 | {value -> 42 | 43 | } 44 | ); 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/docs/src/Main.hx: -------------------------------------------------------------------------------- 1 | import haxe.Json; 2 | import haxe.http.HttpStatus; 3 | import haxe.io.Mime; 4 | import haxe.io.Path; 5 | import js.node.Fs; 6 | import js.node.Http; 7 | import js.node.http.IncomingMessage; 8 | import js.node.http.ServerResponse; 9 | 10 | import react.ReactDOMServer; 11 | import react.ReactMacro.jsx; 12 | 13 | import comp.App; 14 | 15 | using StringTools; 16 | 17 | class Main { 18 | static inline var PORT = 8042; 19 | static inline var HOST = '0.0.0.0'; 20 | 21 | static function main() { 22 | final app = Http.createServer({}, handler); 23 | app.listen(PORT, HOST, () -> { 24 | Sys.println('Server ready, waiting on $HOST:$PORT'); 25 | }); 26 | } 27 | 28 | static function handler(req:IncomingMessage, res:ServerResponse):Void { 29 | function sendFile(path:String) { 30 | return Fs.readFile(path, function(err, data) { 31 | if (err != null) { 32 | res.writeHead(HttpStatus.NotFound); 33 | return res.end(Json.stringify(err)); 34 | } 35 | 36 | var mimeType:Null = switch Path.extension(path) { 37 | case 'css': TextCss; 38 | case 'png': ImagePng; 39 | case _: null; // TODO: handle more needed extensions 40 | } 41 | 42 | if (mimeType != null) res.setHeader("Content-Type", mimeType); 43 | res.writeHead(HttpStatus.OK); 44 | return res.end(data); 45 | }); 46 | } 47 | 48 | if (req.url.endsWith(".css") || req.url.endsWith(".png")) { 49 | var path = Path.join(["bin", req.url.substr(1)]); 50 | sendFile(path); 51 | } else { 52 | var chapter = req.url == "/" ? null : req.url; 53 | var stream = ReactDOMServer.renderToStaticNodeStream(jsx()); 54 | stream.pipe(res); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /samples/docs/src/StaticGenerator.hx: -------------------------------------------------------------------------------- 1 | import haxe.io.Path; 2 | import js.lib.Promise; 3 | import js.node.Fs.Fs; 4 | import sys.FileSystem; 5 | import sys.io.File; 6 | 7 | import data.DocChapter; 8 | import react.ReactDOMServer; 9 | import react.ReactMacro.jsx; 10 | 11 | import comp.App; 12 | 13 | class StaticGenerator { 14 | static inline var OUT = "bin/static"; 15 | 16 | static function main() { 17 | FileSystem.createDirectory(OUT); 18 | for (f in FileSystem.readDirectory(OUT)) FileSystem.deleteFile(Path.join([OUT, f])); 19 | 20 | loadChapters().then(chapters -> { 21 | chapters.unshift(loadReadme()); 22 | Promise.all(chapters.map(renderChapter)).then(_ -> { 23 | File.copy('bin/styles.css', '$OUT/styles.css'); 24 | Sys.println('Generated html and css files in $OUT'); 25 | }); 26 | }); 27 | } 28 | 29 | static function renderChapter(chapter:DocChapter):Promise { 30 | return new Promise((resolve, reject) -> { 31 | var slug = chapter.slug == "/" ? null : chapter.slug; 32 | var path = '$OUT${slug ?? "/index"}.html'; 33 | var fileWriter = Fs.createWriteStream(path); 34 | var stream = ReactDOMServer.renderToStaticNodeStream(jsx()); 35 | stream.on('end', resolve); 36 | stream.on('error', resolve); 37 | stream.pipe(fileWriter); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/docs/src/comp/App.hx: -------------------------------------------------------------------------------- 1 | package comp; 2 | 3 | import comp.layout.*; 4 | import data.DocChapter; 5 | 6 | private typedef PublicProps = { 7 | var chapter:Null; 8 | @:optional var staticSite:Bool; 9 | } 10 | 11 | private typedef Props = { 12 | > PublicProps, 13 | var home:DocChapter; 14 | var chapters:Array; 15 | } 16 | 17 | @:css 18 | @:publicProps(PublicProps) 19 | @:wrap(App.wrap) 20 | class App extends ReactComponent { 21 | static var styles:Stylesheet = { 22 | '_': { 23 | height: '100vh', 24 | width: '100vw', 25 | maxWidth: Unset, 26 | padding: Var('mainBorderWidth'), 27 | overflowY: Scroll, 28 | overflowX: Hidden, 29 | background: Var('bg'), 30 | backgroundClip: 'content-box', 31 | 'scrollbar-color': 'var(--orange) var(--highlight)', 32 | boxSizing: 'border-box', 33 | }, 34 | '_::-webkit-scrollbar': { 35 | width: Var('mainBorderWidth'), 36 | backgroundColor: 'transparent', 37 | }, 38 | '_::-webkit-scrollbar-track': { 39 | display: 'none', 40 | borderRadius: 0, 41 | '-webkit-box-shadow': 'none' 42 | }, 43 | '_::-webkit-scrollbar-thumb': { 44 | borderRadius: 0, 45 | border: 'var(--mainBorderWidth) solid var(--orange)', 46 | borderLeft: 'none', 47 | borderTopColor: 'transparent', 48 | borderBottomColor: 'transparent', 49 | '-webkit-box-shadow': 'none' 50 | } 51 | }; 52 | 53 | static function wrap(Comp) { 54 | return function(props:Props) { 55 | var LazyComp = React.lazy(() -> 56 | loadChapters() 57 | .then(chapters -> (_) -> jsx( 58 | 63 | )) 64 | .then(type -> React.createModule(type)) 65 | ); 66 | 67 | return jsx(); 68 | }; 69 | } 70 | 71 | override function render():ReactFragment { 72 | return jsx( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Haxe react-next docs 81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 | 105 | 106 | ); 107 | } 108 | 109 | function extractChapter():Null { 110 | if (props.chapter == null) return props.home; 111 | return Lambda.find(props.chapters, f -> f.slug == props.chapter); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /samples/docs/src/comp/Article.hx: -------------------------------------------------------------------------------- 1 | package comp; 2 | 3 | import data.DocChapter; 4 | 5 | private typedef Props = { 6 | var chapter:Null; 7 | } 8 | 9 | class Article extends ReactComponent { 10 | override function render():ReactFragment { 11 | if (props.chapter == null) return "404"; 12 | 13 | // TODO: use proper custom html renderer, handle internal/external links 14 | return jsx( 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/docs/src/comp/SidePanel.hx: -------------------------------------------------------------------------------- 1 | package comp; 2 | 3 | import AppContext; 4 | import data.DocChapter; 5 | 6 | private typedef PublicProps = { 7 | var chapter:Null; 8 | var chapters:Array; 9 | } 10 | 11 | private typedef Props = { 12 | > PublicProps, 13 | > AppContextData, 14 | } 15 | 16 | @:css 17 | @:publicProps(PublicProps) 18 | @:wrap(AppContext.wrap) 19 | class SidePanel extends ReactComponent { 20 | static var styles:Stylesheet = { 21 | '_': { 22 | position: Fixed, 23 | top: '4em', 24 | left: '2em', 25 | width: '17em', 26 | }, 27 | '_ a': { 28 | lineHeight: 1.5, 29 | textDecoration: "none", 30 | textTransform: UpperCase, 31 | color: Var("fg"), 32 | }, 33 | '_ a.active': { 34 | color: Var("orange") 35 | }, 36 | '_ a:hover': { 37 | textDecoration: "none", 38 | borderBottom: "none", 39 | color: Var("yellow") 40 | }, 41 | '_ > a': { 42 | display: "block", 43 | fontSize: "2em", 44 | lineHeight: 1, 45 | color: Var("fg"), 46 | marginTop: "1rem" 47 | }, 48 | '_ > a::before': { 49 | content: '"/"', 50 | display: "none" 51 | }, 52 | '_ > a:hover::before, _ > a.active::before': { 53 | display: "inline" 54 | }, 55 | '_ > a.active': { 56 | color: Var("yellow") 57 | }, 58 | } 59 | 60 | static var mediaQueries:Dynamic = { 61 | // TODO: toggle 62 | 'max-width: 1079px': { 63 | '_': { 64 | display: 'none' 65 | } 66 | }, 67 | 'min-width: 1400px': { 68 | '_': { 69 | width: '20em', 70 | left: '3.5em' 71 | } 72 | } 73 | }; 74 | 75 | override function render():ReactFragment { 76 | var ext = props.staticSite ? '.html' : ''; 77 | return jsx( 78 |
79 | Home 80 | 81 | Docs 82 |
83 | 84 | 89 | 90 |
91 |
92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /samples/docs/src/comp/html/ExternalLink.hx: -------------------------------------------------------------------------------- 1 | package comp.html; 2 | 3 | private typedef Props = { 4 | var href:String; 5 | var children:ReactFragment; 6 | } 7 | 8 | class ExternalLink extends ReactComponent { 9 | override function render():ReactFragment { 10 | return jsx( 11 | 12 | {props.children} 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/docs/src/comp/import.hx: -------------------------------------------------------------------------------- 1 | import js.Browser; 2 | import js.Browser.console; 3 | import js.html.DOMElement; 4 | 5 | import css.GlobalValue; 6 | import css.GlobalValue.Important; 7 | import css.GlobalValue.Var; 8 | import react.React; 9 | import react.ReactComponent; 10 | import react.ReactRef; 11 | import react.ReactType; 12 | import react.css.Stylesheet; 13 | 14 | #if !macro 15 | import react.ReactMacro.jsx; 16 | import classnames.ClassNames.fastNull as classNames; 17 | #end 18 | 19 | // using reader.util.NullHelper; 20 | -------------------------------------------------------------------------------- /samples/docs/src/comp/layout/MainContent.hx: -------------------------------------------------------------------------------- 1 | package comp.layout; 2 | 3 | private typedef Props = { 4 | var children:ReactFragment; 5 | } 6 | 7 | @:css 8 | class MainContent extends ReactComponent { 9 | static var styles:Stylesheet = { 10 | '_': { 11 | position: Relative, 12 | maxWidth: '60em', 13 | minHeight: 'calc(100vh - 2 * var(--mainBorderWidth))', 14 | margin: [0, 'auto'], 15 | paddingTop: Var('mainBorderWidth'), 16 | paddingBottom: 'calc(var(--mainBorderWidth) + 2em)', 17 | paddingRight: 'calc(var(--borderPosition) + 1em)', 18 | paddingLeft: 'calc(var(--borderPosition) + 1em)', 19 | boxSizing: 'border-box', 20 | outline: 'none' 21 | }, 22 | '_::before, _::after': { 23 | content: '""', 24 | display: 'block', 25 | position: Absolute, 26 | top: 0, 27 | bottom: 0, 28 | width: 1, 29 | background: Var('dark'), 30 | zIndex: 0 31 | }, 32 | '_::before': { 33 | left: Var('borderPosition') 34 | }, 35 | '_::after': { 36 | right: Var('borderPosition') 37 | }, 38 | '_:focus': { 39 | outline: 'none' 40 | }, 41 | '_:focus::before, _:focus::after': { 42 | background: Var('yellow') 43 | } 44 | }; 45 | 46 | override function render():ReactFragment { 47 | return jsx(
{props.children}
); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /samples/docs/src/comp/layout/MainContentContainer.hx: -------------------------------------------------------------------------------- 1 | package comp.layout; 2 | 3 | private typedef Props = { 4 | var children:ReactFragment; 5 | } 6 | 7 | class MainContentContainer extends ReactComponent { 8 | override function render():ReactFragment { 9 | return jsx(
{props.children}
); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/docs/src/comp/layout/MainWrapper.hx: -------------------------------------------------------------------------------- 1 | package comp.layout; 2 | 3 | private typedef Props = { 4 | var children:ReactFragment; 5 | } 6 | 7 | @:css 8 | class MainWrapper extends ReactComponent { 9 | static var styles:Stylesheet = { 10 | '_': { 11 | paddingLeft: '19em', 12 | marginRight: 'calc(-1 * var(--mainBorderWidth))', 13 | }, 14 | '.fullscreen > _': { 15 | paddingLeft: 0 16 | } 17 | }; 18 | 19 | static var mediaQueries:Dynamic = { 20 | 'max-width: 1079px': { 21 | '_': { 22 | paddingLeft: 0, 23 | paddingRight: 0 24 | } 25 | }, 26 | 'min-width: 1400px': { 27 | '_': { 28 | paddingLeft: "24em", 29 | } 30 | } 31 | } 32 | 33 | override function render():ReactFragment { 34 | return jsx(
{props.children}
); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/docs/src/comp/layout/StaticBorderBottom.hx: -------------------------------------------------------------------------------- 1 | package comp.layout; 2 | 3 | import comp.html.ExternalLink; 4 | 5 | @:css 6 | class StaticBorderBottom extends ReactComponent { 7 | static var styles:Stylesheet = { 8 | '_': { 9 | position: Fixed, 10 | left: 0, 11 | bottom: 0, 12 | width: '100vw', 13 | height: Var('mainBorderWidth'), 14 | padding: '0 var(--mainBorderWidth)', // TODO: fix this in react-css 15 | background: Var('highlight'), 16 | backgroundClip: 'content-box', // TODO: enum abstract in css-types 17 | boxSizing: 'border-box', // TODO: enum abstract in css-types 18 | zIndex: 100 19 | }, 20 | '_ > footer': { 21 | position: Absolute, 22 | left: 0, 23 | bottom: 0, 24 | paddingLeft: Var('footerWidth') 25 | }, 26 | '_ > footer::before': { 27 | content: '""', 28 | display: 'block', 29 | position: Absolute, 30 | left: 0, 31 | bottom: 0, 32 | height: 0, 33 | width: 0, 34 | borderLeft: 'var(--footerWidth) solid var(--highlight)', 35 | borderTop: 'var(--footerHeight) solid transparent' 36 | }, 37 | '_ > footer > a': { 38 | position: Static 39 | }, 40 | '_ > footer svg': { 41 | position: Absolute, 42 | left: 'calc(var(--mainBorderWidth) + .25rem)', // TODO: calc helper 43 | bottom: Var('mainBorderWidth'), 44 | height: '1.5rem', 45 | width: '1.5rem', 46 | cursor: 'pointer' 47 | }, 48 | '_ > footer svg path': { 49 | 'fill': Var('bg') 50 | }, 51 | '_ > footer svg:hover path': { 52 | 'fill': Var('orange') 53 | } 54 | }; 55 | 56 | static var mediaQueries:Dynamic = { 57 | 'max-width: 1079px': { 58 | '_ > footer svg': { 59 | height: '2.5rem', 60 | width: '1.5rem', 61 | left: 'calc(var(--mainBorderWidth) + .125rem)' 62 | } 63 | } 64 | } 65 | 66 | override function render():ReactFragment { 67 | return jsx( 68 |
69 |
70 | 71 | 72 | 77 | 78 | 79 |
80 |
81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /samples/docs/src/comp/layout/StaticBorderTop.hx: -------------------------------------------------------------------------------- 1 | package comp.layout; 2 | 3 | @:css 4 | class StaticBorderTop extends ReactComponent { 5 | static var styles:Stylesheet = { 6 | '_': { 7 | position: Fixed, 8 | left: 0, 9 | top: 0, 10 | width: '100vw', 11 | height: Var('mainBorderWidth'), 12 | padding: '0 var(--mainBorderWidth)', // TODO: fix this in react-css 13 | background: Var('highlight'), 14 | backgroundClip: 'content-box', // TODO: enum abstract in css-types 15 | boxSizing: 'border-box', // TODO: enum abstract in css-types 16 | zIndex: 100 17 | }, 18 | }; 19 | 20 | override function render():ReactFragment { 21 | return jsx(
); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/docs/src/data/DocChapter.hx: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import js.lib.Promise; 4 | import js.node.Fs.Fs; 5 | import sys.FileSystem; 6 | import sys.io.File; 7 | 8 | import markdown.AST; 9 | import yaml.Yaml; 10 | import yaml.util.ObjectMap; 11 | 12 | using StringTools; 13 | using haxe.io.Path; 14 | 15 | typedef DocChapter = { 16 | var slug:String; 17 | var order:Null; 18 | var title:Null; 19 | var parse:Void->String; 20 | } 21 | 22 | function sortChapters(chapters:Array):Array { 23 | chapters.sort((c1, c2) -> { 24 | if (c1.order == null && c2.order == null) 25 | return c1.title > c2.title ? 1 : -1; 26 | 27 | if (c1.order == null) return 1; 28 | if (c2.order == null) return -1; 29 | return c1.order - c2.order; 30 | }); 31 | 32 | return chapters; 33 | } 34 | 35 | function loadChapters():Promise> { 36 | return new Promise((resolve, reject) -> { 37 | Fs.readdir("../../doc", (err, files) -> { 38 | var chapters = (files ?? []) 39 | .map(f -> loadChapter(Path.join(["..", "..", "doc", f]))) 40 | .filter(f -> f != null); 41 | 42 | resolve(sortChapters(chapters)); 43 | }); 44 | }); 45 | } 46 | 47 | function loadReadme():Null { 48 | var ret = loadChapter(Path.join(["..", "..", "README.md"])); 49 | if (ret != null) ret.slug = '/'; 50 | return ret; 51 | } 52 | 53 | function loadChapter(path:String):Null { 54 | if (!FileSystem.exists(path)) return null; 55 | if (!path.endsWith(".md")) return null; 56 | 57 | var file = path.withoutDirectory(); 58 | var slug = file.withoutExtension(); 59 | 60 | var order:Null = null; 61 | var md:Null> = null; 62 | var title:Null = null; 63 | var content = File.getContent(path); 64 | 65 | if (content.startsWith("---\n")) { 66 | var yamlLines = []; 67 | var lines = content.split("\n"); 68 | lines.shift(); // Remove first "---" line 69 | 70 | var line = lines.shift(); 71 | while (line != null) { 72 | if (line == "---") break; 73 | yamlLines.push(line); 74 | line = lines.shift(); 75 | } 76 | 77 | if (lines.length > 0 && lines[0].trim() == "") lines.shift(); 78 | content = lines.join("\n"); 79 | 80 | var yaml:ObjectMap = Yaml.parse(yamlLines.join("\n")); 81 | for (k in yaml.keys()) { 82 | var strVal:String = yaml.get(k); 83 | switch (k) { 84 | case "title": title = strVal; 85 | case "order": order = Std.parseInt(strVal); 86 | case _: 87 | } 88 | } 89 | } 90 | 91 | // Get title from markdown if needed 92 | if (title == null) { 93 | md = Markdown.markdownToAst(content); 94 | 95 | for (node in md) { 96 | if (node is ElementNode) { 97 | var el:ElementNode = cast node; 98 | if (el.tag == "h1" || el.tag == "h2") { 99 | var htmlTitle = Markdown.renderHtml(el.children); 100 | // TODO: sanitize title 101 | title = htmlTitle; 102 | break; 103 | } 104 | } 105 | } 106 | } 107 | 108 | return { 109 | slug: '/' + slug, 110 | order: order, 111 | title: title, 112 | parse: () -> Markdown.renderHtml(md ?? Markdown.markdownToAst(content)) 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /samples/todoapp/bin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo React 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/todoapp/bin/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 12px Arial; 3 | } 4 | 5 | ul, li { 6 | list-style: none; 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | .app { 12 | width: 250px; 13 | } 14 | 15 | .header { 16 | display: flex; 17 | } 18 | .header > input { 19 | flex: 1; 20 | margin-right: 4px; 21 | } 22 | 23 | .list > li { 24 | display: block; 25 | padding: 4px; 26 | margin-top: 2px; 27 | border: solid 1px #ccc; 28 | background: #eee; 29 | user-select: none; 30 | -webkit-user-select: none; 31 | } 32 | .list > li.checked { 33 | background: #3a3; 34 | color: #fff; 35 | } 36 | 37 | .footer { 38 | margin-top: 4px; 39 | color: #666; 40 | } -------------------------------------------------------------------------------- /samples/todoapp/build.hxml: -------------------------------------------------------------------------------- 1 | -js bin/index.js 2 | -cp src 3 | -main Main 4 | -lib react-next 5 | -lib msignal 6 | -D react_global 7 | #-D react_no_inline 8 | -debug 9 | -dce full 10 | -------------------------------------------------------------------------------- /samples/todoapp/install.hxml: -------------------------------------------------------------------------------- 1 | -lib HtmlParser:3.3.2 2 | -lib js-object:0.0.7 3 | -lib html-entities:1.0.0 4 | -lib msignal:1.2.5 5 | 6 | -lib tink_macro:1.0.1 7 | -lib tink_parse:0.4.1 8 | -lib tink_core:2.1.0 9 | -lib tink_domspec:0.5.0 10 | -lib tink_svgspec:0.0.1 11 | -lib tink_anon:git:git@github.com:haxetink/tink_anon.git#0277e6e 12 | -lib tink_hxx:git:git@github.com:kLabz/tink_hxx.git#5a01af1 13 | -------------------------------------------------------------------------------- /samples/todoapp/readme.md: -------------------------------------------------------------------------------- 1 | # Haxe React sample: Todo 2 | 3 | The classic Todo app done using Haxe React and a Flux-like store. 4 | 5 | ## Building 6 | 7 | haxelib install msignal 8 | haxelib install react 9 | haxe build.hxml 10 | 11 | ## Haxe React 12 | 13 | We believe Haxe+React is an excellent combination; here's the render function of the main view: 14 | 15 | ```haxe 16 | override public function render() 17 | { 18 | var unchecked = state.items.filter(function(item) return !item.checked).length; 19 | 20 | return jsx(' 21 |
22 |
23 | 24 | 25 |
26 | <$TodoList data=${state.items}/> 27 |
$unchecked task(s) left
28 |
29 | '); 30 | } 31 | ``` 32 | 33 | The JSX transformation is implemented as a `jsx()` macro, producing code which will be verified 34 | by the Haxe compiler. You can use both classic `{}` syntax or Haxe string interpolation for improved 35 | IDE integration. 36 | 37 | Note: you don't have to add `$` in `<$TodoList>`, it's just for code navigation convenience. 38 | 39 | And as you can see, the whole React API is strongly typed and using `override` you can easily, 40 | and safely, implement the common lifecycle methods (`componentDidMount`, `shouldComponentUpdate`...). 41 | 42 | ## Flux-like store 43 | 44 | This sample includes a very simple, no dependencies, flux-like store, implemented using `msignal` 45 | for the eventing: 46 | 47 | - `TodoActions` defines static strongly typed signals for each action destinated to the store, 48 | - `TodoStore` instance listens to the actions and dispatches a `changed` signal to notify listeners. 49 | -------------------------------------------------------------------------------- /samples/todoapp/src/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import react.ReactDOM; 4 | import react.ReactMacro.jsx; 5 | import js.Browser; 6 | import view.TodoApp; 7 | 8 | class Main 9 | { 10 | public static function main() 11 | { 12 | var root = ReactDOMClient.createRoot(Browser.document.getElementById('app')); 13 | root.render(jsx('<$TodoApp/>')); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/todoapp/src/store/TodoActions.hx: -------------------------------------------------------------------------------- 1 | package store; 2 | 3 | import msignal.Signal.Signal1; 4 | 5 | class TodoActions 6 | { 7 | static public var addItem:Signal1 = new Signal1(); 8 | static public var toggleItem:Signal1 = new Signal1(); 9 | } -------------------------------------------------------------------------------- /samples/todoapp/src/store/TodoItem.hx: -------------------------------------------------------------------------------- 1 | package store; 2 | 3 | typedef TodoItem = { 4 | id:String, 5 | label:String, 6 | ?checked:Bool 7 | } 8 | -------------------------------------------------------------------------------- /samples/todoapp/src/store/TodoStore.hx: -------------------------------------------------------------------------------- 1 | package store; 2 | 3 | import haxe.Json; 4 | import js.Browser; 5 | import msignal.Signal; 6 | 7 | class TodoStore 8 | { 9 | public var changed(default, null):Signal0; 10 | 11 | public var list(default, null):Array; 12 | 13 | var lastId:Int; 14 | 15 | public function new() 16 | { 17 | changed = new Signal0(); 18 | 19 | loadItems(); 20 | 21 | TodoActions.toggleItem.add(toggleItem); 22 | TodoActions.addItem.add(addItem); 23 | } 24 | 25 | function toggleItem(id:String) 26 | { 27 | list = list.map(function(item) { 28 | if (item.id == id) { 29 | item.checked = !item.checked; 30 | } 31 | return item; 32 | }); 33 | 34 | validate(); 35 | } 36 | 37 | function addItem(label:String) 38 | { 39 | list = [{ 40 | id:'${++lastId}', 41 | label:label, 42 | checked:false 43 | }].concat(list); 44 | 45 | validate(); 46 | } 47 | 48 | function validate() 49 | { 50 | saveItems(); 51 | changed.dispatch(); 52 | } 53 | 54 | function saveItems() 55 | { 56 | var data = Json.stringify(list); 57 | Browser.window.localStorage.setItem('todos', data); 58 | } 59 | 60 | function loadItems() 61 | { 62 | var data = Browser.window.localStorage.getItem('todos'); 63 | if (data != null) list = Json.parse(data); 64 | else list = []; 65 | lastId = list.length; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /samples/todoapp/src/view/TodoApp.hx: -------------------------------------------------------------------------------- 1 | package view; 2 | 3 | import react.ReactRef; 4 | import react.React; 5 | import react.ReactComponent; 6 | import react.ReactMacro.jsx; 7 | import js.html.InputElement; 8 | import store.TodoActions; 9 | import store.TodoItem; 10 | import store.TodoStore; 11 | 12 | typedef TodoAppState = { 13 | items:Array 14 | } 15 | 16 | class TodoApp extends ReactComponentOfState 17 | { 18 | var input:ReactRef = React.createRef(); 19 | var todoStore = new TodoStore(); 20 | 21 | public function new(props:Dynamic) 22 | { 23 | super(props); 24 | 25 | state = { items:todoStore.list }; 26 | 27 | todoStore.changed.add(function() { 28 | setState({ items:todoStore.list }); 29 | }); 30 | } 31 | 32 | override public function render() 33 | { 34 | var unchecked = state.items.filter(function(item) return !item.checked).length; 35 | 36 | var listProps = { data:state.items }; 37 | return jsx( 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
{unchecked} task(s) left
47 |
48 | ); 49 | } 50 | 51 | function mountList(comp:ReactComponent) 52 | { 53 | trace('List mounted ' + comp.props); 54 | } 55 | 56 | function addItem(e) 57 | { 58 | e.preventDefault(); 59 | var text = input.current.value; 60 | if (text.length > 0) 61 | { 62 | TodoActions.addItem.dispatch(text); 63 | input.current.value = ""; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/todoapp/src/view/TodoList.hx: -------------------------------------------------------------------------------- 1 | package view; 2 | 3 | import react.ReactComponent; 4 | import react.ReactMacro.jsx; 5 | import js.html.Element; 6 | import js.html.Event; 7 | import store.TodoActions; 8 | import store.TodoItem; 9 | 10 | typedef TodoListProps = { 11 | ?padding:String, 12 | ?className:String, 13 | ?data:Array 14 | } 15 | 16 | class TodoList extends ReactComponent 17 | { 18 | static var defaultProps:TodoListProps = { 19 | padding: '10px', 20 | className: 'list' 21 | } 22 | 23 | public function new(props:TodoListProps) 24 | { 25 | super(props); 26 | } 27 | 28 | override public function render() 29 | { 30 | var style = { 31 | padding: props.padding 32 | }; 33 | 34 | return jsx(' 35 |
    36 | ${createChildren()} 37 |
38 | '); 39 | } 40 | 41 | function createChildren() 42 | { 43 | return [for (entry in props.data) jsx('')]; 44 | } 45 | 46 | function toggleChecked(e:Event) 47 | { 48 | var node:Element = cast e.target; 49 | if (node.nodeName == 'LI') 50 | { 51 | var id = node.id.split('-')[1]; 52 | TodoActions.toggleItem.dispatch(id); 53 | } 54 | } 55 | } 56 | 57 | typedef TodoItemProps = { 58 | ?data:TodoItem, 59 | ?padding:String, 60 | ?border:String 61 | } 62 | 63 | class TodoListItem extends ReactComponent 64 | { 65 | var checked:Bool; 66 | 67 | static var defaultProps:TodoItemProps = { 68 | padding: '10px', 69 | border: 'solid 1px #363' 70 | } 71 | 72 | override public function shouldComponentUpdate(nextProps:TodoItemProps, nextState:react.Empty):Bool 73 | { 74 | return nextProps.data.checked != checked; 75 | } 76 | 77 | override public function render() 78 | { 79 | var style = { 80 | padding: props.padding, 81 | border: props.border 82 | }; 83 | var entry:TodoItem = props.data; 84 | checked = entry.checked; 85 | var id = 'item-${entry.id}'; 86 | return jsx(' 87 |
  • 88 | ${entry.label} 89 |
  • 90 | '); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/react/BaseProps.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactFragment; 4 | import react.ReactComponent.ReactSingleFragment; 5 | import tink.core.Noise; 6 | 7 | typedef BaseProps = { 8 | var children:TChildren; 9 | } 10 | 11 | typedef BasePropsOpt = { 12 | @:optional var children:TChildren; 13 | } 14 | 15 | typedef BasePropsWithChildren = BaseProps; 16 | typedef BasePropsWithChild = BaseProps; 17 | 18 | typedef BasePropsWithoutChildren = BasePropsOpt; 19 | 20 | typedef BasePropsWithOptChildren = BasePropsOpt; 21 | typedef BasePropsWithOptChild = BasePropsOpt; 22 | -------------------------------------------------------------------------------- /src/lib/react/Empty.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | typedef Empty = {} 4 | 5 | -------------------------------------------------------------------------------- /src/lib/react/Fragment.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.BaseProps; 4 | import react.ReactComponent; 5 | 6 | /** 7 | Warning: Fragments are only available in react 16.2.0+ 8 | https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html 9 | **/ 10 | #if (!react_global) 11 | @:jsRequire("react", "Fragment") 12 | #end 13 | @:native('React.Fragment') 14 | extern class Fragment extends ReactComponentOfProps {} 15 | 16 | -------------------------------------------------------------------------------- /src/lib/react/Partial.hx: -------------------------------------------------------------------------------- 1 | /* 2 | From a gist by George Corney: 3 | https://gist.github.com/haxiomic/ad4f5d329ac616543819395f42037aa1 4 | 5 | A Partial, where T is a typedef, is T where all the fields are optional 6 | */ 7 | package react; 8 | 9 | import haxe.macro.Context; 10 | import haxe.macro.Expr; 11 | import haxe.macro.TypeTools; 12 | 13 | #if !macro 14 | @:genericBuild(react.PartialMacro.build()) 15 | #end 16 | class Partial {} 17 | 18 | @:dce 19 | class PartialMacro { 20 | #if macro 21 | static var cache:Map = new Map(); 22 | 23 | static function build() { 24 | var localType = Context.getLocalType(); 25 | var cacheKey = TypeTools.toString(localType); 26 | if (cache.exists(cacheKey)) return cache.get(cacheKey); 27 | 28 | switch (localType) { 29 | // Match when class's type parameter leads to an anonymous type (we convert to a complex type in the process to make it easier to work with) 30 | case TInst(_, [Context.followWithAbstracts(_) => TypeTools.toComplexType(_) => TAnonymous(fields)]): 31 | // Add @:optional meta to all fields 32 | var newFields = fields.map(addMeta); 33 | var ret = TAnonymous(newFields); 34 | cache.set(cacheKey, ret); 35 | return ret; 36 | 37 | default: 38 | Context.fatalError('Type parameter should be an anonymous structure', Context.currentPos()); 39 | } 40 | 41 | return null; 42 | } 43 | 44 | static function addMeta(field: Field): Field { 45 | // Handle Null and optional fields already parsed by the compiler 46 | var kind = switch (field.kind) { 47 | case FVar(TPath({ 48 | name: 'StdTypes', 49 | sub: 'Null', 50 | params: [TPType(TPath(tpath))] 51 | }), write): 52 | FVar(TPath(tpath), write); 53 | 54 | default: 55 | field.kind; 56 | } 57 | 58 | return { 59 | name: field.name, 60 | kind: kind, 61 | access: field.access, 62 | meta: field.meta.concat([{ 63 | name: ':optional', 64 | params: [], 65 | pos: Context.currentPos() 66 | }]), 67 | pos: field.pos 68 | }; 69 | } 70 | #end 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/react/PureComponent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent; 4 | 5 | typedef PureComponent = PureComponentOf; 6 | typedef PureComponentOfProps = PureComponentOf; 7 | typedef PureComponentOfState = PureComponentOf; 8 | 9 | #if (!react_global) 10 | @:jsRequire("react", "PureComponent") 11 | #end 12 | @:native('React.PureComponent') 13 | @:keepSub 14 | extern class PureComponentOf 15 | extends ReactComponentOf 16 | {} 17 | -------------------------------------------------------------------------------- /src/lib/react/React.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.Symbol; 4 | import js.lib.Promise; 5 | 6 | import react.ReactComponent.ReactElement; 7 | import react.ReactComponent.ReactFragment; 8 | import react.ReactComponent.ReactSingleFragment; 9 | import react.ReactContext; 10 | import react.ReactType; 11 | 12 | /** 13 | https://react.dev/reference/react/apis 14 | https://react.dev/reference/react/legacy 15 | **/ 16 | #if (!react_global) 17 | @:jsRequire("react") 18 | #end 19 | @:native('React') 20 | extern class React { 21 | /** 22 | https://react.dev/reference/react/createElement 23 | **/ 24 | public static function createElement(type:ReactType, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement; 25 | 26 | /** 27 | Warning: 28 | Using `cloneElement` is uncommon and can lead to fragile code 29 | 30 | https://react.dev/reference/react/cloneElement 31 | **/ 32 | public static function cloneElement(element:ReactElement, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement; 33 | 34 | /** 35 | https://react.dev/reference/react/isValidElement 36 | **/ 37 | public static function isValidElement(object:ReactFragment):Bool; 38 | 39 | /** 40 | https://react.dev/reference/react/createContext 41 | 42 | Creates a `{ Provider, Consumer }` pair. 43 | When React renders a context `Consumer`, it will read the current 44 | context value from the closest matching `Provider` above it in the tree. 45 | 46 | The `defaultValue` argument is **only** used by a `Consumer` when it 47 | does not have a matching Provider above it in the tree. This can be 48 | helpful for testing components in isolation without wrapping them. 49 | 50 | Note: passing `undefined` as a `Provider` value does not cause Consumers 51 | to use `defaultValue`. 52 | **/ 53 | public static function createContext( 54 | ?defaultValue:TContext, 55 | ?calculateChangedBits:TContext->TContext->Int 56 | ):ReactContext; 57 | 58 | /** 59 | https://react.dev/reference/react/createRef 60 | 61 | Note: this API has been introduced in React 16.3 62 | If you are using an earlier release of React, use callback refs instead 63 | https://reactjs.org/docs/refs-and-the-dom.html#callback-refs 64 | **/ 65 | public static function createRef():ReactRef; 66 | 67 | /** 68 | https://react.dev/reference/react/forwardRef 69 | See also https://react.dev/learn/manipulating-the-dom-with-refs 70 | 71 | Note: this API has been introduced in React 16.3 72 | If you are using an earlier release of React, use callback refs instead 73 | https://reactjs.org/docs/refs-and-the-dom.html#callback-refs 74 | **/ 75 | public static function forwardRef(render:TProps->ReactRef->ReactFragment):ReactType; 76 | 77 | /** 78 | Warning 79 | Using `Children` is uncommon and can lead to fragile code. 80 | 81 | https://react.dev/reference/react/Children 82 | **/ 83 | public static var Children:ReactChildren; 84 | 85 | /** 86 | https://react.dev/reference/react/lazy 87 | **/ 88 | public static function lazy(loader:Void->Promise>):ReactType; 89 | 90 | /** 91 | Utility to use `React.lazy()` on an already loaded `ReactType` (either 92 | class component or function), mostly to be used with `react.Suspense`. 93 | **/ 94 | public static inline function lazify(t:ReactType):ReactType { 95 | return lazy(() -> Promise.resolve(React.createModule(t))); 96 | } 97 | 98 | /** 99 | Let any `ReactType` pretend to be a module in order to be usable by 100 | `React.lazy()`. Works with class components, functions, etc. 101 | **/ 102 | public static inline function createModule(t:ReactType):Module return t; 103 | 104 | public static var version:String; 105 | 106 | public static var Fragment:Symbol; 107 | public static var StrictMode:Symbol; 108 | public static var unstable_AsyncMode:Symbol; 109 | public static var unstable_Profiler:Symbol; 110 | 111 | @:native('__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED') 112 | public static var _internals:ReactSharedInternals; 113 | } 114 | 115 | /** 116 | https://react.dev/reference/react/Children 117 | **/ 118 | extern interface ReactChildren { 119 | /** 120 | https://react.dev/reference/react/Children#children-map 121 | **/ 122 | function map(children:Dynamic, fn:Array->ReactFragment):Null>; 123 | 124 | /** 125 | https://react.dev/reference/react/Children#children-foreach 126 | **/ 127 | function foreach(children:Dynamic, fn:ReactFragment->Void):Void; 128 | 129 | /** 130 | https://react.dev/reference/react/Children#children-count 131 | **/ 132 | function count(children:ReactFragment):Int; 133 | 134 | /** 135 | https://react.dev/reference/react/Children#children-only 136 | **/ 137 | function only(children:ReactFragment):ReactSingleFragment; 138 | 139 | /** 140 | https://react.dev/reference/react/Children#children-toarray 141 | **/ 142 | function toArray(children:ReactFragment):Array; 143 | } 144 | 145 | @:deprecated 146 | typedef CreateElementType = ReactType; 147 | 148 | @:coreType abstract Module { 149 | @:from 150 | static function fromT(v:T):Module return cast {"default": v}; 151 | } 152 | -------------------------------------------------------------------------------- /src/lib/react/ReactClipboardEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | /** 4 | https://facebook.github.io/react/docs/events.html 5 | **/ 6 | extern class ReactClipboardEvent extends ReactEvent 7 | { 8 | public var clipboardData(default, null):String; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/react/ReactComponent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | #if haxe4 4 | import js.lib.Error; 5 | #else 6 | import js.Error; 7 | #end 8 | 9 | import haxe.extern.EitherType; 10 | 11 | typedef ReactComponentProps = { 12 | /** 13 | Children have to be manipulated using React.Children.* 14 | **/ 15 | @:optional var children:Dynamic; 16 | } 17 | 18 | /** 19 | https://facebook.github.io/react/docs/react-component.html 20 | 21 | Can be used as: 22 | - `ReactComponent`, which means TProps = Dynamic and TState = Dynamic (not 23 | recommended, but can help when starting to write externs) 24 | 25 | - `ReactComponent` which means your component doesn't have a state 26 | - `ReactComponent` 27 | 28 | For a component with state and no props, continue using 29 | `ReactComponentOfState`. 30 | **/ 31 | @:genericBuild(react.macro.ReactComponentMacro.buildVariadic()) 32 | class ReactComponent {} 33 | 34 | typedef ReactComponentOfProps = ReactComponentOf; 35 | typedef ReactComponentOfState = ReactComponentOf; 36 | 37 | // Keep the old ReactComponentOfPropsAndState typedef available 38 | typedef ReactComponentOfPropsAndState = ReactComponentOf; 39 | 40 | #if (!react_global) 41 | @:jsRequire("react", "Component") 42 | #end 43 | @:native('React.Component') 44 | @:keepSub 45 | @:autoBuild(react.macro.ReactComponentMacro.build()) 46 | extern class ReactComponentOf 47 | { 48 | #if haxe4 49 | final props:TProps; 50 | #else 51 | var props(default, null):TProps; 52 | #end 53 | 54 | // Note: cannot use final for haxe 4.1.0 - 4.1.1 because of 55 | // https://github.com/HaxeFoundation/haxe/issues/9552 56 | var state(default, null):TState; 57 | 58 | #if react_deprecated_context 59 | // It's better to define it in your ReactComponent subclass as needed, with the right typing. 60 | var context(default, null):Dynamic; 61 | #end 62 | 63 | function new(?props:TProps, ?context:Dynamic); 64 | 65 | /** 66 | https://facebook.github.io/react/docs/react-component.html#forceupdate 67 | **/ 68 | function forceUpdate(?callback:Void -> Void):Void; 69 | 70 | /** 71 | https://facebook.github.io/react/docs/react-component.html#setstate 72 | **/ 73 | @:overload(function(nextState:TState, ?callback:Void -> Void):Void {}) 74 | @:overload(function(nextState:TState -> TProps -> TState, ?callback:Void -> Void):Void {}) 75 | function setState(nextState:TState -> TState, ?callback:Void -> Void):Void; 76 | 77 | /** 78 | https://facebook.github.io/react/docs/react-component.html#render 79 | **/ 80 | function render():ReactFragment; 81 | 82 | /** 83 | https://facebook.github.io/react/docs/react-component.html#componentwillmount 84 | **/ 85 | function componentWillMount():Void; 86 | 87 | /** 88 | https://facebook.github.io/react/docs/react-component.html#componentdidmount 89 | **/ 90 | function componentDidMount():Void; 91 | 92 | /** 93 | https://facebook.github.io/react/docs/react-component.html#componentwillunmount 94 | **/ 95 | function componentWillUnmount():Void; 96 | 97 | /** 98 | https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops 99 | **/ 100 | function componentWillReceiveProps(nextProps:TProps):Void; 101 | 102 | /** 103 | https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate 104 | **/ 105 | dynamic function shouldComponentUpdate(nextProps:TProps, nextState:TState):Bool; 106 | 107 | /** 108 | https://facebook.github.io/react/docs/react-component.html#componentwillupdate 109 | **/ 110 | function componentWillUpdate(nextProps:TProps, nextState:TState):Void; 111 | 112 | /** 113 | https://facebook.github.io/react/docs/react-component.html#componentdidupdate 114 | **/ 115 | function componentDidUpdate(prevProps:TProps, prevState:TState):Void; 116 | 117 | /** 118 | https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html 119 | **/ 120 | function componentDidCatch(error:Error, info:{ componentStack:String }):Void; 121 | 122 | #if (js && (react_force_inline || (!debug && !react_no_inline))) 123 | static function __init__():Void { 124 | // required magic value to tag literal react elements 125 | #if haxe4 126 | js.Syntax.code("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 127 | #else 128 | untyped __js__("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 129 | #end 130 | } 131 | #end 132 | } 133 | 134 | // Used internally to make @:acceptsMoreProps and @:wrap compatible 135 | // Needs to be tested extensively before encouraging manual use 136 | typedef ACCEPTS_MORE_PROPS = TProps; 137 | 138 | typedef ReactSource = { 139 | fileName:String, 140 | lineNumber:Int 141 | } 142 | 143 | typedef ReactElement = { 144 | type:ReactType, 145 | props:Dynamic, 146 | ?key:Dynamic, 147 | ?ref:Dynamic, 148 | ?_owner:Dynamic, 149 | 150 | #if debug 151 | ?_store:{validated:Bool}, 152 | ?_shadowChildren:Dynamic, 153 | ?_source:ReactSource, 154 | #end 155 | } 156 | 157 | @:pure @:coreType abstract ReactSingleFragment 158 | from String 159 | from Float 160 | from Bool 161 | from ReactElement {} 162 | 163 | @:pure @:coreType abstract ReactFragment 164 | from ReactSingleFragment 165 | from Array 166 | from Array 167 | from Array 168 | from Array 169 | from Array 170 | from Array 171 | from Array {} 172 | -------------------------------------------------------------------------------- /src/lib/react/ReactComponentMacro.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.macro.ReactComponentMacro as RealReactComponentMacro; 4 | import react.macro.ReactComponentMacro.Builder; 5 | 6 | @:dce 7 | @:deprecated('ReactComponentMacro has moved to react.macro package') 8 | class ReactComponentMacro { 9 | @:deprecated('ReactComponentMacro has moved to react.macro package') 10 | static public function appendBuilder(builder:Builder):Void 11 | { 12 | RealReactComponentMacro.appendBuilder(builder); 13 | } 14 | 15 | @:deprecated('ReactComponentMacro has moved to react.macro package') 16 | static public function prependBuilder(builder:Builder):Void 17 | { 18 | RealReactComponentMacro.prependBuilder(builder); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/react/ReactContext.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactFragment; 4 | import react.ReactType; 5 | 6 | @:forward 7 | abstract ReactContext(IReactContext) from IReactContext to IReactContext { 8 | @:to 9 | public function toReactType():ReactTypeOf<{children:T->ReactFragment}> { 10 | return cast this; 11 | } 12 | } 13 | 14 | extern interface IReactContext { 15 | var displayName:String; 16 | var Consumer:ReactContext; 17 | var Provider:ReactProviderType; 18 | 19 | var unstable_read:Void->T; 20 | var _calculateChangedBits:NullT->Int>; 21 | 22 | var _currentValue:T; 23 | var _currentValue2:T; 24 | 25 | #if debug 26 | @:optional var _currentRenderer:Null; 27 | @:optional var _currentRenderer2:Null; 28 | #end 29 | } 30 | 31 | @:pure @:coreType 32 | abstract ReactProviderType 33 | from IReactProviderType 34 | to IReactProviderType 35 | to ReactTypeOf<{value:T}> {} 36 | 37 | extern interface IReactProviderType { 38 | var _context:ReactContext; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/react/ReactDOM.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.Element; 4 | import react.ReactComponent; 5 | 6 | /** 7 | https://react.dev/reference/react-dom 8 | **/ 9 | #if (!react_global) 10 | @:jsRequire("react-dom") 11 | #end 12 | @:native('ReactDOM') 13 | extern class ReactDOM 14 | { 15 | /** 16 | https://react.dev/reference/react-dom/render 17 | **/ 18 | @:deprecated("Use ReactDOMClient.createRoot") 19 | public static function render(element:ReactFragment, container:Element, ?callback:Void -> Void):ReactFragment; 20 | 21 | /** 22 | https://react.dev/reference/react-dom/hydrate 23 | **/ 24 | @:deprecated("Use ReactDOMClient.hydrateRoot") 25 | public static function hydrate(element:ReactFragment, container:Element, ?callback:Void -> Void):ReactFragment; 26 | 27 | /** 28 | https://react.dev/reference/react-dom/unmountComponentAtNode 29 | **/ 30 | @:deprecated("In React 18, unmountComponentAtNode was replaced by root.unmount()") 31 | public static function unmountComponentAtNode(container:Element):Bool; 32 | 33 | /** 34 | https://react.dev/reference/react-dom/findDOMNode 35 | **/ 36 | @:deprecated 37 | public static function findDOMNode(component:ReactComponent):Element; 38 | 39 | /** 40 | https://react.dev/reference/react-dom/createPortal 41 | **/ 42 | public static function createPortal(child:ReactFragment, container:Element):ReactFragment; 43 | 44 | /** 45 | Warning: 46 | Using flushSync is uncommon and can hurt the performance of your app. 47 | 48 | https://react.dev/reference/react-dom/flushSync 49 | **/ 50 | public static function flushSync(callback:Void->Void):Void; 51 | } 52 | 53 | #if (!react_global) 54 | @:jsRequire("react-dom/client") 55 | #end 56 | @:native('ReactDOM') 57 | extern class ReactDOMClient { 58 | /** 59 | `createRoot` lets you create a root to display React components inside a 60 | browser DOM node. 61 | 62 | Notes: 63 | - If your app is server-rendered, using `createRoot()` is not supported. 64 | Use `hydrateRoot()` instead 65 | - You’ll likely have only one `createRoot` call in your app 66 | - When you want to render a piece of JSX in a different part of the DOM 67 | tree that isn’t a child of your component (for example, a modal or a 68 | tooltip), use `ReactDOM.createPortal` instead of `createRoot` 69 | 70 | https://react.dev/reference/react-dom/client/createRoot 71 | **/ 72 | public static function createRoot(container:Element, ?options:{ 73 | ?identifierPrefix:String, 74 | ?onRecoverableError:(err:Any)->Void 75 | }):RootType; 76 | 77 | /** 78 | `hydrateRoot` lets you display React components inside a browser DOM 79 | node whose HTML content was previously generated by `ReactDOMServer`. 80 | 81 | Notes: 82 | - `hydrateRoot()` expects the rendered content to be identical with the 83 | server-rendered content. You should treat mismatches as bugs and fix 84 | them 85 | - In development mode, React warns about mismatches during hydration. 86 | There are no guarantees that attribute differences will be patched up 87 | in case of mismatches 88 | - You’ll likely have only one `hydrateRoot` call in your app 89 | - If your app is client-rendered with no HTML rendered already, using 90 | `hydrateRoot()` is not supported. Use `createRoot()` instead 91 | 92 | 93 | https://react.dev/reference/react-dom/client/hydrateRoot 94 | **/ 95 | public static function hydrateRoot(container:Element, element:ReactFragment, ?options:{ 96 | ?identifierPrefix:String, 97 | ?onRecoverableError:(err:Any)->Void 98 | }):RootType; 99 | } 100 | 101 | typedef RootType = { 102 | render:(node:ReactFragment)->Void, 103 | unmount:()->Void 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/react/ReactDOMServer.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.AbortSignal; 4 | import js.lib.Promise; 5 | import react.ReactComponent.ReactFragment; 6 | 7 | #if nodejs 8 | import js.node.stream.Readable; 9 | import js.node.stream.Writable; 10 | #end 11 | 12 | /** 13 | The `ReactDOMServer` APIs let you render React components to HTML on the 14 | server. These APIs are only used on the server at the top level of your app to 15 | generate the initial HTML. Most of your components don’t need to import or use them. 16 | 17 | https://react.dev/reference/react-dom/server 18 | **/ 19 | #if (!react_global) 20 | @:jsRequire('react-dom/server') 21 | #end 22 | @:native('ReactDOMServer') 23 | extern class ReactDOMServer { 24 | /** 25 | Note: `renderToString` does not support streaming or waiting for data. 26 | See the alternatives. 27 | 28 | https://react.dev/reference/react-dom/server/renderToString 29 | **/ 30 | public static function renderToString(node:ReactFragment):String; 31 | 32 | /** 33 | `renderToStaticMarkup` renders a non-interactive React tree to an HTML 34 | string. 35 | 36 | Notes: 37 | - `renderToStaticMarkup` output cannot be hydrated. 38 | - `renderToStaticMarkup` has limited `Suspense support`. If a component 39 | suspends, `renderToStaticMarkup` immediately sends its fallback as HTML. 40 | 41 | https://react.dev/reference/react-dom/server/renderToStaticMarkup 42 | **/ 43 | public static function renderToStaticMarkup(node:ReactFragment):String; 44 | 45 | #if nodejs 46 | /** 47 | `renderToPipeableStream` renders a React tree to a pipeable Node.js Stream. 48 | 49 | Note: This API is specific to Node.js. 50 | Environments with Web Streams should use `renderToReadableStream` instead. 51 | 52 | https://react.dev/reference/react-dom/server/renderToPipeableStream 53 | **/ 54 | public static function renderToPipeableStream(node:ReactFragment, options:{ 55 | ?bootstrapScriptContent:String, 56 | ?bootstrapScripts:Array, 57 | ?bootstrapModules:Array, 58 | ?identifierPrefix:String, 59 | ?namespaceURI:String, 60 | ?nonce:String, 61 | ?onAllReady:Void->Void, 62 | ?onError:Any->Void, 63 | ?onShellReady:Void->Void, 64 | ?onShellError:Any->Void, 65 | ?progressiveChunkSize:Int 66 | }):{ 67 | pipe:IWritable->Void, 68 | abort:Void->Void 69 | } 70 | 71 | /** 72 | `renderToStaticNodeStream` renders a non-interactive React tree to a 73 | Node.js Readable Stream. 74 | 75 | Notes: 76 | - `renderToStaticNodeStream` output cannot be hydrated. 77 | - This method will wait for all `Suspense` boundaries to complete before 78 | returning any output. 79 | - As of React 18, this method buffers all of its output, so it doesn't 80 | actually provide any streaming benefits. 81 | - The returned stream is a byte stream encoded in utf-8 82 | 83 | https://react.dev/reference/react-dom/server/renderToStaticNodeStream 84 | **/ 85 | public static function renderToStaticNodeStream(node:ReactFragment):IReadable; 86 | 87 | /** 88 | https://react.dev/reference/react-dom/server/renderToNodeStream 89 | **/ 90 | @:deprecated("Use renderToPipeableStream instead") 91 | public static function renderToNodeStream(node:ReactFragment):IReadable; 92 | #else 93 | /** 94 | `renderToReadableStream` renders a React tree to a Readable Web Stream. 95 | 96 | Note: This API depends on Web Streams. 97 | For Node.js, use `renderToPipeableStream` instead. 98 | 99 | https://react.dev/reference/react-dom/server/renderToReadableStream 100 | **/ 101 | public static function renderToReadableStream(node:ReactFragment, options:{ 102 | ?bootstrapScriptContent:String, 103 | ?bootstrapScripts:Array, 104 | ?bootstrapModules:Array, 105 | ?identifierPrefix:String, 106 | ?namespaceURI:String, 107 | ?nonce:String, 108 | ?onError:Any->Void, 109 | ?progressiveChunkSize:Int, 110 | ?signal:AbortSignal 111 | }):Promise; 112 | #end 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/react/ReactEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.Event; 4 | import js.html.EventTarget; 5 | 6 | /** 7 | https://facebook.github.io/react/docs/events.html 8 | **/ 9 | extern class ReactEvent 10 | { 11 | public var bubbles(default, null):Bool; 12 | public var cancelable(default, null):Bool; 13 | public var currentTarget(default, null):EventTarget; 14 | public var defaultPrevented(default, null):Bool; 15 | public var eventPhase(default, null):Int; 16 | public var isTrusted(default, null):Bool; 17 | public var nativeEvent(default, null):Event; 18 | public var target(default, null):EventTarget; 19 | public var timeStamp(default, null):Date; 20 | public var type(default, null):String; 21 | 22 | public function preventDefault():Void; 23 | public function isDefaultPrevented():Bool; 24 | public function stopPropagation():Void; 25 | public function isPropagationStopped():Bool; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/react/ReactFocusEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.EventTarget; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/events.html 7 | **/ 8 | extern class ReactFocusEvent extends ReactEvent 9 | { 10 | public var relatedTarget:EventTarget; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/react/ReactKeyboardEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | /** 4 | https://facebook.github.io/react/docs/events.html 5 | **/ 6 | extern class ReactKeyboardEvent extends ReactEvent 7 | { 8 | public var altKey(default, null):Bool; 9 | public var charCode(default, null):Int; 10 | public var ctrlKey(default, null):Bool; 11 | public var key(default, null):String; 12 | public var keyCode(default, null):Int; 13 | public var locale(default, null):String; 14 | public var location(default, null):Int; 15 | public var metaKey(default, null):Bool; 16 | public var repeat(default, null):Bool; 17 | public var shiftKey(default, null):Bool; 18 | public var which(default, null):Int; 19 | 20 | public function getModifierState(key:Int):Bool; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/react/ReactMouseEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.EventTarget; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/events.html 7 | **/ 8 | extern class ReactMouseEvent extends ReactEvent 9 | { 10 | public var altKey:Bool; 11 | public var button:Int; 12 | public var buttons:Int; 13 | public var clientX:Int; 14 | public var clientY:Int; 15 | public var ctrlKey:Bool; 16 | public var metaKey:Bool; 17 | public var pageX:Int; 18 | public var pageY:Int; 19 | public var relatedTarget:EventTarget; 20 | public var screenX:Int; 21 | public var screenY:Int; 22 | public var shiftKey:Bool; 23 | 24 | public function getModifierState(key:Int):Bool; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/react/ReactNode.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactType; 4 | 5 | #if !react_ignore_reactnode_deprecation @:deprecated #end 6 | typedef ReactNode = ReactType; 7 | 8 | #if !react_ignore_reactnode_deprecation @:deprecated #end 9 | typedef ReactNodeOf = ReactTypeOf; 10 | -------------------------------------------------------------------------------- /src/lib/react/ReactPropTypes.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | #if haxe4 4 | import js.lib.Error; 5 | #else 6 | import js.Error; 7 | #end 8 | 9 | import haxe.extern.EitherType; 10 | import react.ReactComponent; 11 | 12 | /** 13 | https://reactjs.org/docs/typechecking-with-proptypes.html 14 | **/ 15 | #if (!react_global) 16 | @:jsRequire('prop-types') 17 | #end 18 | @:native('PropTypes') 19 | extern class ReactPropTypes 20 | { 21 | static var any:ChainableTypeChecker; 22 | static var array:ChainableTypeChecker; 23 | static var bool:ChainableTypeChecker; 24 | static var func:ChainableTypeChecker; 25 | static var number:ChainableTypeChecker; 26 | static var object:ChainableTypeChecker; 27 | static var string:ChainableTypeChecker; 28 | static var symbol:ChainableTypeChecker; 29 | static var element:ChainableTypeChecker; 30 | static var node:ChainableTypeChecker; 31 | 32 | static var arrayOf:ArrayOfTypeChecker -> ChainableTypeChecker; 33 | static var instanceOf:Class -> ChainableTypeChecker; 34 | static var objectOf:ArrayOfTypeChecker -> ChainableTypeChecker; 35 | static var oneOf:Array -> ChainableTypeChecker; 36 | static var oneOfType:Array -> ChainableTypeChecker; 37 | static var shape:TypeShape->ChainableTypeChecker; 38 | static var exact:TypeShape->ChainableTypeChecker; 39 | 40 | static function checkPropTypes( 41 | typeSpecs:Dynamic, 42 | values:Dynamic, 43 | location:PropTypesLocation, 44 | componentName:String, 45 | ?getStack:Void -> Dynamic 46 | ):Dynamic; 47 | } 48 | 49 | typedef TypeChecker = EitherType; 50 | typedef ArrayOfTypeChecker = EitherType; 51 | typedef CustomTypeChecker = Dynamic -> String -> String -> Null; 52 | typedef CustomArrayOfTypeChecker = Array -> String -> String -> PropTypesLocation -> String -> Null; 53 | typedef TypeShape = Dynamic; 54 | 55 | enum abstract PropTypesLocation(String) from String { 56 | var Prop = 'prop'; 57 | var Context = 'context'; 58 | var ChildContext = 'child context'; 59 | } 60 | 61 | private typedef ChainableTypeChecker = { 62 | @:optional var isRequired:Dynamic; 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/react/ReactRef.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.Constraints.Function; 4 | 5 | @:callable 6 | abstract ReactRef(Function) { 7 | public var current(get, never):T; 8 | 9 | public function get_current():T { 10 | return untyped this.current; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/lib/react/ReactSharedInternals.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactSource; 4 | 5 | extern interface ReactSharedInternals 6 | { 7 | var ReactCurrentOwner:{ 8 | current: Null, 9 | currentDispatcher: Null 10 | }; 11 | 12 | #if debug 13 | var ReactDebugCurrentFrame:{ 14 | getCurrentStack:NullString>, 15 | getStackAddendum:Void->String 16 | }; 17 | #end 18 | } 19 | 20 | /** 21 | https://github.com/facebook/react/blob/master/packages/shared/ReactWorkTags.js 22 | **/ 23 | enum abstract WorkTag(Int) from Int to Int 24 | { 25 | var FunctionalComponent = 0; 26 | var FunctionalComponentLazy = 1; 27 | var ClassComponent = 2; 28 | var ClassComponentLazy = 3; 29 | var IndeterminateComponent = 4; // Before we know whether it is functional or class 30 | var HostRoot = 5; // Root of a host tree. Could be nested inside another node. 31 | var HostPortal = 6; // A subtree. Could be an entry point to a different renderer. 32 | var HostComponent = 7; 33 | var HostText = 8; 34 | var Fragment = 9; 35 | var Mode = 10; 36 | var ContextConsumer = 11; 37 | var ContextProvider = 12; 38 | var ForwardRef = 13; 39 | var ForwardRefLazy = 14; 40 | var Profiler = 15; 41 | var PlaceholderComponent = 16; 42 | } 43 | 44 | extern interface Instance { 45 | // Tag identifying the type of fiber. 46 | var tag:WorkTag; 47 | 48 | // Unique identifier of this child. 49 | var key:Null; 50 | 51 | // The function/class/module associated with this fiber. 52 | var type:Any; 53 | 54 | // The local state associated with this fiber. 55 | var stateNode:Any; 56 | } 57 | 58 | extern interface ReactFiber extends Instance { 59 | 60 | // The Fiber to return to after finishing processing this one. 61 | // This is effectively the parent, but there can be multiple parents (two) 62 | // so this is only the parent of the thing we're currently processing. 63 | // It is conceptually the same as the return address of a stack frame. 64 | @:native("return") 65 | var _return:Null; 66 | 67 | // Singly Linked List Tree Structure. 68 | var child:Null; 69 | var sibling:Null; 70 | var index:Int; 71 | 72 | // The ref last used to attach this node. 73 | // I'll avoid adding an owner field for prod and model that as functions. 74 | // ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject, 75 | var ref:Null; 76 | 77 | // Input is the data coming into process this fiber. Arguments. Props. 78 | var pendingProps:Any; // This type will be more specific once we overload the tag. 79 | var memoizedProps:Any; // The props used to create the output. 80 | 81 | // A queue of state updates and callbacks. 82 | var updateQueue:Null>; 83 | 84 | // The state used to create the output 85 | var memoizedState:Any; 86 | 87 | // A linked-list of contexts that this fiber depends on 88 | var firstContextDependency:Null>; 89 | 90 | // Bitfield that describes properties about the fiber and its subtree. E.g. 91 | // the AsyncMode flag indicates whether the subtree should be async-by- 92 | // default. When a fiber is created, it inherits the mode of its 93 | // parent. Additional flags can be set at creation time, but after that the 94 | // value should remain unchanged throughout the fiber's lifetime, particularly 95 | // before its child fibers are created. 96 | var mode:TypeOfMode; 97 | 98 | // Effect 99 | var effectTag:SideEffectTag; 100 | 101 | // Singly linked list fast path to the next fiber with side-effects. 102 | var nextEffect:Null; 103 | 104 | // The first and last fiber with side-effect within this subtree. This allows 105 | // us to reuse a slice of the linked list when we reuse the work done within 106 | // this fiber. 107 | var firstEffect:Null; 108 | var lastEffect:Null; 109 | 110 | // Represents a time in the future by which this work should be completed. 111 | // Does not include work found in its subtree. 112 | var expirationTime:Float; 113 | 114 | // This is used to quickly determine if a subtree has no pending changes. 115 | var childExpirationTime:Float; 116 | 117 | // This is a pooled version of a Fiber. Every fiber that gets updated will 118 | // eventually have a pair. There are cases when we can clean up pairs to save 119 | // memory if we need to. 120 | var alternate:Null; 121 | 122 | // Time spent rendering this Fiber and its descendants for the current update. 123 | // This tells us how well the tree makes use of sCU for memoization. 124 | // It is reset to 0 each time we render and only updated when we don't bailout. 125 | // This field is only set when the enableProfilerTimer flag is enabled. 126 | @:optional var actualDuration:Float; 127 | 128 | // If the Fiber is currently active in the "render" phase, 129 | // This marks the time at which the work began. 130 | // This field is only set when the enableProfilerTimer flag is enabled. 131 | @:optional var actualStartTime:Float; 132 | 133 | // Duration of the most recent render time for this Fiber. 134 | // This value is not updated when we bailout for memoization purposes. 135 | // This field is only set when the enableProfilerTimer flag is enabled. 136 | @:optional var selfBaseDuration:Float; 137 | 138 | // Sum of base times for all descedents of this Fiber. 139 | // This value bubbles up during the "complete" phase. 140 | // This field is only set when the enableProfilerTimer flag is enabled. 141 | @:optional var treeBaseDuration:Float; 142 | 143 | #if debug 144 | @:optional var debugID:Float; 145 | @:optional var debugSource:Null; 146 | @:optional var debugOwner:Null; 147 | @:optional var debugIsCurrentlyTiming:Bool; 148 | #end 149 | } 150 | 151 | extern interface Update 152 | { 153 | var expirationTime:Float; 154 | 155 | var tag:UpdateTag; 156 | var payload:Any; 157 | var callback:NullDynamic>; 158 | 159 | var next:Null>; 160 | var nextEffect:Null>; 161 | } 162 | 163 | extern interface UpdateQueue 164 | { 165 | var baseState:State; 166 | 167 | var firstUpdate:Null>; 168 | var lastUpdate:Null>; 169 | 170 | var firstCapturedUpdate:Null>; 171 | var lastCapturedUpdate:Null>; 172 | 173 | var firstEffect:Null>; 174 | var lastEffect:Null>; 175 | 176 | var firstCapturedEffect:Null>; 177 | var lastCapturedEffect:Null>; 178 | } 179 | 180 | enum abstract UpdateTag(Int) from Int to Int 181 | { 182 | var UpdateState = 0; 183 | var ReplaceState = 1; 184 | var ForceUpdate = 2; 185 | var CaptureUpdate = 3; 186 | } 187 | 188 | enum abstract TypeOfMode(Int) from Int to Int 189 | { 190 | var NoContext = 0; 191 | var AsyncMode = 1; 192 | var StrictMode = 2; 193 | var ProfileMode = 3; 194 | } 195 | 196 | enum abstract SideEffectTag(Int) from Int to Int 197 | { 198 | var NoEffect = 0; 199 | var PerformedWork = 1; 200 | 201 | var Placement = 2; 202 | var Update = 4; 203 | var PlacementAndUpdate = Placement & Update; 204 | var Deletion = 8; 205 | var ContentReset = 16; 206 | var Callback = 32; 207 | var DidCapture = 64; 208 | var Ref = 128; 209 | var Snapshot = 256; 210 | var LifecycleEffectMask = Update & Callback & Ref & Snapshot; 211 | 212 | // Union of all host effects 213 | var HostEffectMask = 511; 214 | 215 | var Incomplete = 512; 216 | var ShouldCapture = 1024; 217 | } 218 | 219 | extern interface ContextDependency 220 | { 221 | var context:ReactContext; 222 | var observedBits:Int; 223 | var next:Null>; 224 | } 225 | 226 | typedef ReactDispatcher = { 227 | var readContext:haxe.Constraints.Function; 228 | } 229 | -------------------------------------------------------------------------------- /src/lib/react/ReactTestUtils.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent; 4 | 5 | /** 6 | https://reactjs.org/docs/test-utils.html 7 | **/ 8 | @:jsRequire("react-dom/test-utils") 9 | extern class ReactTestUtils { 10 | /** 11 | To prepare a component for assertions, wrap the code rendering it and 12 | performing updates inside an act() call. This makes your test run closer 13 | to how React works in the browser. 14 | 15 | https://reactjs.org/docs/test-utils.html#act 16 | **/ 17 | public static function act(task:Void->Void):Void; 18 | 19 | /** 20 | Returns true if element is any React element. 21 | 22 | https://reactjs.org/docs/test-utils.html#iselement 23 | **/ 24 | public static function isElement(element:ReactFragment):Bool; 25 | 26 | /** 27 | Returns true if element is a React element whose type is of a React 28 | componentClass. 29 | 30 | https://reactjs.org/docs/test-utils.html#iselementoftype 31 | **/ 32 | public static function isElementOfType( 33 | element:ReactFragment, 34 | componentClass:ReactType 35 | ):Bool; 36 | 37 | /** 38 | Returns true if instance is a DOM component (such as a
    or ). 39 | 40 | https://reactjs.org/docs/test-utils.html#isdomcomponent 41 | **/ 42 | public static function isDOMComponent(element:ReactFragment):Bool; 43 | 44 | /** 45 | Returns true if instance is a user-defined component, such as a class or 46 | a function. 47 | 48 | https://reactjs.org/docs/test-utils.html#iscompositecomponent 49 | **/ 50 | public static function isCompositeComponent(element:ReactFragment):Bool; 51 | 52 | /** 53 | Returns true if instance is a component whose type is of a React 54 | componentClass. 55 | 56 | https://reactjs.org/docs/test-utils.html#iscompositecomponentwithtype 57 | **/ 58 | public static function isCompositeComponentWithType( 59 | element:ReactFragment, 60 | componentClass:ReactType 61 | ):Bool; 62 | 63 | /** 64 | Traverse all components in tree and accumulate all components where 65 | test(component) is true. This is not that useful on its own, but it’s 66 | used as a primitive for other test utils. 67 | 68 | https://reactjs.org/docs/test-utils.html#findallinrenderedtree 69 | **/ 70 | public static function findAllInRenderedTree( 71 | tree:ReactFragment, 72 | predicate:ReactFragment->Bool 73 | ):Array; 74 | 75 | /** 76 | Finds all DOM elements of components in the rendered tree that are DOM 77 | components with the class name matching className. 78 | 79 | https://reactjs.org/docs/test-utils.html#scryrendereddomcomponentswithclass 80 | **/ 81 | public static function scryRenderedDOMComponentsWithClass( 82 | tree:ReactFragment, 83 | className:String 84 | ):Array; 85 | 86 | /** 87 | Like scryRenderedDOMComponentsWithClass() but expects there to be one 88 | result, and returns that one result, or throws exception if there is any 89 | other number of matches besides one. 90 | 91 | https://reactjs.org/docs/test-utils.html#findrendereddomcomponentwithclass 92 | **/ 93 | public static function findRenderedDOMComponentWithClass( 94 | tree:ReactFragment, 95 | className:String 96 | ):ReactFragment; 97 | 98 | /** 99 | Finds all DOM elements of components in the rendered tree that are DOM 100 | components with the tag name matching tagName. 101 | 102 | https://reactjs.org/docs/test-utils.html#scryrendereddomcomponentswithtag 103 | **/ 104 | public static function scryRenderedDOMComponentsWithTag( 105 | tree:ReactFragment, 106 | tagsName:String 107 | ):Array; 108 | 109 | /** 110 | Like scryRenderedDOMComponentsWithTag() but expects there to be one 111 | result, and returns that one result, or throws exception if there is any 112 | other number of matches besides one. 113 | 114 | https://reactjs.org/docs/test-utils.html#findrendereddomcomponentwithtag 115 | **/ 116 | public static function findRenderedDOMComponentWithTag( 117 | tree:ReactFragment, 118 | tagsName:String 119 | ):ReactFragment; 120 | 121 | /** 122 | Finds all instances of components with type equal to componentClass. 123 | 124 | https://reactjs.org/docs/test-utils.html#scryrenderedcomponentswithtype 125 | **/ 126 | public static function scryRenderedComponentsWithType( 127 | tree:ReactFragment, 128 | componentClass:ReactType 129 | ):Array; 130 | 131 | /** 132 | Same as scryRenderedComponentsWithType() but expects there to be one 133 | result and returns that one result, or throws exception if there is any 134 | other number of matches besides one. 135 | 136 | https://reactjs.org/docs/test-utils.html#findrenderedcomponentwithtype 137 | **/ 138 | public static function findRenderedComponentWithType( 139 | tree:ReactFragment, 140 | componentClass:ReactType 141 | ):ReactFragment; 142 | 143 | /** 144 | Render a React element into a detached DOM node in the document. This 145 | function requires a DOM. 146 | 147 | It is effectively equivalent to: 148 | 149 | ```haxe 150 | var domContainer = js.Browser.document.createElement('div'); 151 | ReactDOM.render(element, domContainer); 152 | ``` 153 | 154 | https://reactjs.org/docs/test-utils.html#renderintodocument 155 | **/ 156 | public static function renderIntoDocument(element:ReactFragment):ReactFragment; 157 | } 158 | 159 | @:jsRequire("react-dom/test-utils", "Simulate") 160 | extern class Simulate { 161 | // TODO 162 | } 163 | -------------------------------------------------------------------------------- /src/lib/react/ReactTouchEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.TouchList; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/events.html 7 | **/ 8 | extern class ReactTouchEvent extends ReactEvent 9 | { 10 | public var altKey:Bool; 11 | public var changedTouches:TouchList; 12 | public var ctrlKey:Bool; 13 | public var metaKey:Bool; 14 | public var shiftKey:Bool; 15 | public var targetTouches:TouchList; 16 | public var touches:TouchList; 17 | 18 | function getModifierState(key:Int):Bool; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/react/ReactType.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.Constraints.Function; 4 | import haxe.extern.EitherType; 5 | 6 | #if macro 7 | import haxe.macro.Expr; 8 | import haxe.macro.Context; 9 | 10 | abstract ReactType(Dynamic) 11 | #else 12 | import react.ReactComponent; 13 | 14 | private typedef Node = EitherType, Class>>; 15 | 16 | abstract ReactType(Node) to Node 17 | #end 18 | { 19 | #if !macro 20 | @:from 21 | static public inline function fromString(s:String):ReactType 22 | { 23 | #if react.debug 24 | if (s == null) return isNull(); 25 | #end 26 | return cast s; 27 | } 28 | 29 | @:from 30 | static public inline function fromFunction(f:Void->ReactFragment):ReactType 31 | { 32 | #if react.debug 33 | if (f == null) return isNull(); 34 | #end 35 | return cast f; 36 | } 37 | 38 | @:from 39 | static public inline function fromFunctionWithProps(f:TProps->ReactFragment):ReactType 40 | { 41 | #if react.debug 42 | if (f == null) return isNull(); 43 | #end 44 | return cast f; 45 | } 46 | 47 | @:from 48 | static public inline function fromComp(cls:Class>):ReactType 49 | { 50 | #if react.debug 51 | if (cls == null) return isNull(); 52 | #end 53 | 54 | if (untyped cls.__jsxStatic != null) 55 | return untyped cls.__jsxStatic; 56 | 57 | return cast cls; 58 | } 59 | 60 | #if react.debug 61 | static public function isNull():ReactType { 62 | js.Browser.console.error( 63 | 'Runtime value for ReactType is null.' 64 | + ' Something may be wrong with your externs.' 65 | ); 66 | 67 | return cast "div"; 68 | } 69 | #end 70 | #end 71 | 72 | @:from 73 | static public macro function fromExpr(expr:Expr) 74 | { 75 | switch (Context.typeof(expr)) { 76 | case TType(_.get() => def, _): 77 | try { 78 | switch (Context.getType(def.module)) { 79 | case TInst(_.get() => clsType, _): 80 | if (!clsType.meta.has(react.jsx.JsxStaticMacro.META_NAME)) 81 | Context.error( 82 | 'Incompatible class for ReactType: expected a ReactComponent or a @:jsxStatic component', 83 | expr.pos 84 | ); 85 | else 86 | return macro { 87 | $expr.__jsxStatic; 88 | }; 89 | 90 | default: throw ''; 91 | } 92 | } catch (e:Dynamic) { 93 | Context.error( 94 | 'Incompatible expression for ReactType', 95 | expr.pos 96 | ); 97 | } 98 | 99 | default: 100 | Context.error( 101 | 'Incompatible expression for ReactType', 102 | expr.pos 103 | ); 104 | } 105 | 106 | return null; 107 | } 108 | } 109 | 110 | abstract ReactTypeOf(ReactType) to ReactType { 111 | private inline function new(node:ReactType) this = node; 112 | 113 | #if !macro 114 | @:from 115 | static public function fromFunctionWithProps(f:TProps->ReactFragment):ReactTypeOf 116 | { 117 | return new ReactTypeOf(f); 118 | } 119 | 120 | @:from 121 | static public function fromComp( 122 | cls:Class> 123 | ):ReactTypeOf 124 | { 125 | return new ReactTypeOf(cls); 126 | } 127 | 128 | @:from 129 | static public function fromFunctionWithoutProps(f:Void->ReactFragment):ReactTypeOf 130 | { 131 | return new ReactTypeOf(f); 132 | } 133 | 134 | @:from 135 | static public function fromCompWithoutProps( 136 | cls:Class> 137 | ):ReactTypeOf 138 | { 139 | return new ReactTypeOf(cls); 140 | } 141 | 142 | #if coconut_react_core 143 | public function fromHxx(hxxMeta:{ ?key:coconut.react.Key }, props:TProps):ReactElement 144 | { 145 | return React.createElement(this, js.Object.assign(cast props, hxxMeta)); 146 | } 147 | #end 148 | #end 149 | 150 | @:from 151 | static public macro function fromExpr(expr:Expr) 152 | { 153 | if (!isReactType(expr)) 154 | { 155 | Context.error('Incompatible expression for ReactType', expr.pos); 156 | return null; 157 | } 158 | 159 | switch (Context.getExpectedType()) { 160 | case TAbstract(_, [TType(_.get() => tProps, [])]): 161 | Context.error( 162 | 'Props do not unify with ${tProps.name}', 163 | expr.pos 164 | ); 165 | 166 | case TAbstract( 167 | _.toString() => 'Null', 168 | [TAbstract(_, [TType(_.get() => tProps, [])])] 169 | ): 170 | Context.error( 171 | 'Props do not unify with ${tProps.name}', 172 | expr.pos 173 | ); 174 | 175 | default: 176 | Context.error('Props do not unify', expr.pos); 177 | } 178 | 179 | return null; 180 | } 181 | 182 | #if macro 183 | static function isReactType(expr:Expr):Bool 184 | { 185 | try { 186 | Context.typeExpr(macro { var a:react.ReactType = $e{expr}; }); 187 | return true; 188 | } catch (e:Dynamic) { 189 | return false; 190 | } 191 | } 192 | #end 193 | } 194 | 195 | -------------------------------------------------------------------------------- /src/lib/react/ReactUIEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.Element; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/events.html 7 | **/ 8 | extern class ReactUIEvent extends ReactEvent 9 | { 10 | public var detail:Float; 11 | public var view:Element; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/react/ReactUtil.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent; 4 | 5 | typedef ChangesSummary = { 6 | @:optional var added:Array; 7 | @:optional var updated:Array; 8 | @:optional var deleted:Array; 9 | } 10 | 11 | class ReactUtil 12 | { 13 | public static function cx(arrayOrObject:Dynamic) 14 | { 15 | var array:Array>; 16 | if (Std.is(arrayOrObject, Array)) array = arrayOrObject; 17 | else array = [arrayOrObject]; 18 | var classes:Array = []; 19 | for (value in array) 20 | { 21 | if (value == null) continue; 22 | if (Std.is(value, String)) 23 | { 24 | classes.push(cast value); 25 | } 26 | else 27 | { 28 | for (field in Reflect.fields(value)) 29 | if (Reflect.field(value, field) == true) 30 | classes.push(field); 31 | } 32 | } 33 | return classes.join(' '); 34 | } 35 | 36 | public static function assign(target:Dynamic, sources:Array):Dynamic 37 | { 38 | for (source in sources) 39 | if (source != null) 40 | for (field in Reflect.fields(source)) 41 | Reflect.setField(target, field, Reflect.field(source, field)); 42 | return target; 43 | } 44 | 45 | public static function copy(source1:Dynamic, ?source2:Dynamic):Dynamic 46 | { 47 | var target = {}; 48 | for (field in Reflect.fields(source1)) 49 | Reflect.setField(target, field, Reflect.field(source1, field)); 50 | if (source2 != null) 51 | for (field in Reflect.fields(source2)) 52 | Reflect.setField(target, field, Reflect.field(source2, field)); 53 | return target; 54 | } 55 | 56 | public static function copyWithout(source1:Dynamic, source2:Dynamic, fields:Array) 57 | { 58 | var target = {}; 59 | for (field in Reflect.fields(source1)) 60 | if (!Lambda.has(fields, field)) 61 | Reflect.setField(target, field, Reflect.field(source1, field)); 62 | if (source2 != null) 63 | for (field in Reflect.fields(source2)) 64 | if (!Lambda.has(fields, field)) 65 | Reflect.setField(target, field, Reflect.field(source2, field)); 66 | return target; 67 | } 68 | 69 | public static function mapi(items:Array, map:Int -> A -> B):Array 70 | { 71 | if (items == null) return null; 72 | var newItems = []; 73 | for (i in 0...items.length) 74 | newItems.push(map(i, items[i])); 75 | return newItems; 76 | } 77 | 78 | /** 79 | Clone opaque children structure, providing additional props to merge: 80 | - as a object 81 | - or as a function (child->props) 82 | **/ 83 | public static function cloneChildren(children:ReactFragment, props:Dynamic):ReactFragment 84 | { 85 | if (Reflect.isFunction(props)) 86 | return React.Children.map(children, function(child) { 87 | if (!React.isValidElement(child)) return child; 88 | return React.cloneElement((cast child :ReactElement), props(child)); 89 | }); 90 | else 91 | return React.Children.map(children, function(child) { 92 | if (!React.isValidElement(child)) return child; 93 | return React.cloneElement((cast child :ReactElement), props); 94 | }); 95 | } 96 | 97 | /** 98 | https://facebook.github.io/react/docs/pure-render-mixin.html 99 | 100 | Implementing a simple shallow compare of next props and next state 101 | similar to the PureRenderMixin react addon 102 | **/ 103 | public static function shouldComponentUpdate(component:Dynamic, nextProps:Dynamic, nextState:Dynamic):Bool 104 | { 105 | return !shallowCompare(component.props, nextProps) || !shallowCompare(component.state, nextState); 106 | } 107 | 108 | public static function shallowCompare(a:Dynamic, b:Dynamic):Bool 109 | { 110 | var aFields = Reflect.fields(a); 111 | var bFields = Reflect.fields(b); 112 | if (aFields.length != bFields.length) 113 | return false; 114 | for (field in aFields) 115 | if (!Reflect.hasField(b, field) || Reflect.field(b, field) != Reflect.field(a, field)) 116 | return false; 117 | return true; 118 | } 119 | 120 | public static function shallowChanges( 121 | obj:T, 122 | obj2:T, 123 | ?ignoreEqual:Bool 124 | ):Null { 125 | var keys1 = getKeys(obj); 126 | var keys2 = getKeys(obj2); 127 | 128 | var added = []; 129 | var deleted = []; 130 | var updated = []; 131 | var hasRet = false; 132 | 133 | for (i in 0...keys1.length) { 134 | var key = keys1[i]; 135 | 136 | if (Lambda.has(keys2, key)) { 137 | if (Reflect.field(obj, key) != Reflect.field(obj2, key)) { 138 | updated.push(key); 139 | hasRet = true; 140 | } 141 | } else { 142 | deleted.push(key); 143 | hasRet = true; 144 | } 145 | } 146 | 147 | for (i in 0...keys2.length) { 148 | var key = keys2[i]; 149 | 150 | if (!Lambda.has(keys1, key)) { 151 | added.push(key); 152 | hasRet = true; 153 | } 154 | } 155 | 156 | if (!hasRet) return null; 157 | var ret:ChangesSummary = {}; 158 | if (added.length > 0) ret.added = added; 159 | if (deleted.length > 0) ret.deleted = deleted; 160 | if (updated.length > 0) ret.updated = updated; 161 | return ret; 162 | } 163 | 164 | public static function getKeys(obj:T):Array { 165 | return Reflect.fields(obj).filter(k -> Reflect.field(obj, k) != null); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/lib/react/ReactWheelEvent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import js.html.Element; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/events.html 7 | **/ 8 | extern class ReactWheelEvent extends ReactEvent 9 | { 10 | public var deltaMode:Float; 11 | public var deltaX:Float; 12 | public var deltaY:Float; 13 | public var deltaZ:Float; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/react/StrictMode.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.BaseProps; 4 | import react.ReactComponent; 5 | 6 | /** 7 | See https://reactjs.org/docs/strict-mode.html 8 | */ 9 | #if (!react_global) 10 | @:jsRequire("react", "StrictMode") 11 | #end 12 | @:native('React.StrictMode') 13 | extern class StrictMode extends ReactComponentOfProps {} 14 | 15 | -------------------------------------------------------------------------------- /src/lib/react/Suspense.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactFragment; 4 | 5 | typedef SuspenseProps = { 6 | var fallback:ReactFragment; 7 | @:optional var children:ReactFragment; 8 | } 9 | 10 | /** 11 | https://react.dev/reference/react/Suspense 12 | 13 | `` lets you display a fallback until its children have finished 14 | loading. 15 | **/ 16 | #if (!react_global) 17 | @:jsRequire("react", "Suspense") 18 | #end 19 | @:native('React.Suspense') 20 | extern class Suspense extends ReactComponent {} 21 | -------------------------------------------------------------------------------- /src/lib/react/jsx/AriaAttributes.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | #if macro 4 | import haxe.macro.Expr.ComplexType; 5 | #end 6 | 7 | class AriaAttributes { 8 | #if macro 9 | static public var map:Map = [ 10 | "aria-activedescendant" => macro :react.jsx.AriaAttributes.IdReference, 11 | "aria-atomic" => macro :Bool, 12 | "aria-autocomplete" => macro :react.jsx.AriaAttributes.AriaAutoComplete, 13 | "aria-busy" => macro :Bool, 14 | "aria-checked" => macro :react.jsx.AriaAttributes.TriState, 15 | "aria-colcount" => macro :Int, 16 | "aria-colindex" => macro :Int, 17 | "aria-colspan" => macro :Int, 18 | "aria-controls" => macro :react.jsx.AriaAttributes.IdReferenceList, 19 | "aria-current" => macro :react.jsx.AriaAttributes.AriaCurrent, 20 | "aria-describedby" => macro:react.jsx.AriaAttributes.IdReferenceList, 21 | "aria-details" => macro :react.jsx.AriaAttributes.IdReference, 22 | "aria-disabled" => macro :Bool, 23 | "aria-errormessage" => macro :react.jsx.AriaAttributes.IdReference, 24 | "aria-expanded" => macro :Bool, 25 | "aria-flowto" => macro :react.jsx.AriaAttributes.IdReferenceList, 26 | "aria-haspopup" => macro :react.jsx.AriaAttributes.AriaHasPopup, 27 | "aria-hidden" => macro :Bool, 28 | "aria-invalid" => macro :react.jsx.AriaAttributes.AriaInvalid, 29 | "aria-keyshortcuts" => macro :String, 30 | "aria-label" => macro :String, 31 | "aria-labelledby" => macro :react.jsx.AriaAttributes.IdReferenceList, 32 | "aria-level" => macro :Int, 33 | "aria-live" => macro :react.jsx.AriaAttributes.AriaLive, 34 | "aria-modal" => macro :Bool, 35 | "aria-multiline" => macro :Bool, 36 | "aria-multiselectable" => macro :Bool, 37 | "aria-orientation" => macro :react.jsx.AriaAttributes.AriaOrientation, 38 | "aria-owns" => macro :react.jsx.AriaAttributes.IdReferenceList, 39 | "aria-placeholder" => macro :String, 40 | "aria-posinset" => macro :Int, 41 | "aria-pressed" => macro :react.jsx.AriaAttributes.TriState, 42 | "aria-readonly" => macro :Bool, 43 | "aria-relevant" => macro :react.jsx.AriaAttributes.AriaRelevant, 44 | "aria-required" => macro :Bool, 45 | "aria-roledescription" => macro :String, 46 | "aria-rowcount" => macro :Int, 47 | "aria-rowindex" => macro :Int, 48 | "aria-rowspan" => macro :Int, 49 | "aria-selected" => macro :Bool, 50 | "aria-setsize" => macro :Int, 51 | "aria-sort" => macro :react.jsx.AriaAttributes.AriaSort, 52 | "aria-valuemax" => macro :Float, 53 | "aria-valuemin" => macro :Float, 54 | "aria-valuenow" => macro :Float, 55 | "aria-valuetext" => macro :String 56 | ]; 57 | #end 58 | } 59 | 60 | typedef IdReference = String; 61 | typedef IdReferenceList = String; 62 | 63 | enum abstract TriState(Dynamic) from Bool { 64 | var Mixed = "mixed"; 65 | } 66 | 67 | enum abstract AriaAutoComplete(String) { 68 | var Inline = "inline"; 69 | var List = "list"; 70 | var Both = "both"; 71 | var None = "none"; 72 | } 73 | 74 | enum abstract AriaCurrent(Dynamic) from Bool { 75 | var Page = "page"; 76 | var Step = "step"; 77 | var Location = "location"; 78 | var Date = "date"; 79 | var Time = "time"; 80 | } 81 | 82 | enum abstract AriaHasPopup(Dynamic) from Bool { 83 | var Menu = "menu"; 84 | var Listbox = "listbox"; 85 | var Tree = "tree"; 86 | var Grid = "grid"; 87 | var Dialog = "dialog"; 88 | } 89 | 90 | enum abstract AriaInvalid(Dynamic) from Bool { 91 | var Grammar = "grammar"; 92 | var Spelling = "spelling"; 93 | } 94 | 95 | enum abstract AriaLive(String) { 96 | var Assertive = "assertive"; 97 | var Off = "off"; 98 | var Polite = "polite"; 99 | } 100 | 101 | enum abstract AriaOrientation(String) { 102 | var Horizontal = "horizontal"; 103 | var Vertical = "vertical"; 104 | } 105 | 106 | enum abstract AriaRelevant(String) { 107 | var Additions = "additions"; 108 | var AdditionsText = "additions text"; 109 | var All = "all"; 110 | var Removals = "removals"; 111 | var Text = "text"; 112 | } 113 | 114 | enum abstract AriaSort(String) { 115 | var Ascending = "ascending"; 116 | var Descending = "descending"; 117 | var None = "none"; 118 | var Other = "other"; 119 | } 120 | -------------------------------------------------------------------------------- /src/lib/react/jsx/JsxLiteral.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | #if macro 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import react.macro.ReactComponentMacro; 7 | 8 | #if (haxe_ver < 4) 9 | private typedef ObjectField = {field:String, expr:Expr}; 10 | #end 11 | 12 | class JsxLiteral { 13 | static public function genLiteral(type:Expr, props:Expr, ref:Expr, key:Expr, pos:Position) 14 | { 15 | if (key == null) key = macro null; 16 | if (ref == null) ref = macro null; 17 | 18 | var fields:Array = [ 19 | #if haxe4 20 | {field: "$$typeof", quotes: Quoted, expr: macro js.Syntax.code("$$tre")}, 21 | #else 22 | {field: "@$__hx__$$typeof", expr: macro untyped __js__("$$tre")}, 23 | #end 24 | {field: 'type', expr: macro (${type} : react.ReactType)}, 25 | {field: 'props', expr: props} 26 | ]; 27 | 28 | if (key != null) fields.push({field: 'key', expr: key}); 29 | if (ref != null) fields.push({field: 'ref', expr: ref}); 30 | var obj = {expr: EObjectDecl(fields), pos: pos}; 31 | 32 | return macro @:pos(pos) ($obj : react.ReactComponent.ReactElement); 33 | } 34 | 35 | static public function canUseLiteral(typeInfo:ComponentInfo, ref:Expr) 36 | { 37 | #if (!react_force_inline && (debug || react_no_inline)) 38 | return false; 39 | #end 40 | 41 | // do not use literals for externs: we don't know their defaultProps 42 | if (typeInfo != null && typeInfo.isExtern) return false; 43 | 44 | // no ref is always ok 45 | if (ref == null) return true; 46 | 47 | // only refs as functions are allowed in literals, strings require the full createElement context 48 | return switch (Context.typeof(ref)) { 49 | case TFun(_): true; 50 | default: false; 51 | } 52 | } 53 | } 54 | #end 55 | -------------------------------------------------------------------------------- /src/lib/react/jsx/JsxMacro.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.ExprTools; 5 | import haxe.macro.Type; 6 | 7 | @:dce 8 | class JsxMacro 9 | { 10 | static public function handleMarkup(inClass:ClassType, fields:Array):Array 11 | { 12 | if (inClass.isExtern) return fields; 13 | 14 | for (f in fields) 15 | if (StringTools.startsWith(f.name, 'render')) 16 | wrapMarkupInField(f); 17 | 18 | return fields; 19 | } 20 | 21 | static public function wrapMarkupInField(f:Field) 22 | { 23 | switch (f.kind) { 24 | case FFun({expr: e, ret: ret, params: params, args: args}): 25 | function wrap(e:Expr) { 26 | return switch(e.expr) { 27 | // Replace markup by a jsx call 28 | case EMeta({name: ':markup'}, e): 29 | macro @:pos(e.pos) react.ReactMacro.jsx(@:markup $e); 30 | 31 | // Do not iterate inside function calls to avoid jsx calls of all sorts 32 | case ECall(ecall, params): e; 33 | 34 | // Iterate recursively 35 | default: ExprTools.map(e, wrap); 36 | } 37 | } 38 | 39 | f.kind = FFun({ 40 | expr: ExprTools.map(e, wrap), 41 | ret: ret, 42 | params: params, 43 | args: args 44 | }); 45 | 46 | default: 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/react/jsx/JsxPropsBuilder.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | #if macro 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.TypeTools; 7 | 8 | #if (haxe_ver < 4) 9 | private typedef ObjectField = {field:String, expr:Expr}; 10 | #end 11 | 12 | class JsxPropsBuilder 13 | { 14 | 15 | static public function makeProps(spread:Array, attrs:Array, pos:Position) 16 | { 17 | #if (!debug && !react_no_inline) 18 | flattenSpreadProps(spread, attrs); 19 | #end 20 | 21 | return spread.length > 0 22 | ? makeSpread(spread, attrs, pos) 23 | : attrs.length == 0 ? macro {} : {pos:pos, expr:EObjectDecl(attrs)} 24 | } 25 | 26 | /** 27 | * Attempt flattening spread/default props into the user-defined props 28 | */ 29 | static function flattenSpreadProps(spread:Array, attrs:Array) 30 | { 31 | function hasAttr(name:String) { 32 | for (prop in attrs) if (prop.field == name) return true; 33 | return false; 34 | } 35 | var mergeProps = getSpreadProps(spread, []); 36 | if (mergeProps.length > 0) 37 | { 38 | for (prop in mergeProps) 39 | if (!hasAttr(prop.field)) attrs.push(prop); 40 | } 41 | } 42 | 43 | static function makeSpread(spread:Array, attrs:Array, pos:Position) 44 | { 45 | // single spread, no props 46 | if (spread.length == 1 && attrs.length == 0) 47 | return spread[0]; 48 | 49 | // combine using Object.assign 50 | var args = [macro {}].concat(spread); 51 | if (attrs.length > 0) args.push({pos:pos, expr:EObjectDecl(attrs)}); 52 | return macro (untyped Object).assign($a{args}); 53 | } 54 | 55 | /** 56 | * Flatten literal objects into the props 57 | */ 58 | static function getSpreadProps(spread:Array, props:Array) 59 | { 60 | if (spread.length == 0) return props; 61 | var last = spread[spread.length - 1]; 62 | return switch (last.expr) { 63 | case ECheckType({expr: EObjectDecl(fields)}, ct): 64 | spread.pop(); 65 | var newProps = props.concat(fields.map(function(f) { 66 | var fname = f.field; 67 | var fct = TypeTools.toComplexType( 68 | Context.typeof( 69 | macro @:pos(f.expr.pos) (null :$ct).$fname 70 | ) 71 | ); 72 | 73 | return { 74 | expr: {expr: ECheckType(f.expr, fct), pos: f.expr.pos}, 75 | field: fname, 76 | #if (haxe_ver >= 4) quotes: f.quotes #end 77 | } 78 | })); 79 | // push props and recurse in case another literal object is in the list 80 | getSpreadProps(spread, newProps); 81 | 82 | case EObjectDecl(fields): 83 | spread.pop(); 84 | var newProps = props.concat(fields); 85 | // push props and recurse in case another literal object is in the list 86 | getSpreadProps(spread, newProps); 87 | default: 88 | props; 89 | } 90 | } 91 | } 92 | #end 93 | -------------------------------------------------------------------------------- /src/lib/react/jsx/JsxStaticMacro.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import react.macro.MacroUtil; 7 | 8 | using haxe.macro.Tools; 9 | 10 | private typedef JsxStaticDecl = { 11 | var module:String; 12 | var className:String; 13 | var displayName:String; 14 | var fieldName:String; 15 | } 16 | 17 | private enum MetaValueType { 18 | NoMeta; 19 | NoParams(meta:MetadataEntry); 20 | WithParams(meta:MetadataEntry, params:Array); 21 | } 22 | 23 | @:dce 24 | class JsxStaticMacro 25 | { 26 | static public inline var DISALLOW_IN_REACT_COMPONENT_BUILDER = 'JsxStaticDisallowInReactComponent'; 27 | static public inline var META_NAME = ':jsxStatic'; 28 | static public inline var FIELD_NAME = '__jsxStatic'; 29 | 30 | static var decls:Array = []; 31 | 32 | static public function build():Array 33 | { 34 | var cls = Context.getLocalClass(); 35 | if (cls == null) return null; 36 | var inClass = cls.get(); 37 | 38 | if (inClass.meta.has(META_NAME)) 39 | { 40 | var fields = Context.getBuildFields(); 41 | var proxyName = extractMetaString(inClass.meta, META_NAME); 42 | if (proxyName == null) return null; 43 | 44 | var metaPos = inClass.meta.extract(META_NAME)[0].pos; 45 | var pos = inClass.meta.extract(META_NAME)[0].params[0].pos; 46 | 47 | for (f in fields) { 48 | if (f.name == FIELD_NAME) return null; 49 | 50 | #if (haxe4 && react_auto_jsx) 51 | if (f.name == proxyName) react.jsx.JsxMacro.wrapMarkupInField(f); 52 | #end 53 | } 54 | 55 | fields.push({ 56 | access: [APublic, AStatic], 57 | name: FIELD_NAME, 58 | #if react_check_jsxstatic_type 59 | kind: FVar(macro :react.ReactType, macro @:pos(pos) $i{proxyName}), 60 | #else 61 | kind: FVar(null, macro $i{proxyName}), 62 | #end 63 | doc: null, 64 | meta: null, 65 | pos: metaPos 66 | }); 67 | 68 | return fields; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | static public function disallowInReactComponent( 75 | inClass:ClassType, 76 | fields:Array 77 | ):Array { 78 | if (inClass.meta.has(META_NAME)) 79 | Context.error( 80 | '@${META_NAME} cannot be used on ReactComponent classes.', 81 | inClass.meta.extract(META_NAME)[0].pos 82 | ); 83 | 84 | return fields; 85 | } 86 | 87 | static public function addHook() 88 | { 89 | // Add hook to generate __init__ at the end of the compilation 90 | Context.onAfterTyping(afterTypingHook); 91 | } 92 | 93 | static public function injectDisplayNames(type:Expr) 94 | { 95 | #if !debug 96 | return; 97 | #end 98 | 99 | switch (Context.typeExpr(type).expr) { 100 | case TConst(TString(_)): 101 | // HTML component, nothing to do 102 | 103 | case TTypeExpr(TClassDecl(_)): 104 | // ReactComponent, should already handle its displayName 105 | 106 | case TField(_, FStatic(clsTypeRef, _.get() => {kind: FMethod(_), name: fieldName})): 107 | var clsType = clsTypeRef.get(); 108 | var displayName = handleJsxStaticMeta(clsType, fieldName); 109 | 110 | addDisplayNameDecl({ 111 | module: clsType.module, 112 | className: clsType.name, 113 | displayName: displayName, 114 | fieldName: fieldName 115 | }); 116 | 117 | case TCall({expr: TField(_, FStatic(clsTypeRef, clsField))}, _): 118 | var clsType = clsTypeRef.get(); 119 | var fieldName = clsField.get().name; 120 | var displayName = StringTools.startsWith(fieldName, 'get_') 121 | ? handleJsxStaticMeta(clsType, fieldName.substr(4)) 122 | : fieldName; 123 | 124 | addDisplayNameDecl({ 125 | module: clsType.module, 126 | className: clsType.name, 127 | displayName: displayName, 128 | fieldName: fieldName 129 | }); 130 | 131 | case TLocal({name: varName}): 132 | // Local vars not handled at the moment 133 | 134 | case TField(_, FInstance(_, _, _)): 135 | // Instance fields not handled at the moment 136 | 137 | case TField(_, FStatic(_, _)): 138 | // Static variables not handled at the moment 139 | 140 | default: 141 | // Unknown type, not handled 142 | // trace(typedExpr); 143 | } 144 | } 145 | 146 | static public function handleJsxStaticProxy(type:Expr) 147 | { 148 | var typedExpr = Context.typeExpr(type); 149 | 150 | switch (typedExpr.expr) 151 | { 152 | case TTypeExpr(TClassDecl(_.get() => c)): 153 | if (c.meta.has(META_NAME)) 154 | type.expr = EField( 155 | {expr: EConst(CIdent(c.name)), pos: type.pos}, 156 | extractMetaString(c.meta, META_NAME) 157 | ); 158 | 159 | default: 160 | } 161 | } 162 | 163 | static function extractMeta(meta:MetaAccess, name:String):MetaValueType 164 | { 165 | if (!meta.has(name)) return NoMeta; 166 | 167 | var metas = meta.extract(name); 168 | if (metas.length == 0) return NoMeta; 169 | 170 | var meta = metas.pop(); 171 | var params = meta.params; 172 | if (params.length == 0) return NoParams(meta); 173 | 174 | return WithParams(meta, params); 175 | } 176 | 177 | static public function extractMetaString(meta:MetaAccess, name:String):String 178 | { 179 | return switch(extractMeta(meta, name)) { 180 | case NoMeta: null; 181 | 182 | case WithParams(_, params): 183 | var param = params[0]; 184 | var name = MacroUtil.extractMetaString(param); 185 | if (name == null) { 186 | Context.fatalError( 187 | '@${META_NAME}: invalid parameter. Expected static function name.', 188 | param.pos 189 | ); 190 | } 191 | name; 192 | 193 | case NoParams(meta): 194 | Context.fatalError( 195 | 'Parameter required for @${META_NAME}(nameOfStaticFunction)', 196 | meta.pos 197 | ); 198 | }; 199 | } 200 | 201 | static function handleJsxStaticMeta(clsType:ClassType, displayName:String) 202 | { 203 | var jsxStatic = extractMetaString(clsType.meta, META_NAME); 204 | if (jsxStatic != null && jsxStatic == displayName) return clsType.name; 205 | return displayName; 206 | } 207 | 208 | static function addDisplayNameDecl(decl:JsxStaticDecl) 209 | { 210 | var previousDecl = Lambda.find(decls, function(d) { 211 | return d.module == decl.module 212 | && d.className == decl.className 213 | && d.fieldName == decl.fieldName; 214 | }); 215 | 216 | if (previousDecl == null) decls.push(decl); 217 | } 218 | 219 | static var initModuleGenerated:Bool = false; 220 | 221 | static function afterTypingHook(_:Array) 222 | { 223 | if (initModuleGenerated) return; 224 | var initModule = "JsxStaticInit__"; 225 | initModuleGenerated = true; 226 | 227 | var exprs = decls.map(function(decl) { 228 | var fName = decl.fieldName; 229 | return macro { 230 | untyped $i{decl.className}.$fName.displayName = 231 | $i{decl.className}.$fName.displayName || $v{decl.displayName}; 232 | }; 233 | }); 234 | 235 | var cls = macro class $initModule { 236 | static function __init__() { 237 | $a{exprs}; 238 | } 239 | }; 240 | 241 | var imports = decls.map(function(decl) return generatePath(decl.module)); 242 | Context.defineModule(initModule, [cls], imports); 243 | } 244 | 245 | static function generatePath(module:String) 246 | { 247 | var parts = module.split('.'); 248 | 249 | return { 250 | mode: ImportMode.INormal, 251 | path: parts.map(function(part) return {pos: (macro null).pos, name: part}) 252 | }; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/lib/react/macro/ContextMacro.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.TypeTools; 7 | 8 | class ContextMacro { 9 | public static inline var REACT_CONTEXT_BUILDER = 'ReactContext'; 10 | 11 | public static function buildComponent(inClass:ClassType, fields:Array):Array 12 | { 13 | if (inClass.isExtern) return fields; 14 | if (!inClass.meta.has(ReactMeta.ContextMeta)) return fields; 15 | 16 | var meta = inClass.meta.extract(ReactMeta.ContextMeta); 17 | for (i in 1...meta.length) { 18 | Context.warning( 19 | 'You can only use one @${ReactMeta.ContextMeta} meta per' 20 | + ' component; only the first one will apply.', 21 | meta[i].pos 22 | ); 23 | } 24 | 25 | var first = meta[0]; 26 | if (first.params.length != 1) { 27 | Context.error( 28 | '@${ReactMeta.ContextMeta} expects an argument: the instance of' 29 | + ' the context provider', 30 | first.pos 31 | ); 32 | } 33 | 34 | var contextProvider = first.params[0]; 35 | var ctContext = switch (Context.typeof(contextProvider)) { 36 | case TAbstract(_.toString() => "react.ReactContext", [t]): 37 | TypeTools.toComplexType(t); 38 | 39 | case _: 40 | Context.error( 41 | 'Context provider should be of type react.ReactContext', 42 | contextProvider.pos 43 | ); 44 | }; 45 | 46 | return fields.concat((macro class { 47 | @:keep static var contextType = $contextProvider; 48 | var context:$ctContext; 49 | }).fields); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/react/macro/MacroUtil.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.ComplexTypeTools; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | import haxe.macro.TypeTools; 8 | 9 | using haxe.macro.Tools; 10 | 11 | typedef ComponentTypes = { 12 | @:optional var tprops:ComplexType; 13 | @:optional var tstate:ComplexType; 14 | } 15 | 16 | @:dce 17 | class MacroUtil { 18 | static public function isEmpty(type:ComplexType):Bool 19 | { 20 | return switch (type) { 21 | case TPath({name: "Dynamic", sub: null, pack: [], params: []}): 22 | true; 23 | 24 | case TPath({name: "Empty", sub: null, pack: ["react"], params: []}): 25 | true; 26 | 27 | default: 28 | false; 29 | }; 30 | } 31 | 32 | static public function toFieldExpr(sl:Array, pos:Position = null):Expr 33 | { 34 | if (pos == null) pos = Context.currentPos(); 35 | 36 | return Lambda.fold( 37 | sl, 38 | function(s, e) { 39 | return e == null 40 | ? (macro @:pos(pos) $i{s}) 41 | : (macro @:pos(pos) $e.$s); 42 | }, 43 | null 44 | ); 45 | } 46 | 47 | // TODO: add some cache 48 | static public function extractComponentTypes(inClass:ClassType):ComponentTypes 49 | { 50 | var ret = {tprops: null, tstate: null}; 51 | 52 | switch (inClass.superClass) 53 | { 54 | case {params: params, t: _.toString() => cls} 55 | if (cls == 'react.ReactComponentOf' || cls == 'react.PureComponentOf'): 56 | ret.tprops = TypeTools.toComplexType(params[0]); 57 | ret.tstate = TypeTools.toComplexType(params[1]); 58 | 59 | default: 60 | } 61 | 62 | return ret; 63 | } 64 | 65 | static public function testType(t:Type):Bool 66 | { 67 | if (t == null) return false; 68 | 69 | return try { 70 | var ct = TypeTools.toComplexType(t); 71 | testComplexType(ct); 72 | } catch (e:Dynamic) { 73 | false; 74 | } 75 | } 76 | 77 | static public function testComplexType(ct:ComplexType):Bool 78 | { 79 | if (ct == null) return false; 80 | 81 | return try { 82 | compileComplexType(ct); 83 | true; 84 | } catch (e:Dynamic) { 85 | false; 86 | } 87 | } 88 | 89 | static function compileType(t:Type):Void 90 | { 91 | compileComplexType(t.toComplexType()); 92 | } 93 | 94 | static function compileComplexType(ct:ComplexType):Void 95 | { 96 | Context.typeExpr(macro (null :$ct)); 97 | } 98 | 99 | static public function tryFollow(t:Type):Type 100 | { 101 | return try { 102 | var t1 = t.follow(); 103 | compileType(t1); 104 | t1; 105 | } catch (e:Dynamic) { 106 | null; 107 | }; 108 | } 109 | 110 | static public function tryMapFollow(t:Type):Type 111 | { 112 | return try { 113 | var t1 = t.map(function(t) return t.follow()); 114 | compileType(t1); 115 | t1; 116 | } catch (e:Dynamic) { 117 | null; 118 | }; 119 | } 120 | 121 | static public function getField(fields:Array, name:String) 122 | { 123 | for (field in fields) 124 | if (field.name == name) 125 | return field; 126 | 127 | return null; 128 | } 129 | 130 | static public function extractMetaString(metaExpr:Expr):Null 131 | { 132 | return switch (metaExpr.expr) { 133 | case EConst(CString(str)): str; 134 | case EConst(CIdent(ident)): ident; 135 | default: null; 136 | }; 137 | } 138 | 139 | static public function functionToType(fun:haxe.macro.Function):Type 140 | { 141 | return TFun(fun.args.map(funArgToTFunArg), ComplexTypeTools.toType(fun.ret)); 142 | } 143 | 144 | static public function funArgToTFunArg(arg:FunctionArg):{t:Type, opt:Bool, name:String} 145 | { 146 | return { 147 | t: ComplexTypeTools.toType(arg.type), 148 | opt: arg.opt, 149 | name: arg.name 150 | }; 151 | } 152 | } 153 | 154 | class PositionTools { 155 | public static function or(pos:Position, fallback:Position):Position { 156 | if (pos == null) return fallback; 157 | if (haxe.macro.PositionTools.getInfos(pos).min == -1) return fallback; 158 | return pos; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/lib/react/macro/PropsValidator.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.Expr; 4 | 5 | typedef Validator = String->Expr->Null; 6 | 7 | class PropsValidator { 8 | static var validators:Map = new Map(); 9 | 10 | public static function get(name:String):Null { 11 | return validators.get(name); 12 | } 13 | 14 | public static function register(name:String, validator:Validator) { 15 | // Note: no warning/error on name collision atm 16 | // because a registration can happen twice with compilation server 17 | // TODO: properly handle that (with haxe 4 final?) 18 | validators.set(name, validator); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/react/macro/PureComponentMacro.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.TypeTools; 7 | 8 | import react.macro.MacroUtil.*; 9 | 10 | @:dce 11 | class PureComponentMacro 12 | { 13 | static public inline var PURE_COMPONENT_BUILDER = 'PureComponent'; 14 | @:deprecated static public inline var PURE_META = ReactMeta.PureComponent; 15 | 16 | static public function buildComponent(inClass:ClassType, fields:Array):Array 17 | { 18 | if (inClass.meta.has(ReactMeta.PureComponentInjected)) return fields; 19 | if (!inClass.meta.has(ReactMeta.PureComponent)) return fields; 20 | 21 | if (getField(fields, 'shouldComponentUpdate') == null) { 22 | var propsType:Type = TDynamic(null); 23 | var stateType:Type = TDynamic(null); 24 | 25 | switch (inClass.superClass) 26 | { 27 | case {params: params, t: _.toString() => cls} 28 | if (cls == 'react.ReactComponentOf' || cls == 'react.PureComponentOf'): 29 | propsType = params[0]; 30 | stateType = params[1]; 31 | 32 | default: 33 | } 34 | 35 | var propsCT:ComplexType = TypeTools.toComplexType(propsType); 36 | var stateCT:ComplexType = TypeTools.toComplexType(stateType); 37 | 38 | var hasProps = !isEmpty(propsCT); 39 | var hasState = !isEmpty(stateCT); 40 | 41 | var shouldUpdateExpr:Expr = null; 42 | 43 | if (hasProps && hasState) 44 | shouldUpdateExpr = exprShouldUpdatePropsAndState(); 45 | else if (hasProps) 46 | shouldUpdateExpr = exprShouldUpdateProps(); 47 | else if (hasState) 48 | shouldUpdateExpr = exprShouldUpdateState(); 49 | else 50 | shouldUpdateExpr = exprShouldNeverUpdate(); 51 | 52 | addShouldUpdate(fields, propsCT, stateCT, shouldUpdateExpr); 53 | } 54 | 55 | return fields; 56 | } 57 | 58 | static function addShouldUpdate( 59 | fields:Array, 60 | propsType:ComplexType, 61 | stateType:ComplexType, 62 | shouldUpdateExpr:Expr 63 | ) { 64 | var args:Array = [ 65 | {name: 'nextProps', type: propsType, opt: false, value: null}, 66 | {name: 'nextState', type: stateType, opt: false, value: null} 67 | ]; 68 | 69 | var fun = { 70 | args: args, 71 | ret: macro :Bool, 72 | expr: shouldUpdateExpr 73 | }; 74 | 75 | fields.push({ 76 | name: 'shouldComponentUpdate', 77 | access: [APublic, AOverride], 78 | kind: FFun(fun), 79 | pos: Context.currentPos() 80 | }); 81 | } 82 | 83 | static function exprShouldNeverUpdate() 84 | { 85 | return macro { 86 | return false; 87 | }; 88 | } 89 | 90 | static function exprShouldUpdateProps() 91 | { 92 | return macro { 93 | return !react.ReactUtil.shallowCompare(props, nextProps); 94 | }; 95 | } 96 | 97 | static function exprShouldUpdateState() 98 | { 99 | return macro { 100 | return !react.ReactUtil.shallowCompare(state, nextState); 101 | }; 102 | } 103 | 104 | static function exprShouldUpdatePropsAndState() 105 | { 106 | return macro { 107 | return !react.ReactUtil.shallowCompare(state, nextState) 108 | || !react.ReactUtil.shallowCompare(props, nextProps); 109 | }; 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/lib/react/macro/ReactDebugMacro.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | #if macro 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | import haxe.macro.TypeTools; 8 | 9 | import react.macro.MacroUtil.isEmpty; 10 | #end 11 | 12 | @:dce 13 | class ReactDebugMacro 14 | { 15 | public static inline var REACT_DEBUG_BUILDER = 'ReactDebug'; 16 | public static var firstRenderWarning:Bool = true; 17 | 18 | @:deprecated public static inline var IGNORE_RENDER_WARNING_META = ReactMeta.IgnoreRenderWarning; 19 | @:deprecated public static inline var WHY_RENDER_META = ReactMeta.WhyRender; 20 | 21 | #if macro 22 | public static function buildComponent(inClass:ClassType, fields:Array):Array 23 | { 24 | if (inClass.isExtern) return fields; 25 | 26 | var pos = Context.currentPos(); 27 | var propsType = macro :Dynamic; 28 | var stateType = macro :Dynamic; 29 | 30 | switch (inClass.superClass) { 31 | case {params: params, t: _.toString() => cls} 32 | if (cls == 'react.ReactComponentOf' || cls == 'react.PureComponentOf'): 33 | propsType = TypeTools.toComplexType(params[0]); 34 | stateType = TypeTools.toComplexType(params[1]); 35 | 36 | default: 37 | } 38 | 39 | #if !react_runtime_warnings_ignore_rerender 40 | if (!inClass.meta.has(ReactMeta.IgnoreRenderWarning)) 41 | if (!updateComponentUpdate(fields, inClass, propsType, stateType)) 42 | addComponentUpdate(fields, inClass, propsType, stateType); 43 | #end 44 | 45 | if (!isEmpty(stateType) && !updateConstructor(fields, inClass)) 46 | addConstructor(fields, inClass, propsType, stateType); 47 | 48 | return fields; 49 | } 50 | 51 | static function updateConstructor(fields:Array, inClass:ClassType) { 52 | for (field in fields) 53 | { 54 | if (field.name == "new") 55 | { 56 | switch (field.kind) { 57 | case FFun(f): 58 | f.expr = macro @:pos(field.pos) @:mergeBlock { 59 | @:mergeBlock ${f.expr}; 60 | @:mergeBlock ${exprConstructor(inClass)}; 61 | }; 62 | 63 | return true; 64 | default: 65 | } 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | static function addConstructor( 73 | fields:Array, 74 | inClass:ClassType, 75 | propsType:ComplexType, 76 | stateType:ComplexType 77 | ) { 78 | var constructor = { 79 | args: [ 80 | { 81 | meta: [], 82 | name: "props", 83 | type: propsType, 84 | opt: false, 85 | value: null 86 | } 87 | ], 88 | ret: macro :Void, 89 | expr: macro { 90 | super(props); 91 | ${exprConstructor(inClass)}; 92 | } 93 | } 94 | 95 | fields.push({ 96 | name: 'new', 97 | access: [APublic], 98 | kind: FFun(constructor), 99 | pos: inClass.pos 100 | }); 101 | } 102 | 103 | static function exprConstructor(inClass:ClassType) 104 | { 105 | return macro { 106 | if (state == null) { 107 | js.Browser.console.error($v{ 108 | 'Warning: component ${inClass.name} is stateful but its ' 109 | + '`state` is not initialized inside its constructor.\n\n' 110 | 111 | + 'Either add a `state = { ... }` statement to its constructor ' 112 | + 'or define this component as a `ReactComponentOfProps` ' 113 | + 'if it is only using `props`.\n\n' 114 | 115 | + 'If it is using neither `props` nor `state`, you might ' 116 | + 'consider using `@:jsxStatic` to avoid unneeded lifecycle. ' 117 | + 'See https://github.com/kLabz/haxe-react/blob/next/doc/static-components.md ' 118 | + 'for more information on static components.' 119 | }); 120 | } 121 | }; 122 | } 123 | 124 | static function updateComponentUpdate( 125 | fields:Array, 126 | inClass:ClassType, 127 | propsType:ComplexType, 128 | stateType:ComplexType 129 | ) { 130 | for (field in fields) 131 | { 132 | if (field.name == "componentDidUpdate") 133 | { 134 | switch (field.kind) { 135 | case FFun(f): 136 | if (f.args.length != 2) 137 | return Context.error('componentDidUpdate should accept two arguments', inClass.pos); 138 | 139 | var expr = exprComponentDidUpdate( 140 | inClass, 141 | f.args[0].name, 142 | f.args[1].name, 143 | !isEmpty(propsType), 144 | !isEmpty(stateType) 145 | ); 146 | 147 | f.expr = macro { 148 | ${expr} 149 | ${f.expr} 150 | }; 151 | 152 | return true; 153 | default: 154 | } 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | 161 | static function addComponentUpdate( 162 | fields:Array, 163 | inClass:ClassType, 164 | propsType:ComplexType, 165 | stateType:ComplexType 166 | ) { 167 | var expr = exprComponentDidUpdate( 168 | inClass, 169 | "prevProps", 170 | "prevState", 171 | !isEmpty(propsType), 172 | !isEmpty(stateType) 173 | ); 174 | 175 | var componentDidUpdate = { 176 | args: [ 177 | { 178 | meta: [], 179 | name: "prevProps", 180 | type: propsType, 181 | opt: false, 182 | value: null 183 | }, 184 | { 185 | meta: [], 186 | name: "prevState", 187 | type: stateType, 188 | opt: false, 189 | value: null 190 | } 191 | ], 192 | ret: macro :Void, 193 | expr: expr 194 | } 195 | 196 | fields.push({ 197 | name: 'componentDidUpdate', 198 | access: [APublic, AOverride], 199 | kind: FFun(componentDidUpdate), 200 | pos: inClass.pos 201 | }); 202 | } 203 | 204 | static function exprComponentDidUpdate( 205 | inClass:ClassType, 206 | prevProps:String, 207 | prevState:String, 208 | hasProps:Bool, 209 | hasState:Bool 210 | ) { 211 | if (!hasProps && !hasState) return macro {}; 212 | 213 | var displayLongWarning = macro { 214 | if (react.macro.ReactDebugMacro.firstRenderWarning) { 215 | react.macro.ReactDebugMacro.firstRenderWarning = false; 216 | 217 | js.Browser.console.warn($v{ 218 | 'Make sure your props are flattened, or implement shouldComponentUpdate.\n' + 219 | 'See https://facebook.github.io/react/docs/optimizing-performance.html#shouldcomponentupdate-in-action' + 220 | '\n\nAlso note that legacy context API can trigger false positives if children ' + 221 | 'rely on context. You can hide this warning for a specific component by adding ' + 222 | '`@${ReactMeta.IgnoreRenderWarning}` meta to its class.' 223 | }); 224 | } 225 | } 226 | 227 | if (inClass.meta.has(ReactMeta.WhyRender)) { 228 | return macro { 229 | var propsChanges = react.ReactUtil.shallowChanges(props, prevProps); 230 | if (propsChanges != null) { 231 | js.Browser.console.log($v{'Props changed for `${inClass.name}`.'}); 232 | js.Browser.console.log(propsChanges); 233 | } 234 | 235 | var stateChanges = react.ReactUtil.shallowChanges(state, prevState); 236 | if (stateChanges != null) { 237 | js.Browser.console.log($v{'State changed for `${inClass.name}`.'}); 238 | js.Browser.console.log(stateChanges); 239 | } 240 | 241 | if (propsChanges == null && stateChanges == null) { 242 | js.Browser.console.warn($v{ 243 | 'Warning: avoidable re-render of `${inClass.name}`.' 244 | }); 245 | 246 | ${displayLongWarning} 247 | } 248 | }; 249 | } 250 | 251 | return macro { 252 | ${hasProps 253 | ? macro var propsAreEqual = react.ReactUtil.shallowCompare(this.props, $i{prevProps}) 254 | : macro {} 255 | }; 256 | 257 | ${hasState 258 | ? macro var statesAreEqual = react.ReactUtil.shallowCompare(this.state, $i{prevState}) 259 | : macro {} 260 | }; 261 | 262 | var cond = ${!hasProps 263 | ? macro statesAreEqual 264 | : !hasState 265 | ? macro propsAreEqual 266 | : macro (propsAreEqual && statesAreEqual) 267 | }; 268 | 269 | if (cond) 270 | { 271 | ${hasProps ? macro { 272 | // Using Object.create(null) to avoid prototype for clean output 273 | var debugProps = untyped Object.create(null); 274 | debugProps.currentProps = this.props; 275 | debugProps.prevProps = $i{prevProps}; 276 | 277 | js.Browser.console.warn( 278 | $v{'Warning: avoidable re-render of `${inClass.name}`.\n'}, 279 | debugProps 280 | ); 281 | } : macro { 282 | js.Browser.console.warn( 283 | $v{'Warning: avoidable re-render of `${inClass.name}`.\n'} 284 | ); 285 | }}; 286 | 287 | ${displayLongWarning} 288 | } 289 | } 290 | } 291 | #end 292 | } 293 | -------------------------------------------------------------------------------- /src/lib/react/macro/ReactMeta.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | enum abstract ReactMeta(String) to String { 4 | // Components specification 5 | 6 | /** 7 | Wrap current component (must extend `ReactComponent`) in a HOC. 8 | 9 | See [Wrapping your components in HOCs](https://github.com/kLabz/haxe-react/tree/next/doc/wrapping-with-hoc.md) 10 | */ 11 | var Wrap = ':wrap'; 12 | 13 | /** 14 | `@:jsxStatic(method)` 15 | 16 | Create a static component that you can use in jsx (and where "real" 17 | components are expected) from a static function from a class (**not** a 18 | `ReactComponent` class). 19 | 20 | See [Static components](https://github.com/kLabz/haxe-react/tree/next/doc/static-components.md). 21 | */ 22 | var JsxStatic = ':jsxStatic'; 23 | 24 | /** 25 | TODO: Documentation for macro context API 26 | */ 27 | var ContextMeta = ':context'; 28 | 29 | /** 30 | TODO: Documentation for macro implementation of pure components. 31 | */ 32 | var PureComponent = ':pureComponent'; 33 | 34 | /** 35 | To be used with `@:wrap`. 36 | 37 | `@:publicProps(TProps)` 38 | Set public props type to ensure jsx type checking. 39 | 40 | See [Wrapping your components in HOCs](https://github.com/kLabz/haxe-react/tree/next/doc/wrapping-with-hoc.md) 41 | */ 42 | var PublicProps = ':publicProps'; 43 | 44 | /** 45 | To be used with `@:wrap`. 46 | 47 | `@:noPublicProps` 48 | Disallow public props for this component when used in jsx. 49 | 50 | See [Wrapping your components in HOCs](https://github.com/kLabz/haxe-react/tree/next/doc/wrapping-with-hoc.md) 51 | */ 52 | var NoPublicProps = ':noPublicProps'; 53 | 54 | /** 55 | See [`@:acceptsMoreProps`](https://github.com/kLabz/haxe-react/blob/next/doc/custom-meta.md#acceptsmoreprops) 56 | */ 57 | var AcceptsMoreProps = ':acceptsMoreProps'; 58 | 59 | // Debug config 60 | 61 | /** 62 | There is a compile-time check for an override of the `render` function 63 | in your components. This helps catching following runtime warning: 64 | 65 | Warning: Index(...): No `render` method found on the returned 66 | component instance: you may have forgotten to define `render`. 67 | 68 | Catching it at compile-time also ensures it does not happen to a 69 | component only visible for a few specific application state. 70 | 71 | You can disable this with the `-D react_ignore_empty_render` 72 | compilation flag, or for a specific component by adding 73 | `@:ignoreEmptyRender` meta to it. 74 | */ 75 | var IgnoreEmptyRender = ':ignoreEmptyRender'; 76 | 77 | /** 78 | TODO: Documentation for runtime warnings. 79 | */ 80 | var IgnoreRenderWarning = ':ignoreRenderWarning'; 81 | 82 | /** 83 | TODO: Documentation for runtime warnings. 84 | */ 85 | var WhyRender = ':whyRender'; 86 | 87 | // Internal metas 88 | 89 | /** 90 | This special meta is added by the `@:wrap` macro for internal use, do 91 | not set it if you don't want to break the functionality. 92 | */ 93 | var WrappedByMacro = ':wrapped_by_macro'; 94 | 95 | /** 96 | This special meta is added by the `@:pureComponent` macro for internal 97 | use, do not set it if you don't want to break the functionality. 98 | */ 99 | var PureComponentInjected = ':pureComponent_injected'; 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/react/macro/ReactTypeMacro.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.TypeTools; 7 | 8 | @:dce 9 | class ReactTypeMacro 10 | { 11 | static public inline var ALTER_SIGNATURES_BUILDER = 'AlterSignatures'; 12 | static public inline var ENSURE_RENDER_OVERRIDE_BUILDER = 'EnsureRenderOverride'; 13 | static public inline var CHECK_GET_DERIVED_STATE_BUILDER = 'CheckDerivedState'; 14 | @:deprecated static public inline var IGNORE_EMPTY_RENDER_META = ReactMeta.IgnoreEmptyRender; 15 | 16 | #if macro 17 | public static function alterComponentSignatures(inClass:ClassType, fields:Array):Array 18 | { 19 | if (inClass.isExtern) return fields; 20 | 21 | var types = MacroUtil.extractComponentTypes(inClass); 22 | var tprops = types.tprops == null ? macro :Dynamic : types.tprops; 23 | var tstate = types.tstate == null ? macro :Dynamic : types.tstate; 24 | 25 | // Only alter setState signature for non-dynamic states 26 | switch (tstate) { 27 | case TPath({name: "Empty", pack: ["react"]}), TPath({name: "Dynamic", pack: []}): 28 | 29 | case TPath(_) | TAnonymous(_) if (!hasSetState(fields)): 30 | addSetStateType(fields, inClass, tprops, tstate); 31 | 32 | default: 33 | } 34 | 35 | return fields; 36 | } 37 | 38 | public static function ensureRenderOverride(inClass:ClassType, fields:Array):Array 39 | { 40 | if (!(inClass.isExtern || inClass.meta.has(ReactMeta.IgnoreEmptyRender))) 41 | if (!Lambda.exists(fields, function(f) return f.name == 'render')) 42 | Context.warning( 43 | 'Component ${inClass.name}: ' 44 | + 'No `render` method found: you may have forgotten to ' 45 | + 'override `render` from `ReactComponent`.', 46 | inClass.pos 47 | ); 48 | 49 | return fields; 50 | } 51 | 52 | public static function checkGetDerivedState(inClass:ClassType, fields:Array):Array 53 | { 54 | if (!inClass.isExtern) { 55 | var getDerived = MacroUtil.getField(fields, "getDerivedStateFromProps"); 56 | 57 | if (getDerived != null) { 58 | switch (getDerived.kind) 59 | { 60 | case FFun(fun) if (Lambda.has(getDerived.access, AStatic)): 61 | var types = MacroUtil.extractComponentTypes(inClass); 62 | var tprops = types.tprops == null ? macro :Dynamic : types.tprops; 63 | var tstate = types.tstate == null ? macro :Dynamic : types.tstate; 64 | 65 | var expected = macro :$tprops->$tstate->react.Partial<$tstate>; 66 | var ct = TypeTools.toComplexType(MacroUtil.functionToType(fun)); 67 | 68 | Context.typeof(macro @:pos(getDerived.pos) { 69 | var a:$ct = null; 70 | var b:$expected = a; 71 | }); 72 | 73 | default: 74 | Context.warning( 75 | 'Component ${inClass.name}: ' 76 | + 'Field getDerivedStateFromProps should be a static function ' 77 | + 'with `props` and `prevState` as arguments.', 78 | getDerived.pos 79 | ); 80 | 81 | } 82 | } 83 | } 84 | 85 | return fields; 86 | } 87 | 88 | static function hasSetState(fields:Array) { 89 | for (field in fields) 90 | { 91 | if (field.name == 'setState') 92 | { 93 | return switch (field.kind) { 94 | case FFun(f): true; 95 | default: false; 96 | } 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | static function addSetStateType( 104 | fields:Array, 105 | inClass:ClassType, 106 | propsType:ComplexType, 107 | stateType:ComplexType 108 | ) { 109 | fields.push((macro class C { 110 | @:overload(function(nextState:react.Partial<$stateType>, ?callback:Void -> Void):Void {}) 111 | @:overload(function(nextState:$stateType -> $propsType -> react.Partial<$stateType>, ?callback:Void -> Void):Void {}) 112 | override public extern function setState(nextState:$stateType -> react.Partial<$stateType>, ?callback:Void -> Void):Void 113 | #if !haxe4 114 | { super.setState(nextState, callback); } 115 | #end 116 | ; 117 | }).fields[0]); 118 | } 119 | 120 | #end 121 | } 122 | -------------------------------------------------------------------------------- /src/lib/react/macro/ReactWrapperMacro.hx: -------------------------------------------------------------------------------- 1 | package react.macro; 2 | 3 | import haxe.macro.ComplexTypeTools; 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.ExprTools; 7 | import haxe.macro.Type; 8 | import haxe.macro.TypeTools; 9 | import react.jsx.JsxStaticMacro; 10 | 11 | @:dce 12 | class ReactWrapperMacro 13 | { 14 | static public inline var WRAP_BUILDER = 'Wrap'; 15 | 16 | @:deprecated static public inline var WRAP_META = ReactMeta.Wrap; 17 | @:deprecated static public inline var PUBLIC_PROPS_META = ReactMeta.PublicProps; 18 | @:deprecated static public inline var NO_PUBLIC_PROPS_META = ReactMeta.NoPublicProps; 19 | 20 | static public function buildComponent(inClass:ClassType, fields:Array):Array 21 | { 22 | if (inClass.meta.has(ReactMeta.WrappedByMacro)) return fields; 23 | if (!inClass.meta.has(ReactMeta.Wrap)) return fields; 24 | 25 | if (inClass.meta.has(JsxStaticMacro.META_NAME)) 26 | Context.fatalError( 27 | 'Cannot use @${ReactMeta.Wrap} and @${JsxStaticMacro.META_NAME} on the same component', 28 | inClass.pos 29 | ); 30 | 31 | var wrapperExpr = null; 32 | var wrappersMeta = inClass.meta.extract(ReactMeta.Wrap); 33 | wrappersMeta.reverse(); 34 | 35 | var publicProps = extractPublicProps(inClass.meta, wrappersMeta[0].pos); 36 | if (publicProps != null && inClass.meta.has(ReactMeta.AcceptsMoreProps)) { 37 | publicProps = macro :react.ReactComponent.ACCEPTS_MORE_PROPS<$publicProps>; 38 | } 39 | 40 | var fieldType = publicProps == null 41 | ? null 42 | : macro :$publicProps->react.ReactComponent.ReactFragment; 43 | 44 | Lambda.iter(wrappersMeta, function(m) { 45 | if (m.params.length == 0) 46 | Context.fatalError( 47 | 'Invalid number of parameters for @${ReactMeta.Wrap}; ' 48 | + 'expected 1 parameter (hoc expression).', 49 | m.pos 50 | ); 51 | 52 | var e = m.params[0]; 53 | wrapperExpr = wrapperExpr == null 54 | ? macro @:pos(e.pos) ${e}($i{inClass.name}) 55 | : macro @:pos(e.pos) ${e}(${wrapperExpr}); 56 | }); 57 | 58 | var fieldName = '_renderWrapper'; 59 | fields.push({ 60 | access: [APublic, AStatic], 61 | name: fieldName, 62 | kind: FVar(fieldType, macro cast (${wrapperExpr} :react.ReactType)), 63 | doc: null, 64 | meta: null, 65 | pos: inClass.pos 66 | }); 67 | 68 | inClass.meta.add(JsxStaticMacro.META_NAME, [macro $v{fieldName}], inClass.pos); 69 | inClass.meta.add(ReactMeta.WrappedByMacro, [], inClass.pos); 70 | 71 | return fields; 72 | } 73 | 74 | static function extractPublicProps(meta:MetaAccess, wrapPos:Position):Null 75 | { 76 | if (meta.has(ReactMeta.PublicProps)) 77 | { 78 | var publicProps = meta.extract(ReactMeta.PublicProps)[0]; 79 | if (publicProps.params.length == 0) 80 | Context.fatalError( 81 | 'Invalid number of parameters for @${ReactMeta.PublicProps}; ' 82 | + 'expected 1 parameter (props type identifier).', 83 | publicProps.pos 84 | ); 85 | 86 | var e = publicProps.params[0]; 87 | var tprops = try { 88 | Context.getType(switch (e.expr) { 89 | case EConst(CString(str)), EConst(CIdent(str)): str; 90 | case EField(_, _): ExprTools.toString(e); 91 | 92 | default: 93 | Context.error( 94 | '@${ReactMeta.PublicProps}: unsupported argument; ' 95 | + 'expected a type identifier.', 96 | publicProps.pos 97 | ); 98 | }); 99 | } catch (e:String) { 100 | Context.error(e, publicProps.params[0].pos); 101 | } 102 | 103 | return TypeTools.toComplexType(tprops); 104 | } 105 | 106 | if (meta.has(ReactMeta.NoPublicProps)) return macro :{}; 107 | 108 | #if react_wrap_strict 109 | Context.warning( 110 | '@${ReactMeta.Wrap}: missing @${ReactMeta.PublicProps} meta required by ' 111 | + 'strict mode (`-D react_wrap_strict`).', 112 | wrapPos 113 | ); 114 | #end 115 | 116 | return null; 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /tagNext.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | VERSION=$1 5 | MESSAGE=$2 6 | 7 | # Validate version 8 | if [ -z "$VERSION" ]; then 9 | echo "No version specified" 10 | exit 1 11 | fi 12 | if [ $(git tag -l "$VERSION") ]; then 13 | echo "Tag $VERSION already exists" 14 | exit 1 15 | fi 16 | 17 | # Validate message 18 | if [ -z "$MESSAGE" ]; then 19 | echo "No release note specified" 20 | exit 1 21 | fi 22 | 23 | # Change version and release note in haxelib.json 24 | echo "Update version to $VERSION" 25 | EXTRA_PATTERN="s/-D react=[0-9]+.[0-9]+.[0-9]+/-D react=$VERSION/g" 26 | 27 | V=$VERSION RN=$MESSAGE jq '. + {"version": env.V, "releasenote": env.RN}' ./haxelib.json | sponge ./haxelib.json 28 | V=$VERSION jq '. + {"version": env.V}' ./mdk/info.json | sponge ./mdk/info.json 29 | sed -E -i "$EXTRA_PATTERN" ./extraParams.hxml 30 | MESSAGE=" - $MESSAGE" 31 | 32 | # Tag, commit and push to trigger a new CI release 33 | git add ./haxelib.json 34 | git add ./mdk/info.json 35 | git add ./extraParams.hxml 36 | git commit -m "$VERSION$MESSAGE" 37 | git push fork next 38 | git tag $VERSION 39 | git push fork $VERSION 40 | -------------------------------------------------------------------------------- /tagRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | VERSION=$1 5 | MESSAGE=$2 6 | 7 | # Validate version 8 | if [ -z "$VERSION" ]; then 9 | echo "No version specified" 10 | exit 1 11 | fi 12 | if [ $(git tag -l "$VERSION") ]; then 13 | echo "Tag $VERSION already exists" 14 | exit 1 15 | fi 16 | 17 | if [ -z "$MESSAGE" ]; then 18 | MESSAGE="" 19 | else 20 | MESSAGE=" - $MESSAGE" 21 | fi 22 | 23 | # Change version in haxelib.json and package.json 24 | echo "Update version to $VERSION" 25 | PATTERN="s/\"version\": \"[0-9]+.[0-9]+.[0-9]+\"/\"version\": \"$VERSION\"/g" 26 | EXTRA_PATTERN="s/-D react=[0-9]+.[0-9]+.[0-9]+/-D react=$VERSION/g" 27 | 28 | case "$(uname -s)" in 29 | Darwin) 30 | sed -E -i "" "$PATTERN" ./haxelib.json 31 | sed -E -i "" "$PATTERN" ./mdk/info.json 32 | sed -E -i "" "$EXTRA_PATTERN" ./extraParams.hxml 33 | ;; 34 | *) 35 | sed -E -i "$PATTERN" ./haxelib.json 36 | sed -E -i "$PATTERN" ./mdk/info.json 37 | sed -E -i "$EXTRA_PATTERN" ./extraParams.hxml 38 | ;; 39 | esac 40 | 41 | # Tag, commit and push to trigger a new CI release 42 | git add ./haxelib.json 43 | git add ./mdk/info.json 44 | git add ./extraParams.hxml 45 | git commit -m "$VERSION$MESSAGE" 46 | git push fork next 47 | git tag $VERSION 48 | git push fork $VERSION 49 | -------------------------------------------------------------------------------- /test.hxml: -------------------------------------------------------------------------------- 1 | test/test.hxml -------------------------------------------------------------------------------- /test/src/AssertTools.hx: -------------------------------------------------------------------------------- 1 | import haxe.macro.Expr; 2 | import massive.munit.Assert; 3 | 4 | class AssertTools 5 | { 6 | macro static public function assertHasProps(oExpr:Expr, namesExpr:ExprOf>, ?valuesExpr:ExprOf>) 7 | { 8 | return macro { 9 | var o:Dynamic = ${oExpr}, names:Array = ${namesExpr}, values:Array = ${valuesExpr}; 10 | var props = Reflect.fields(o); 11 | Assert.areEqual(names.length, props.length); 12 | for (i in 0...names.length) 13 | { 14 | var name = names[i]; 15 | Assert.areNotEqual( -1, props.indexOf(name)); 16 | if (values != null && values[i] != null) 17 | Assert.areEqual(values[i], Reflect.field(o, name)); 18 | } 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /test/src/ReactMacroTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import js.Syntax; 4 | import massive.munit.Assert; 5 | import react.ReactComponent; 6 | import react.ReactMacro.jsx; 7 | import support.sub.CompExternModule; 8 | import support.sub.CompModule; 9 | import AssertTools.assertHasProps; 10 | 11 | @:ignoreEmptyRender 12 | class CompBasic extends ReactComponent {} 13 | 14 | @:ignoreEmptyRender 15 | class CompBasicProps extends ReactComponentOfProps<{a:Int}> {} 16 | 17 | typedef RenderProps = { 18 | @:optional var children:Int->ReactFragment; 19 | } 20 | 21 | @:ignoreEmptyRender 22 | class CompRenderProp extends ReactComponentOfProps {} 23 | 24 | @:ignoreEmptyRender 25 | class CompDefaults extends ReactComponent { 26 | static public var defaultProps = { 27 | defA:'A', 28 | defB:42 29 | } 30 | } 31 | 32 | enum SomeStatus { 33 | Loading; 34 | Done(data:String); 35 | } 36 | 37 | typedef StateWithEnum = { 38 | var status:SomeStatus; 39 | } 40 | 41 | @:ignoreEmptyRender 42 | class CompStateWithEnum extends ReactComponentOfState { 43 | // Just checking compilation for now 44 | function test() { 45 | setState({status: Loading}); 46 | setState({status: Loading}, function() {}); 47 | setState(function(_) return {status: Loading}); 48 | setState(function(_) return {status: Loading}, function() {}); 49 | setState(function(s) return {status: s.status}); 50 | setState(function(s) return {status: s.status}, function() {}); 51 | setState(function(_, _) return {status: Loading}); 52 | setState(function(_, _) return {status: Loading}, function() {}); 53 | } 54 | } 55 | 56 | extern class CompExtern extends ReactComponent {} 57 | 58 | 59 | class ReactMacroTest 60 | { 61 | 62 | public function new() {} 63 | 64 | @BeforeClass 65 | public function setup() 66 | { 67 | Syntax.code("$global.CompExtern = function() {}"); 68 | Syntax.code("$global.CompExternModule = function() {}"); 69 | } 70 | 71 | @Test 72 | public function DOM_without_props() 73 | { 74 | var e = jsx('
    '); 75 | Assert.areEqual('div', e.type); 76 | assertHasProps(e.props, []); 77 | } 78 | 79 | @Test 80 | public function DOM_with_const_props() 81 | { 82 | var e = jsx('
    '); 83 | Assert.areEqual('div', e.type); 84 | assertHasProps(e.props, ['a'], ['foo']); 85 | } 86 | 87 | @Test 88 | public function DOM_with_const_and_binding_props() 89 | { 90 | var foo = 12; 91 | var e = jsx('
    '); 92 | Assert.areEqual('div', e.type); 93 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 94 | } 95 | 96 | @Test 97 | public function function_with_props() 98 | { 99 | var e = jsx(''); 100 | Assert.areEqual(RenderFunction, e.type); 101 | assertHasProps(e.props, ['a'], ['foo']); 102 | } 103 | 104 | @Test 105 | public function component_with_props() 106 | { 107 | var e = jsx(''); 108 | Assert.areEqual(CompBasic, e.type); 109 | assertHasProps(e.props, ['a'], ['foo']); 110 | } 111 | 112 | @Test 113 | public function render_prop() 114 | { 115 | var e = jsx('<$CompRenderProp>${function(value:Int) { return null; }}'); 116 | Assert.areEqual(CompRenderProp, e.type); 117 | 118 | #if haxe4 119 | var e = jsx('<$CompRenderProp>${(value:Int) -> null}'); 120 | Assert.areEqual(CompRenderProp, e.type); 121 | 122 | var e = jsx('<$CompRenderProp>${value -> null}'); 123 | Assert.areEqual(CompRenderProp, e.type); 124 | #end 125 | } 126 | 127 | @Test 128 | public function interpolate_attributes() 129 | { 130 | var answer = "42"; 131 | var e = jsx(''); 132 | Assert.areEqual(e.props.value, 'the answer is $42'); 133 | 134 | var e = jsx(''); 135 | Assert.areEqual(e.props.value, 'the answer is 42'); 136 | } 137 | 138 | @Test 139 | public function fragments() 140 | { 141 | var e = jsx('<>
    '); 142 | Assert.areEqual(react.Fragment, e.type); 143 | Assert.areEqual(2, e.props.children.length); 144 | } 145 | 146 | @Test 147 | public function extern_component_qualified_module_should_DEOPT() 148 | { 149 | var e = jsx(''); 150 | Assert.areEqual('NATIVE', e.type); 151 | } 152 | 153 | @Test 154 | public function extern_component_module_should_DEOPT() 155 | { 156 | var e = jsx(''); 157 | Assert.areEqual('NATIVE', e.type); 158 | } 159 | 160 | @Test 161 | public function extern_component_should_DEOPT() 162 | { 163 | var e = jsx(''); 164 | Assert.areEqual('NATIVE', e.type); 165 | } 166 | 167 | @Test 168 | public function DOM_with_spread() 169 | { 170 | var o = { 171 | a:'foo', 172 | b:12 173 | } 174 | var e = jsx('
    '); 175 | Assert.areEqual('div', e.type); 176 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 177 | } 178 | 179 | @Test 180 | public function DOM_with_spread_and_prop() 181 | { 182 | var o = { 183 | a:'foo', 184 | b:12 185 | } 186 | var e = jsx('
    '); 187 | Assert.areEqual('div', e.type); 188 | assertHasProps(e.props, ['a', 'b', 'c'], (['foo', 12, 'bar']:Array)); 189 | } 190 | 191 | @Test 192 | public function DOM_with_spread_and_prop_override() 193 | { 194 | var o = { 195 | a:'foo', 196 | b:12 197 | } 198 | var e = jsx('
    '); 199 | Assert.areEqual('div', e.type); 200 | assertHasProps(e.props, ['a', 'b'], (['bar', 12]:Array)); 201 | } 202 | 203 | @Test 204 | public function component_with_defaultProps() 205 | { 206 | var e = jsx(''); 207 | Assert.areEqual(CompDefaults, e.type); 208 | assertHasProps(e.props, ['defA', 'defB'], (['A', 42]:Array)); 209 | } 210 | 211 | @Test 212 | public function component_in_sub_package_with_defaultProps() 213 | { 214 | var e = jsx(''); 215 | Assert.areEqual(CompModule, e.type); 216 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 217 | } 218 | 219 | @Test 220 | public function qualified_component_in_sub_package_with_defaultProps() 221 | { 222 | var e = jsx(''); 223 | Assert.areEqual(CompModule, e.type); 224 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 225 | } 226 | 227 | @Test 228 | public function component_with_defaultProps_and_spread() 229 | { 230 | var o = { 231 | a:'foo', 232 | b:12 233 | } 234 | var e = jsx(''); 235 | Assert.areEqual(CompDefaults, e.type); 236 | assertHasProps(e.props, ['defA', 'defB', 'a', 'b'], (['A', 42, 'foo', 12]:Array)); 237 | } 238 | 239 | @Test 240 | public function component_with_defaultProps_and_prop_override() 241 | { 242 | var e = jsx(''); 243 | Assert.areEqual(CompDefaults, e.type); 244 | assertHasProps(e.props, ['defA', 'defB'], (['foo', 42]:Array)); 245 | } 246 | 247 | @Test 248 | public function component_with_defaultProps_and_spread_override() 249 | { 250 | var o = { 251 | defA:'foo', 252 | b:12 253 | } 254 | var e = jsx(''); 255 | Assert.areEqual(CompDefaults, e.type); 256 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['foo', 42, 12]:Array)); 257 | } 258 | 259 | @Test 260 | public function component_with_defaultProps_and_spread_and_prop_override() 261 | { 262 | var o = { 263 | defA:'foo', 264 | b:12 265 | } 266 | var e = jsx(''); 267 | Assert.areEqual(CompDefaults, e.type); 268 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['bar', 42, 12]:Array)); 269 | } 270 | 271 | @Test 272 | public function component_with_defaultProps_and_undefined() 273 | { 274 | var obj:Dynamic = {}; 275 | var e = jsx(''); 276 | Assert.areEqual(CompDefaults, e.type); 277 | assertHasProps(e.props, ['defA', 'defB'], ['A', null]); 278 | } 279 | 280 | @Test 281 | public function DOM_with_ref_function_should_be_inlined() 282 | { 283 | function setRef() {}; 284 | var e = jsx('
    '); 285 | Assert.areEqual('div', e.type); 286 | Assert.areEqual(setRef, e.ref); 287 | assertHasProps(e.props, []); 288 | } 289 | 290 | @Test 291 | public function DOM_with_ref_const_string_should_be_DEOPT() 292 | { 293 | var e = jsx('
    '); 294 | Assert.areEqual('NATIVE', e.type); 295 | } 296 | 297 | @Test 298 | public function DOM_with_ref_string_should_be_DEOPT() 299 | { 300 | var setRef = 'myRef'; 301 | var e = jsx('
    '); 302 | Assert.areEqual('NATIVE', e.type); 303 | } 304 | 305 | @Test 306 | public function DOM_with_ref_unknown_should_be_DEOPT() 307 | { 308 | var setRef:Dynamic = function() {}; 309 | var e = jsx('
    '); 310 | Assert.areEqual('NATIVE', e.type); 311 | } 312 | 313 | @Test 314 | public function DOM_with_single_child_text_should_NOT_be_array() 315 | { 316 | var e = jsx('
    hello
    '); 317 | Assert.areEqual('div', e.type); 318 | assertHasProps(e.props, ['children']); 319 | var children = e.props.children; 320 | Assert.areEqual('hello', children); 321 | } 322 | 323 | @Test 324 | public function DOM_with_single_child_node_should_NOT_be_array() 325 | { 326 | var e = jsx('
    '); 327 | Assert.areEqual('div', e.type); 328 | assertHasProps(e.props, ['children']); 329 | var children = e.props.children; 330 | Assert.isFalse(Std.is(children, Array)); 331 | Assert.areEqual('span', children.type); 332 | } 333 | 334 | @Test 335 | public function DOM_with_single_child_binding_should_NOT_be_array() 336 | { 337 | var o = "{ name:'o' }"; 338 | var e = jsx('
    ${o}
    '); 339 | Assert.areEqual('div', e.type); 340 | Assert.areEqual(e.props.children, o); 341 | } 342 | 343 | @Test 344 | public function entities() 345 | { 346 | var e = jsx('
    hello &world; <3
    '); 347 | assertHasProps( 348 | e.props, 349 | ['title', 'children'], ["a < b", "hello &world; <3"] 350 | ); 351 | 352 | e = jsx('
    {"hello &world; <3"}
    '); 353 | assertHasProps( 354 | e.props, 355 | ['title', 'children'], ["a < b", "hello &world; <3"] 356 | ); 357 | 358 | e = jsx('
    <div/>
    '); 359 | assertHasProps( 360 | e.props, 361 | ['children'], ["
    "] 362 | ); 363 | 364 | var nbsp = String.fromCharCode(160); 365 | e = jsx('
    hello <3
    '); 366 | assertHasProps( 367 | e.props, 368 | ['children'], ['hello$nbsp<3'] 369 | ); 370 | 371 | e = jsx('
    hello  <3
    '); 372 | assertHasProps( 373 | e.props, 374 | ['children'], ['hello$nbsp$nbsp<3'] 375 | ); 376 | } 377 | 378 | @Test 379 | public function control_structures() { 380 | for (flag in [true, false]) { 381 | var counter = 0; 382 | for (i in 0...5) { 383 | var inner = 0; 384 | jsx(' 385 |
    386 | 387 |
    Foo
    388 | 389 |

    Test ${inner += j}

    390 | 391 |
    392 | 393 | 394 | 395 | 396 | 397 | <$CompBasic /> 398 |
    405 | '); 406 | Assert.areEqual(if (flag) (i * (i - 1)) >> 1 else 0, inner); 407 | } 408 | Assert.areEqual(if (flag) 0 else 9, counter); 409 | } 410 | } 411 | 412 | @Test 413 | public function DOM_with_children_should_be_array() 414 | { 415 | var e = jsx('
    hello
    '); 416 | Assert.areEqual('div', e.type); 417 | assertHasProps(e.props, ['children']); 418 | var children = e.props.children; 419 | Assert.isTrue(Std.is(children, Array)); 420 | Assert.areEqual('hello ', children[0]); 421 | Assert.areEqual('span', children[1].type); 422 | } 423 | 424 | /* TOOLS */ 425 | 426 | function assertHasProps(o:Dynamic, names:Array, ?values:Array) 427 | { 428 | var props = Reflect.fields(o); 429 | Assert.areEqual(names.length, props.length); 430 | for (i in 0...names.length) 431 | { 432 | var name = names[i]; 433 | Assert.areNotEqual( -1, props.indexOf(name)); 434 | if (values != null && values[i] != null) 435 | Assert.areEqual(values[i], Reflect.field(o, name)); 436 | } 437 | } 438 | 439 | function RenderFunction(props:{ a:String }) 440 | { 441 | return jsx('
    '); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /test/src/TestMain.hx: -------------------------------------------------------------------------------- 1 | import massive.munit.client.PrintClient; 2 | import massive.munit.client.RichPrintClient; 3 | import massive.munit.client.HTTPClient; 4 | import massive.munit.client.JUnitReportClient; 5 | import massive.munit.client.SummaryReportClient; 6 | import massive.munit.TestRunner; 7 | 8 | #if js 9 | import js.Lib; 10 | #end 11 | 12 | /** 13 | * Auto generated Test Application. 14 | * Refer to munit command line tool for more information (haxelib run munit) 15 | */ 16 | class TestMain 17 | { 18 | static function main(){ new TestMain(); } 19 | 20 | public function new() 21 | { 22 | var suites = new Array>(); 23 | suites.push(TestSuite); 24 | 25 | var host = "http://127.0.0.1:2000/"; 26 | 27 | #if MCOVER 28 | var client = new mcover.coverage.munit.client.MCoverPrintClient(); 29 | var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient(), host); 30 | #else 31 | var client = new RichPrintClient(); 32 | var httpClient = new HTTPClient(new SummaryReportClient(), host); 33 | #end 34 | 35 | var runner:TestRunner = new TestRunner(client); 36 | runner.addResultClient(httpClient); 37 | //runner.addResultClient(new HTTPClient(new JUnitReportClient())); 38 | 39 | runner.completionHandler = completionHandler; 40 | 41 | #if (js && !nodejs) 42 | var seconds = 0; // edit here to add some startup delay 43 | function delayStartup() 44 | { 45 | if (seconds > 0) { 46 | seconds--; 47 | js.Browser.document.getElementById("munit").innerHTML = 48 | "Tests will start in " + seconds + "s..."; 49 | haxe.Timer.delay(delayStartup, 1000); 50 | } 51 | else { 52 | js.Browser.document.getElementById("munit").innerHTML = ""; 53 | runner.run(suites); 54 | } 55 | } 56 | delayStartup(); 57 | #else 58 | runner.run(suites); 59 | #end 60 | } 61 | 62 | /* 63 | updates the background color and closes the current browser 64 | for flash and html targets (useful for continous integration servers) 65 | */ 66 | function completionHandler(successful:Bool):Void 67 | { 68 | try 69 | { 70 | #if flash 71 | flash.external.ExternalInterface.call("testResult", successful); 72 | #elseif js 73 | js.Lib.eval("testResult(" + successful + ");"); 74 | #elseif sys 75 | Sys.exit(0); 76 | #end 77 | } 78 | // if run from outside browser can get error which we can ignore 79 | catch (e:Dynamic) 80 | { 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/src/TestSuite.hx: -------------------------------------------------------------------------------- 1 | import massive.munit.TestSuite; 2 | 3 | import ReactMacroTest; 4 | 5 | /** 6 | * Auto generated Test Suite for MassiveUnit. 7 | * Refer to munit command line tool for more information (haxelib run munit) 8 | */ 9 | class TestSuite extends massive.munit.TestSuite 10 | { 11 | public function new() 12 | { 13 | super(); 14 | 15 | add(ReactMacroTest); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/src/react/Fragment.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | typedef Fragment = ReactComponent; 4 | -------------------------------------------------------------------------------- /test/src/react/React.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactElement; 4 | import react.ReactComponent.ReactFragment; 5 | 6 | /** 7 | STUB 8 | **/ 9 | extern class React 10 | { 11 | /** 12 | https://facebook.github.io/react/docs/react-api.html#react.proptypes 13 | **/ 14 | public static var PropTypes(default, null):ReactPropTypes; 15 | 16 | /** 17 | https://facebook.github.io/react/docs/react-api.html#createelement 18 | **/ 19 | public inline static function createElement(type:CreateElementType, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement 20 | { 21 | return untyped { type:'NATIVE' }; 22 | } 23 | 24 | /** 25 | https://facebook.github.io/react/docs/react-api.html#cloneelement 26 | **/ 27 | public inline static function cloneElement(element:ReactElement, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement 28 | { 29 | return untyped { type:'NATIVE' }; 30 | } 31 | 32 | /** 33 | https://facebook.github.io/react/docs/react-api.html#isvalidelement 34 | **/ 35 | public static inline function isValidElement(object:Dynamic):Bool 36 | { 37 | return true; 38 | } 39 | 40 | /** 41 | https://facebook.github.io/react/docs/react-api.html#react.children 42 | **/ 43 | public static var Children:ReactChildren; 44 | } 45 | 46 | /** 47 | https://facebook.github.io/react/docs/react-api.html#react.children 48 | **/ 49 | extern interface ReactChildren 50 | { 51 | /** 52 | https://facebook.github.io/react/docs/react-api.html#react.children.map 53 | **/ 54 | function map(children:Dynamic, fn:ReactFragment->ReactFragment):Dynamic; 55 | 56 | /** 57 | https://facebook.github.io/react/docs/react-api.html#react.children.foreach 58 | **/ 59 | function foreach(children:Dynamic, fn:ReactFragment->Void):Void; 60 | 61 | /** 62 | https://facebook.github.io/react/docs/react-api.html#react.children.count 63 | **/ 64 | function count(children:Dynamic):Int; 65 | 66 | /** 67 | https://facebook.github.io/react/docs/react-api.html#react.children.only 68 | **/ 69 | function only(children:Dynamic):ReactElement; 70 | 71 | /** 72 | https://facebook.github.io/react/docs/react-api.html#react.children.toarray 73 | **/ 74 | function toArray(children:Dynamic):Array; 75 | } 76 | 77 | private typedef CET = haxe.extern.EitherType, Class>; 78 | 79 | abstract CreateElementType(CET) to CET 80 | { 81 | @:from 82 | static public function fromString(s:String):CreateElementType 83 | { 84 | return cast s; 85 | } 86 | 87 | @:from 88 | static public function fromFunction(f:Void->ReactFragment):CreateElementType 89 | { 90 | return cast f; 91 | } 92 | 93 | @:from 94 | static public function fromFunctionWithProps(f:TProps->ReactFragment):CreateElementType 95 | { 96 | return cast f; 97 | } 98 | 99 | @:from 100 | static public function fromComp(cls:Class):CreateElementType 101 | { 102 | if (untyped cls.__jsxStatic != null) 103 | return untyped cls.__jsxStatic; 104 | 105 | return cast cls; 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /test/src/react/ReactComponent.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | import js.Syntax; 3 | import haxe.extern.EitherType; 4 | 5 | typedef ReactComponentProps = { 6 | /** 7 | Children have to be manipulated using React.Children.* 8 | **/ 9 | @:optional var children:Dynamic; 10 | } 11 | 12 | /** 13 | STUB CLASSES 14 | **/ 15 | @:genericBuild(react.macro.ReactComponentMacro.buildVariadic()) 16 | class ReactComponent {} 17 | typedef ReactComponentOfProps = ReactComponentOf; 18 | typedef ReactComponentOfState = ReactComponentOf; 19 | 20 | #if react_deprecated_refs 21 | // Keep the old ReactComponentOfPropsAndState typedef available for a few versions 22 | // But now we should use ReactComponentOf directly 23 | typedef ReactComponentOfPropsAndState = ReactComponentOf; 24 | #end 25 | 26 | @:autoBuild(react.macro.ReactComponentMacro.build()) 27 | class ReactComponentOf 28 | { 29 | static var defaultProps:Dynamic; 30 | 31 | var props(default, null):TProps; 32 | var state(default, null):TState; 33 | 34 | #if react_deprecated_context 35 | // It's better to define it in your ReactComponent subclass as needed, with the right typing. 36 | static var contextTypes:Dynamic; 37 | var context(default, null):Dynamic; 38 | #end 39 | 40 | function new(?props:TProps) {} 41 | 42 | /** 43 | https://facebook.github.io/react/docs/react-component.html#forceupdate 44 | **/ 45 | function forceUpdate(?callback:Void -> Void):Void {} 46 | 47 | /** 48 | https://facebook.github.io/react/docs/react-component.html#setstate 49 | **/ 50 | @:overload(function(nextState:TState, ?callback:Void -> Void):Void {}) 51 | @:overload(function(nextState:TState -> TProps -> TState, ?callback:Void -> Void):Void {}) 52 | function setState(nextState:TState -> TState, ?callback:Void -> Void):Void {} 53 | 54 | /** 55 | https://facebook.github.io/react/docs/react-component.html#render 56 | **/ 57 | function render():ReactFragment { return null; } 58 | 59 | /** 60 | https://facebook.github.io/react/docs/react-component.html#componentwillmount 61 | **/ 62 | function componentWillMount():Void {} 63 | 64 | /** 65 | https://facebook.github.io/react/docs/react-component.html#componentdidmount 66 | **/ 67 | function componentDidMount():Void {} 68 | 69 | /** 70 | https://facebook.github.io/react/docs/react-component.html#componentwillunmount 71 | **/ 72 | function componentWillUnmount():Void {} 73 | 74 | /** 75 | https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops 76 | **/ 77 | function componentWillReceiveProps(nextProps:TProps):Void {} 78 | 79 | /** 80 | https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate 81 | **/ 82 | dynamic function shouldComponentUpdate(nextProps:TProps, nextState:TState):Bool { return true; } 83 | 84 | /** 85 | https://facebook.github.io/react/docs/react-component.html#componentwillupdate 86 | **/ 87 | function componentWillUpdate(nextProps:TProps, nextState:TState):Void {} 88 | 89 | /** 90 | https://facebook.github.io/react/docs/react-component.html#componentdidupdate 91 | **/ 92 | function componentDidUpdate(prevProps:TProps, prevState:TState):Void {} 93 | 94 | static function __init__():Void { 95 | // required magic value to tag literal react elements 96 | Syntax.code("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 97 | } 98 | } 99 | 100 | typedef ReactElement = { 101 | type:ReactType, 102 | props:Dynamic, 103 | ?key:Dynamic, 104 | ?ref:Dynamic, 105 | } 106 | 107 | @:pure @:coreType abstract ReactSingleFragment 108 | from String 109 | from Float 110 | from Bool 111 | from ReactElement {} 112 | 113 | @:pure @:coreType abstract ReactFragment 114 | from ReactSingleFragment 115 | from Array 116 | from Array 117 | from Array {} 118 | -------------------------------------------------------------------------------- /test/src/support/sub/CompExternModule.hx: -------------------------------------------------------------------------------- 1 | package support.sub; 2 | import react.ReactComponent; 3 | 4 | @:native('CompExternModule') 5 | extern class CompExternModule extends ReactComponent 6 | { 7 | public function new(); 8 | } 9 | -------------------------------------------------------------------------------- /test/src/support/sub/CompModule.hx: -------------------------------------------------------------------------------- 1 | package support.sub; 2 | 3 | import react.ReactComponent; 4 | 5 | @:ignoreEmptyRender 6 | class CompModule extends ReactComponent 7 | { 8 | static public var defaultProps = { 9 | defA:'B', 10 | defB:43 11 | } 12 | 13 | public function new() 14 | { 15 | super(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/test.hxml: -------------------------------------------------------------------------------- 1 | -main TestMain 2 | -lib munit 3 | -lib tink_hxx 4 | -cp src/lib 5 | 6 | -lib hxnodejs 7 | -cp test/src 8 | -js test/build/js_test.js 9 | --------------------------------------------------------------------------------