├── .gitignore ├── .munit ├── .travis.yml ├── autocomplete.hxml ├── changes.md ├── doc ├── hoc-wrap.md ├── react-api-jsx.md ├── react-dependency.md └── static-components.md ├── extraParams.hxml ├── haxelib.json ├── mdk └── info.json ├── readme.md ├── releaseHaxelib.sh ├── samples └── todoapp │ ├── bin │ ├── index.html │ └── styles.css │ ├── build.hxml │ ├── readme.md │ └── src │ ├── Main.hx │ ├── store │ ├── TodoActions.hx │ ├── TodoItem.hx │ └── TodoStore.hx │ └── view │ ├── TodoApp.hx │ └── TodoList.hx ├── src └── lib │ └── react │ ├── Empty.hx │ ├── Fragment.hx │ ├── Partial.hx │ ├── PureComponent.hx │ ├── React.hx │ ├── ReactClipboardEvent.hx │ ├── ReactComponent.hx │ ├── ReactComponentMacro.hx │ ├── ReactDOM.hx │ ├── ReactDOMServer.hx │ ├── ReactDebugMacro.hx │ ├── ReactEvent.hx │ ├── ReactFocusEvent.hx │ ├── ReactKeyboardEvent.hx │ ├── ReactMacro.hx │ ├── ReactMouseEvent.hx │ ├── ReactPropTypes.hx │ ├── ReactRef.hx │ ├── ReactTouchEvent.hx │ ├── ReactTypeMacro.hx │ ├── ReactUIEvent.hx │ ├── ReactUtil.hx │ ├── ReactWheelEvent.hx │ ├── jsx │ ├── JsxParser.hx │ ├── JsxSanitize.hx │ └── JsxStaticMacro.hx │ └── wrap │ └── ReactWrapperMacro.hx ├── tagRelease.sh └── test ├── src ├── AssertTools.hx ├── JsxParserTest.hx ├── JsxSanitizeTest.hx ├── ReactMacroInlineMarkupTest.hx ├── ReactMacroTest.hx ├── TestMain.hx ├── TestSuite.hx ├── react │ ├── React.hx │ └── ReactComponent.hx └── support │ └── sub │ ├── CompExternModule.hx │ └── CompModule.hx └── test.hxml /.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 | index.js 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: haxe 2 | 3 | haxe: 4 | - 3.4.4 5 | - stable 6 | - development 7 | 8 | matrix: 9 | allow_failures: 10 | - haxe: development 11 | 12 | install: 13 | - haxelib install munit 14 | - haxelib install hxnodejs 15 | - haxelib install html-entities 16 | 17 | script: 18 | - haxelib run munit test -js 19 | 20 | deploy: 21 | - provider: script 22 | haxe: 3.4.4 23 | script: bash ./releaseHaxelib.sh $HAXELIB_USER $HAXELIB_PWD 24 | on: 25 | tags: true 26 | -------------------------------------------------------------------------------- /autocomplete.hxml: -------------------------------------------------------------------------------- 1 | -lib munit 2 | -cp src/lib 3 | -cp test/src 4 | -js _ 5 | react.React 6 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | ### 1.11.1 4 | 5 | - Fixes `PureComponent` typing 6 | 7 | ### 1.11.0 8 | 9 | - Added proper React feature flags system using `-D react_ver=major.minor` (default to latest) 10 | - Added UNSAFE lifecycle methods for React 16.9+ 11 | - Enabling `getSnapshotBeforeUpdate` livecycle API for React 16.3+ 12 | - Declaring `componentDidUpdate`'s arguments is now optional, to avoid API breaking changes 13 | 14 | ### 1.10.0 15 | 16 | - Added `getSnapshotBeforeUpdate` livecycle methods behind `-D react_snapshot_api` 17 | 18 | Breaking change: `componentDidUpdate` will expects an extra `snapshot` optional parameter: 19 | ``` 20 | override function componentDidUpdate(prevProps:TProps, prevState:TState, ?snapshot:Dynamic):Void {} 21 | ``` 22 | 23 | ### 1.9.0 24 | 25 | - Removed string-based `Refs` API 26 | - Added inline XML support 27 | - Removed `context` field in base `ReactComponent` class; 28 | it should be declared as needed, but can be restored by adding `-D react_deprecated_context` 29 | 30 | ### 1.8.0 31 | 32 | - Haxe 4.0.0 support 33 | - Fixed `ReactDOMServer` extern 34 | - `ReactComponentMacro` is now extensible 35 | 36 | ### 1.7.0 37 | 38 | - Added new Context API extern 39 | 40 | ### 1.6.0 41 | 42 | - Use `html-entities` library 43 | 44 | ### 1.5.0 45 | 46 | - Haxe 4 preview support 47 | - Improved breakpoint on JSX 48 | - Added PureComponent extern 49 | - Added new Refs API extern 50 | - Improvement and documentation of `@:jsxStatic` functionality 51 | 52 | ### 1.4.0 53 | 54 | - Generate `displayName` for `@:jsxStatic` components #86 55 | - React 16.2: added Fragments support #87: https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html 56 | - User overloads instead of `EitherType` for `setState` #91 57 | - Added utility function `ReactUtil.copyWithout` #96 58 | - ReactComponent macros refactoring #97 59 | - Travis CI 60 | 61 | ### 1.3.0 62 | 63 | - React 16 support; React 15 is still compatible but won't support new APIs (`componentDidCatch`, `createPortal`) 64 | - added missing `ReactDOM.hydrate` method (server-side rendering) 65 | - added `@:jsxStatic` optional meta 66 | - breaking: `react.ReactPropTypes` now requires the NPM `prop-types` module 67 | 68 | ### 1.2.1 69 | 70 | - fixed auto-complete issue on `this.state` caused by the `1.2.0` changes 71 | 72 | ### 1.2.0 73 | 74 | - `setState` now accepts `Partial`; where `T` is a `typedef`, `Partial` is `T` will all the fields made optional 75 | - `react.React.PropTypes` removed in favor of `react.ReactPropTypes` 76 | - added `-D react_runtime_warnings` option 77 | -------------------------------------------------------------------------------- /doc/hoc-wrap.md: -------------------------------------------------------------------------------- 1 | # Wrapping your components in HOCs 2 | 3 | You can use HOCs with your components by adding `@:wrap` meta. 4 | 5 | Note: not compatible with `@:jsxStatic` meta. 6 | 7 | In JavaScript it may look like that: 8 | ```javascript 9 | import React from 'react'; 10 | import { withRouter } from 'react-router'; 11 | 12 | class MyComponent extends React.Component { 13 | render() { 14 | return ( 15 |

Current path is {this.props.location.pathname}

16 | ); 17 | } 18 | } 19 | 20 | // HOC wrap 21 | export default withRouter(MyComponent); 22 | ``` 23 | 24 | In Haxe it will be: 25 | 26 | ```haxe 27 | import react.ReactComponent; 28 | import react.ReactMacro.jsx; 29 | import react.router.ReactRouter; 30 | import react.router.Route.RouteRenderProps; 31 | 32 | @:wrap(ReactRouter.withRouter) 33 | class MyComponent extends ReactComponentOfProps { 34 | function render() { 35 | return jsx( 36 | '

Current path is ${props.location.pathname}

' 37 | ); 38 | } 39 | } 40 | ``` 41 | 42 | You can add multiple `@:wrap` metas: 43 | 44 | ```haxe 45 | import react.ReactComponent; 46 | import react.ReactMacro.jsx; 47 | import react.React.CreateElementType; 48 | import react.router.ReactRouter; 49 | import react.router.Route.RouteRenderProps; 50 | 51 | // combined props 52 | private typedef Props = { 53 | > RouteRenderProps, 54 | var answer: Int; 55 | } 56 | 57 | @:wrap(ReactRouter.withRouter) 58 | @:wrap(uselessHoc(42)) 59 | class MyComponent extends ReactComponentOfProps { 60 | 61 | static function uselessHoc(value:Int) { 62 | return function(Comp: CreateElementType) { 63 | return function(props: Any) { 64 | return jsx('<$Comp {...props} answer=${value} />'); 65 | }; 66 | }; 67 | } 68 | 69 | function render() { 70 | return jsx(' 71 |

72 | Current path is ${props.location.pathname} and the answer is ${props.answer} 73 |

74 | '); 75 | } 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /doc/react-api-jsx.md: -------------------------------------------------------------------------------- 1 | # React API and JSX 2 | 3 | Most of the regular React API is integrated (like `React.createElement`), 4 | and the library uses a compile-time macro to parse JSX and generate 5 | the same kind of code that Facebook's JSX, Babel and Typescript will. 6 | 7 | ```haxe 8 | import react.React; 9 | import react.ReactDOM; 10 | import react.ReactMacro.jsx; 11 | import Browser.document; 12 | 13 | class App extends ReactComponent { 14 | 15 | static public function main() { 16 | ReactDOM.render( 17 | jsx(''), 18 | document.getElementById('root') 19 | ); 20 | } 21 | 22 | override function render() { 23 | var cname = 'it-bops'; 24 | return jsx(' 25 |
26 |

Hello React

27 |
28 | '); 29 | } 30 | } 31 | ``` 32 | 33 | Tips: 34 | 35 | - JSX has limitations, check the gotchas below, 36 | - Both classic JSX `{var}` binding and Haxe string interpolation are allowed: 37 | `attr=$var` / `${expression}` / `<$Comp>`. 38 | String interpolation can help for code completion/navigation. 39 | - Spread operator and complex expressions within curly braces are supported. 40 | 41 | Note: when writing externs, make sure to `extend ReactComponent` 42 | 43 | ```haxe 44 | @:jsRequire('react-redux', 'Provider') 45 | extern class Provider extends ReactComponent { } 46 | ``` 47 | 48 | ### JSX gotchas 49 | 50 | 1. JSX must be a String literal! 51 | **Do not concatenate Strings** to construct the JSX expression 52 | 53 | 2. JSX parser is not "re-entrant" 54 | 55 | In JavaScript you can nest JSX inside curly-brace expressions: 56 | ```javascript 57 | return ( 58 |
{ isA ? : }
59 | ); 60 | ``` 61 | 62 | However this isn't allowed in Haxe, so you must extract nested JSX into variables: 63 | ```haxe 64 | var content = isA ? jsx() : jsx(); 65 | return jsx(
{content}
); 66 | ``` 67 | 68 | ## Feature flags 69 | 70 | To control React features that should be enabled, depending on your target React version, 71 | use `-D react_ver=`, like `-D react_ver=16.3` if you want to restrict to `16.3`. 72 | 73 | Otherwise all the features will be turned on: 74 | 75 | - `react_fragments`: e.g ``, since React 16.2 76 | - `react_context_api`: e.g. `React.createContext`, since React 16.3 77 | - `react_ref_api`: e.g. `React.createRef`, since React 16.3 78 | - `react_snapshot_api`: e.g. `getSnapshotBeforeUpdate`, since React 16.3 79 | - `react_unsafe_lifecycle`: e.g. `UNSAFE_componentWillMount`, since React 16.9 80 | 81 | ## Components strict typing 82 | 83 | The default `ReactComponent` type is a shorthand for 84 | `ReactComponentOf`, a fully untyped component. 85 | 86 | To benefit from Haxe's strict typing you should look into extending a stricter base class: 87 | 88 | ```haxe 89 | class ReactComponentOf {...} 90 | typedef ReactComponentOfProps = ReactComponentOf; 91 | typedef ReactComponentOfState = ReactComponentOf; 92 | ``` 93 | 94 | The `Empty` type is an alias to `{}`, which means: 95 | - `ReactComponentOfProps` can NOT use any state, 96 | - `ReactComponentOfState` can NOT use any props. 97 | 98 | ### Special case 99 | 100 | `componentDidUpdate` exceptionally doesn't need to be overriden with all its 101 | parameters, as it's common in JS to omit or add just what is needed: 102 | since React 16.3 you should normally exactly override the function as: 103 | 104 | ```haxe 105 | override function componentDidUpdate(prevProps:Props, prevState:State, ?snapshot:Dynamic):Void { 106 | // ugh :( 107 | } 108 | 109 | override function componentDidUpdate() { 110 | // nicer, and valid! 111 | } 112 | ``` 113 | 114 | ## Optimization 115 | 116 | ### JSX compiler: inline ReactElements 117 | 118 | By default, when building for release (eg. without `-debug`), calls to `React.createElement` are replaced by inline JS objects (if possible). 119 | 120 | See: https://github.com/facebook/react/issues/3228 121 | 122 | ```javascript 123 | // regular 124 | return React.createElement('div', {key:'bar', className:'foo'}); 125 | 126 | // inlined (simplified) 127 | return {$$typeof:Symbol.for('react.element'), type:'div', props:{className:'foo'}, key:'bar'} 128 | ``` 129 | 130 | This behaviour can be **disabled** using `-D react_no_inline`. 131 | 132 | ## Warning for avoidable renders 133 | 134 | Setting `-D react_runtime_warnings` will enable runtime warnings for avoidable renders. 135 | 136 | This will add a `componentDidUpdate` (or update the existing one) where a 137 | **shallowCompare** is done on current and previous props and state. If both did 138 | not change, a warning will be displayed in the console. 139 | 140 | False positives can happen if your props are not flat, due to the shallowCompare. 141 | -------------------------------------------------------------------------------- /doc/react-dependency.md: -------------------------------------------------------------------------------- 1 | # React JS dependency 2 | 3 | Haxe React doesn't automatically includes React.js. 4 | 5 | There are 2 ways to link the React JS library: 6 | 7 | ## Require method (default) 8 | 9 | By default the library uses `require('react')` to reference React JS, 10 | which means that you need to use `npm` and `package.json` to manage 11 | your JS dependencies. 12 | 13 | (1) use `npm` to install this dependency: 14 | 15 | ```bash 16 | npm init 17 | npm install react react-dom 18 | # or for a specific version 19 | npm install react@16.3 react-dom@16.3 20 | ``` 21 | 22 | (2) and use a second build step to generate the JS "bundle", that is a 23 | single JS file including both your Haxe JS output and any npm libraries 24 | that it's refering to. 25 | 26 | ### Example using Browserify 27 | 28 | ```bash 29 | npm install browserify watchify 30 | # bundle once 31 | npx browserify haxe-output.js -o bundle.js 32 | # bundle automatically (and debug friendly) 33 | npx watchify haxe-output.js -o bundle.js --debug 34 | ``` 35 | 36 | ### Example using Webpack (without config) 37 | 38 | ```bash 39 | npm install webpack webpack-cli 40 | # bundle once 41 | npx webpack haxe-output.js -o bundle.js 42 | # bundle automatically (and debug friendly) 43 | npx webpack haxe-output.js -o bundle.js -w --mode development 44 | ``` 45 | 46 | For a more complexe setup of Webpack + React, look at: 47 | 48 | - https://github.com/elsassph/webpack-haxe-example/tree/react 49 | 50 | ## Global JS method 51 | 52 | The other common method is to download or reference the standalone 53 | JS files of React JS in your HTML page. These JS files will declare 54 | React in the "global scope" of the browser. 55 | 56 | ```html 57 | 58 | 59 | ``` 60 | 61 | In this case you must compile with the following flag: 62 | 63 | -D react_global 64 | 65 | Look at `samples/todoapp` for an example of this approach. 66 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro react.ReactTypeMacro.setFlags() 2 | --macro react.jsx.JsxStaticMacro.addHook() 3 | --macro addGlobalMetadata('', '@:build(react.jsx.JsxStaticMacro.build())') 4 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "license": "MIT", 4 | "tags": [], 5 | "description": "Haxe React is a set of externs and tools to use Facebook React with Haxe", 6 | "contributors": [ 7 | "massive","elsassph" 8 | ], 9 | "releasenote": "See https://github.com/massiveinteractive/haxe-react/blob/master/readme.md", 10 | "version": "1.12.0", 11 | "url": "https://github.com/massiveinteractive/haxe-react", 12 | "classPath": "src/lib", 13 | "dependencies": 14 | { 15 | "html-entities": "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mdk/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-react", 3 | "version": "1.12.0" 4 | } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Haxe React 2 | 3 | [![TravisCI Build Status](https://travis-ci.org/massiveinteractive/haxe-react.svg?branch=master)](https://travis-ci.org/massiveinteractive/haxe-react) 4 | [![Haxelib Version](https://img.shields.io/github/tag/massiveinteractive/haxe-react.svg?label=haxelib)](http://lib.haxe.org/p/react) 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 8 | type system and compile time macros to offer a strongly typed language to work 9 | with the popular [React](https://facebook.github.io/react/) library. 10 | 11 | haxelib install react 12 | 13 | # Documentation (RTFM) 14 | 15 | ## Topics 16 | 17 | - [React API and JSX](doc/react-api-jsx.md) - what's the syntax? 18 | - [React JS library dependency](doc/react-dependency.md) - how to bundle? 19 | - [Static/functional components](doc/static-components.md) - advanced syntax 20 | - [High-order components](doc/hoc-wrap.md) - wrapping components 21 | 22 | ## Support / discussions 23 | 24 | If you have questions / issues, join [haxe-react on Gitter.im](https://gitter.im/haxe-react/Lobby) 25 | 26 | ## Roadmap 27 | 28 | The following fork ("react-next") works on major evolutions to the library: 29 | 30 | - https://github.com/kLabz/haxe-react 31 | 32 | # Learn more 33 | 34 | ## What's included / not included 35 | 36 | This library covers React core and ReactDOM. 37 | It does NOT cover: ReactAddOns, react-router, Redux, or React Native. 38 | 39 | Biggest source of up to date React libraries for Haxe: 40 | 41 | - https://github.com/haxe-react 42 | 43 | Useful to see how to write quick 3rd party React externs: 44 | 45 | - https://github.com/tokomlabs/haxe-react-addons 46 | 47 | ## Application examples 48 | 49 | React doesn't enforce any specific application architecture; 50 | here are a few examples to get started: 51 | 52 | Using **Redux**, Haxe-style: 53 | 54 | - https://github.com/elsassph/haxe-react-redux 55 | 56 | Using **Webpack**: 57 | 58 | - https://github.com/elsassph/webpack-haxe-example/tree/react 59 | -------------------------------------------------------------------------------- /releaseHaxelib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f haxe-react.zip 3 | zip -r haxe-react.zip src haxelib.json readme.md changes.md 4 | haxelib submit haxe-react.zip $1 $2 --always 5 | -------------------------------------------------------------------------------- /samples/todoapp/bin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo React 5 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 5 | -lib msignal 6 | -D react_global 7 | -D react_ver=16.3 8 | #-D react_no_inline 9 | -debug 10 | -dce full 11 | -------------------------------------------------------------------------------- /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 | ReactDOM.render(jsx('<$TodoApp/>'), Browser.document.getElementById('app')); 13 | } 14 | } -------------------------------------------------------------------------------- /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.React; 4 | import react.ReactComponent; 5 | import react.ReactMacro.jsx; 6 | import js.html.InputElement; 7 | import store.TodoActions; 8 | import store.TodoItem; 9 | import store.TodoStore; 10 | 11 | typedef TodoAppState = { 12 | items:Array 13 | } 14 | 15 | class TodoApp extends ReactComponentOfState 16 | { 17 | var todoStore = new TodoStore(); 18 | var input: js.html.InputElement; 19 | 20 | public function new(props:Dynamic) 21 | { 22 | super(props); 23 | 24 | state = { items:todoStore.list }; 25 | 26 | todoStore.changed.add(function() { 27 | setState({ items:todoStore.list }); 28 | }); 29 | } 30 | 31 | override function componentDidUpdate() { // notice: args optional here 32 | trace('App updated...'); 33 | } 34 | 35 | override public function render() 36 | { 37 | var unchecked = state.items.filter(function(item) return !item.checked).length; 38 | 39 | var listProps = { data:state.items }; 40 | return jsx(' 41 |
42 |
43 | 44 | 45 |
46 |
47 | <$TodoList ref={mountList} {...listProps} className="list"/> 48 |
49 |
$unchecked task(s) left
50 |
51 | '); 52 | } 53 | 54 | function setInput(ref: js.html.InputElement) { 55 | input = ref; 56 | } 57 | 58 | function mountList(comp:ReactComponent) 59 | { 60 | trace('List mounted ' + comp.props); 61 | } 62 | 63 | function addItem() 64 | { 65 | var text = input.value; 66 | if (text.length > 0) 67 | { 68 | TodoActions.addItem.dispatch(text); 69 | input.value = ""; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 ReactComponentOfProps 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 ReactComponentOfProps 64 | { 65 | var checked:Bool; 66 | 67 | static var defaultProps:TodoItemProps = { 68 | padding: '10px', 69 | border: 'solid 1px #363' 70 | } 71 | 72 | public function new(props:TodoItemProps) 73 | { 74 | super(props); 75 | } 76 | 77 | override public function shouldComponentUpdate(nextProps:TodoItemProps, nextState:{}):Bool 78 | { 79 | return nextProps.data.checked != checked; 80 | } 81 | 82 | override public function render() 83 | { 84 | var style = { 85 | padding: props.padding, 86 | border: props.border 87 | }; 88 | var entry:TodoItem = props.data; 89 | checked = entry.checked; 90 | var id = 'item-${entry.id}'; 91 | return jsx(' 92 |
  • 93 | ${entry.label} 94 |
  • 95 | '); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/react/Empty.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | typedef Empty = {} 4 | 5 | -------------------------------------------------------------------------------- /src/lib/react/Fragment.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | /** 4 | Warning: Fragments are only available in react 16.2.0+ 5 | https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html 6 | **/ 7 | #if react_fragments 8 | @:native('React.Fragment') 9 | extern class Fragment extends ReactComponent { 10 | #if debug 11 | static function __init__():Void 12 | { 13 | var version = react.React.version.split('.') 14 | .filter(function(s) return s != '.') 15 | .map(untyped parseInt); 16 | 17 | if (version[0] < 16 || version[0] == 16 && version[1] < 2) 18 | js.Browser.console.warn('Fragments are not available on react < 16.2.0'); 19 | } 20 | #end 21 | } 22 | #end 23 | -------------------------------------------------------------------------------- /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 | class PartialMacro { 19 | #if macro 20 | static function build() 21 | { 22 | switch Context.getLocalType() 23 | { 24 | // 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) 25 | case TInst(_, [Context.followWithAbstracts(_) => TypeTools.toComplexType(_) => TAnonymous(fields)]): 26 | // Add @:optional meta to all fields 27 | var newFields = fields.map(addMeta); 28 | return TAnonymous(newFields); 29 | 30 | default: 31 | Context.fatalError('Type parameter should be an anonymous structure', Context.currentPos()); 32 | } 33 | 34 | return null; 35 | } 36 | 37 | static function addMeta(field: Field): Field 38 | { 39 | // Handle Null and optional fields already parsed by the compiler 40 | var kind = switch (field.kind) { 41 | case FVar(TPath({ 42 | name: 'StdTypes', 43 | sub: 'Null', 44 | params: [TPType(TPath(tpath))] 45 | }), write): 46 | FVar(TPath(tpath), write); 47 | 48 | default: 49 | field.kind; 50 | } 51 | 52 | return { 53 | name: field.name, 54 | kind: kind, 55 | access: field.access, 56 | meta: field.meta.concat([{ 57 | name: ':optional', 58 | params: [], 59 | pos: Context.currentPos() 60 | }]), 61 | pos: field.pos 62 | }; 63 | } 64 | #end 65 | } 66 | -------------------------------------------------------------------------------- /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 | typedef PureComponentOfPropsAndState = PureComponentOf; 9 | 10 | #if (!react_global) 11 | @:jsRequire("react", "PureComponent") 12 | #end 13 | @:native('React.PureComponent') 14 | @:keepSub 15 | extern class PureComponentOf 16 | extends ReactComponentOf 17 | {} 18 | -------------------------------------------------------------------------------- /src/lib/react/React.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactElement; 4 | 5 | /** 6 | https://facebook.github.io/react/docs/react-api.html 7 | **/ 8 | #if (!react_global) 9 | @:jsRequire("react") 10 | #end 11 | @:native('React') 12 | extern class React 13 | { 14 | // Warning: react.React.PropTypes is deprecated, reference as react.ReactPropTypes 15 | 16 | /** 17 | https://facebook.github.io/react/docs/react-api.html#createelement 18 | **/ 19 | public static function createElement(type:CreateElementType, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement; 20 | 21 | /** 22 | https://facebook.github.io/react/docs/react-api.html#cloneelement 23 | **/ 24 | public static function cloneElement(element:ReactElement, ?attrs:Dynamic, children:haxe.extern.Rest):ReactElement; 25 | 26 | /** 27 | https://facebook.github.io/react/docs/react-api.html#isvalidelement 28 | **/ 29 | public static function isValidElement(object:Dynamic):Bool; 30 | 31 | #if react_context_api 32 | /** 33 | https://reactjs.org/docs/context.html#reactcreatecontext 34 | Note: this API has been introduced in React 16.3 35 | **/ 36 | public static function createContext(?defaultValue:T, ?calculateChangedBits: T->T->Int):ReactContext; 37 | #end 38 | 39 | #if react_ref_api 40 | /** 41 | https://reactjs.org/docs/react-api.html#reactcreateref 42 | 43 | Note: this API has been introduced in React 16.3 44 | If you are using an earlier release of React, use callback refs instead 45 | https://reactjs.org/docs/refs-and-the-dom.html#callback-refs 46 | **/ 47 | public static function createRef():ReactRef; 48 | #end 49 | 50 | #if react_ref_api 51 | /** 52 | https://reactjs.org/docs/react-api.html#reactforwardref 53 | See also https://reactjs.org/docs/forwarding-refs.html 54 | 55 | Note: this API has been introduced in React 16.3 56 | If you are using an earlier release of React, use callback refs instead 57 | https://reactjs.org/docs/refs-and-the-dom.html#callback-refs 58 | **/ 59 | public static function forwardRef(render:TProps->ReactRef->ReactElement):CreateElementType; 60 | #end 61 | 62 | /** 63 | https://facebook.github.io/react/docs/react-api.html#react.children 64 | **/ 65 | public static var Children:ReactChildren; 66 | 67 | public static var version:String; 68 | } 69 | 70 | /** 71 | https://facebook.github.io/react/docs/react-api.html#react.children 72 | **/ 73 | extern interface ReactChildren 74 | { 75 | /** 76 | https://facebook.github.io/react/docs/react-api.html#react.children.map 77 | **/ 78 | function map(children:Dynamic, fn:ReactElement->ReactElement):Dynamic; 79 | 80 | /** 81 | https://facebook.github.io/react/docs/react-api.html#react.children.foreach 82 | **/ 83 | function foreach(children:Dynamic, fn:ReactElement->Void):Void; 84 | 85 | /** 86 | https://facebook.github.io/react/docs/react-api.html#react.children.count 87 | **/ 88 | function count(children:Dynamic):Int; 89 | 90 | /** 91 | https://facebook.github.io/react/docs/react-api.html#react.children.only 92 | **/ 93 | function only(children:Dynamic):ReactElement; 94 | 95 | /** 96 | https://facebook.github.io/react/docs/react-api.html#react.children.toarray 97 | **/ 98 | function toArray(children:Dynamic):Array; 99 | } 100 | 101 | typedef CreateElementType = haxe.extern.EitherType, Class>; 102 | 103 | #if react_context_api 104 | typedef ReactContext = { 105 | var displayName:String; 106 | var Provider:{value:T}->ReactElement; 107 | var Consumer:T->ReactElement; 108 | } 109 | #end 110 | -------------------------------------------------------------------------------- /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 | typedef ReactComponentProps = { 10 | /** 11 | Children have to be manipulated using React.Children.* 12 | **/ 13 | @:optional var children:Dynamic; 14 | } 15 | 16 | /** 17 | https://facebook.github.io/react/docs/react-component.html 18 | **/ 19 | typedef ReactComponent = ReactComponentOf; 20 | 21 | typedef ReactComponentOfProps = ReactComponentOf; 22 | typedef ReactComponentOfState = ReactComponentOf; 23 | 24 | // Keep the old ReactComponentOfPropsAndState typedef available a few versions 25 | typedef ReactComponentOfPropsAndState = ReactComponentOf; 26 | 27 | #if (!react_global) 28 | @:jsRequire("react", "Component") 29 | #end 30 | @:native('React.Component') 31 | @:keepSub 32 | @:autoBuild(react.ReactComponentMacro.build()) 33 | extern class ReactComponentOf 34 | { 35 | var props(default, null):TProps; 36 | var state(default, null):TState; 37 | 38 | #if react_deprecated_context 39 | // It's better to define it in your ReactComponent subclass as needed, with the right typing. 40 | var context(default, null):Dynamic; 41 | #end 42 | 43 | function new(?props:TProps, ?context:Dynamic); 44 | 45 | /** 46 | https://facebook.github.io/react/docs/react-component.html#forceupdate 47 | **/ 48 | function forceUpdate(?callback:Void -> Void):Void; 49 | 50 | /** 51 | https://facebook.github.io/react/docs/react-component.html#setstate 52 | **/ 53 | @:overload(function(nextState:TState, ?callback:Void -> Void):Void {}) 54 | @:overload(function(nextState:TState -> TProps -> TState, ?callback:Void -> Void):Void {}) 55 | function setState(nextState:TState -> TState, ?callback:Void -> Void):Void; 56 | 57 | /** 58 | https://facebook.github.io/react/docs/react-component.html#render 59 | **/ 60 | function render():ReactElement; 61 | 62 | /** 63 | https://facebook.github.io/react/docs/react-component.html#componentwillmount 64 | **/ 65 | #if react_unsafe_lifecycle 66 | function UNSAFE_componentWillMount():Void; 67 | #else 68 | function componentWillMount():Void; 69 | #end 70 | 71 | /** 72 | https://facebook.github.io/react/docs/react-component.html#componentdidmount 73 | **/ 74 | function componentDidMount():Void; 75 | 76 | /** 77 | https://facebook.github.io/react/docs/react-component.html#componentwillunmount 78 | **/ 79 | function componentWillUnmount():Void; 80 | 81 | /** 82 | https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops 83 | **/ 84 | #if react_unsafe_lifecycle 85 | function UNSAFE_componentWillReceiveProps(nextProps:TProps):Void; 86 | #else 87 | function componentWillReceiveProps(nextProps:TProps):Void; 88 | #end 89 | 90 | /** 91 | https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate 92 | **/ 93 | dynamic function shouldComponentUpdate(nextProps:TProps, nextState:TState):Bool; 94 | 95 | /** 96 | https://facebook.github.io/react/docs/react-component.html#componentwillupdate 97 | **/ 98 | #if react_unsafe_lifecycle 99 | function UNSAFE_componentWillUpdate(nextProps:TProps, nextState:TState):Void; 100 | #else 101 | function componentWillUpdate(nextProps:TProps, nextState:TState):Void; 102 | #end 103 | 104 | /** 105 | https://facebook.github.io/react/docs/react-component.html#componentdidupdate 106 | Note: Updated to version introduced in React 16.3 107 | **/ 108 | #if react_snapshot_api 109 | function componentDidUpdate(prevProps:TProps, prevState:TState, ?snapshot:Dynamic):Void; 110 | #else 111 | function componentDidUpdate(prevProps:TProps, prevState:TState):Void; 112 | #end 113 | 114 | /** 115 | https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html 116 | **/ 117 | function componentDidCatch(error:Error, info:{ componentStack:String }):Void; 118 | 119 | #if react_snapshot_api 120 | /** 121 | https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate 122 | Note: this API has been introduced in React 16.3 123 | **/ 124 | function getSnapshotBeforeUpdate(prevProps:TProps, prevState:TState):Dynamic; 125 | #end 126 | 127 | #if (js && !debug && !react_no_inline) 128 | static function __init__():Void { 129 | // required magic value to tag literal react elements 130 | #if !haxe4 131 | untyped __js__("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 132 | #else 133 | js.Syntax.code("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 134 | #end 135 | } 136 | #end 137 | } 138 | 139 | typedef ReactElement = { 140 | type:Dynamic, 141 | props:Dynamic, 142 | ?key:Dynamic, 143 | ?ref:Dynamic 144 | } 145 | -------------------------------------------------------------------------------- /src/lib/react/ReactComponentMacro.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | #if macro 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | 8 | import react.wrap.ReactWrapperMacro; 9 | 10 | typedef Builder = ClassType -> Array -> Array; 11 | typedef BuilderWithKey = {?key:String, build:Builder}; 12 | 13 | class ReactComponentMacro { 14 | static public inline var REACT_COMPONENT_BUILDER = "ReactComponent"; 15 | 16 | static var builders:Array = [ 17 | {build: ReactMacro.buildComponent, key: REACT_COMPONENT_BUILDER}, 18 | {build: ReactTypeMacro.alterComponentSignatures, key: ReactTypeMacro.ALTER_SIGNATURES_BUILDER}, 19 | {build: ReactWrapperMacro.buildComponent, key: ReactWrapperMacro.WRAP_BUILDER}, 20 | 21 | #if !react_ignore_empty_render 22 | {build: ReactTypeMacro.ensureRenderOverride, key: ReactTypeMacro.ENSURE_RENDER_OVERRIDE_BUILDER}, 23 | #end 24 | 25 | #if (debug && react_runtime_warnings) 26 | {build: ReactDebugMacro.buildComponent, key: ReactDebugMacro.REACT_DEBUG_BUILDER} 27 | #end 28 | ]; 29 | 30 | static public function appendBuilder(builder:Builder, ?key:String):Void { 31 | builders.push({build: builder, key: key}); 32 | } 33 | 34 | static public function prependBuilder(builder:Builder, ?key:String):Void { 35 | builders.unshift({build: builder, key: key}); 36 | } 37 | 38 | static public function hasBuilder(key:String):Bool { 39 | if (key == null) return false; 40 | return Lambda.exists(builders, function(b) return b.key == key); 41 | } 42 | 43 | static public function insertBuilderBefore(before:String, builder:Builder, ?key:String):Void { 44 | var index = -1; 45 | if (before != null) { 46 | for (i in 0...builders.length) { 47 | if (builders[i].key == before) { 48 | index = i; 49 | break; 50 | } 51 | } 52 | } 53 | 54 | if (index == -1) return appendBuilder(builder, key); 55 | builders.insert(index, {build: builder, key: key}); 56 | } 57 | 58 | static public function insertBuilderAfter(after:String, builder:Builder, ?key:String):Void { 59 | var index = -1; 60 | if (after != null) { 61 | for (i in 0...builders.length) { 62 | if (builders[i].key == after) { 63 | index = i + 1; 64 | break; 65 | } 66 | } 67 | } 68 | 69 | if (index == -1) return appendBuilder(builder, key); 70 | builders.insert(index, {build: builder, key: key}); 71 | } 72 | 73 | static public function build():Array 74 | { 75 | var inClass = Context.getLocalClass().get(); 76 | 77 | return Lambda.fold( 78 | builders, 79 | function(builder, fields) return builder.build(inClass, fields), 80 | Context.getBuildFields() 81 | ); 82 | } 83 | } 84 | #end 85 | 86 | -------------------------------------------------------------------------------- /src/lib/react/ReactDOM.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent; 4 | import js.html.Element; 5 | 6 | /** 7 | https://facebook.github.io/react/docs/react-dom.html 8 | **/ 9 | #if (!react_global) 10 | @:jsRequire("react-dom") 11 | #end 12 | @:native('ReactDOM') 13 | extern class ReactDOM 14 | { 15 | /** 16 | https://facebook.github.io/react/docs/react-dom.html#render 17 | **/ 18 | public static function render(element:ReactElement, container:Element, ?callback:Void -> Void):ReactElement; 19 | 20 | /** 21 | https://facebook.github.io/react/docs/react-dom.html#hydrate 22 | **/ 23 | public static function hydrate(element:ReactElement, container:Element, ?callback:Void -> Void):ReactElement; 24 | 25 | /** 26 | https://facebook.github.io/react/docs/react-dom.html#unmountcomponentatnode 27 | **/ 28 | public static function unmountComponentAtNode(container:Element):Bool; 29 | 30 | /** 31 | https://facebook.github.io/react/docs/react-dom.html#finddomnode 32 | **/ 33 | public static function findDOMNode(component:ReactComponent):Element; 34 | 35 | /** 36 | https://reactjs.org/docs/react-dom.html#createportal 37 | **/ 38 | public static function createPortal(child:ReactElement, container:Element):ReactElement; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/react/ReactDOMServer.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent; 4 | 5 | #if nodejs 6 | import js.node.stream.Readable; 7 | 8 | @:native('ReactMarkupReadableStream') 9 | @:jsRequire('react-dom/server/ReactDOMNodeStreamRenderer', 'ReactMarkupReadableStream') 10 | class ReactMarkupReadableStream extends Readable {} 11 | #end 12 | 13 | /** 14 | https://facebook.github.io/react/docs/react-dom-server.html 15 | **/ 16 | #if (!react_global) 17 | @:jsRequire('react-dom/server') 18 | #end 19 | @:native('ReactDOMServer') 20 | extern class ReactDOMServer 21 | { 22 | /** 23 | https://facebook.github.io/react/docs/react-dom-server.html#rendertostring 24 | **/ 25 | public static function renderToString(component:ReactElement):String; 26 | 27 | /** 28 | https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup 29 | **/ 30 | public static function renderToStaticMarkup(component:ReactElement):String; 31 | 32 | #if nodejs 33 | /** 34 | https://reactjs.org/docs/react-dom-server.html#rendertonodestream 35 | **/ 36 | public static function renderToNodeStream(component:ReactElement):ReactMarkupReadableStream; 37 | 38 | /** 39 | https://reactjs.org/docs/react-dom-server.html#rendertostaticnodestream 40 | **/ 41 | public static function renderToStaticNodeStream(component:ReactElement):ReactMarkupReadableStream; 42 | #end 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/react/ReactDebugMacro.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.macro.Context; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.TypeTools; 7 | 8 | class ReactDebugMacro 9 | { 10 | public static inline var IGNORE_RENDER_WARNING_META = ':ignoreRenderWarning'; 11 | public static inline var REACT_DEBUG_BUILDER = 'ReactDebug'; 12 | public static var firstRenderWarning:Bool = true; 13 | 14 | #if macro 15 | public static function buildComponent(inClass:ClassType, fields:Array):Array 16 | { 17 | var pos = Context.currentPos(); 18 | var propsType:ComplexType = macro :Dynamic; 19 | var stateType:ComplexType = macro :Dynamic; 20 | 21 | switch (inClass.superClass) 22 | { 23 | case {params: params, t: _.toString() => cls} 24 | if (cls == 'react.ReactComponentOf' || cls == 'react.PureComponentOf'): 25 | propsType = TypeTools.toComplexType(params[0]); 26 | stateType = TypeTools.toComplexType(params[1]); 27 | 28 | default: 29 | } 30 | 31 | if (!inClass.meta.has(IGNORE_RENDER_WARNING_META)) 32 | if (!updateComponentUpdate(fields, inClass, propsType, stateType)) 33 | addComponentUpdate(fields, inClass, propsType, stateType); 34 | 35 | return fields; 36 | } 37 | 38 | static function updateComponentUpdate( 39 | fields:Array, 40 | inClass:ClassType, 41 | propsType:ComplexType, 42 | stateType:ComplexType 43 | ) { 44 | for (field in fields) 45 | { 46 | if (field.name == "componentDidUpdate") 47 | { 48 | switch (field.kind) { 49 | case FFun(f): 50 | #if react_snapshot_api 51 | if (f.args.length != 3) 52 | return Context.error('componentDidUpdate should accept three arguments', inClass.pos); 53 | #else 54 | if (f.args.length != 2) 55 | return Context.error('componentDidUpdate should accept two arguments', inClass.pos); 56 | #end 57 | 58 | f.expr = macro { 59 | ${exprComponentDidUpdate(inClass, f.args[0].name, f.args[1].name)} 60 | ${f.expr} 61 | }; 62 | 63 | return true; 64 | default: 65 | } 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | static function addComponentUpdate( 73 | fields:Array, 74 | inClass:ClassType, 75 | propsType:ComplexType, 76 | stateType:ComplexType 77 | ) { 78 | var componentDidUpdate = { 79 | args: [ 80 | { 81 | meta: [], 82 | name: "prevProps", 83 | type: propsType, 84 | opt: false, 85 | value: null 86 | }, 87 | { 88 | meta: [], 89 | name: "prevState", 90 | type: stateType, 91 | opt: false, 92 | value: null 93 | }, 94 | #if react_snapshot_api 95 | { 96 | meta: [], 97 | name: "snapshot", 98 | type: macro :Dynamic, 99 | opt: true, 100 | value: null 101 | } 102 | #end 103 | ], 104 | ret: macro :Void, 105 | expr: exprComponentDidUpdate(inClass, "prevProps", "prevState") 106 | } 107 | 108 | fields.push({ 109 | name: 'componentDidUpdate', 110 | access: [APublic, AOverride], 111 | kind: FFun(componentDidUpdate), 112 | pos: inClass.pos 113 | }); 114 | } 115 | 116 | static function exprComponentDidUpdate(inClass:ClassType, prevProps:String, prevState:String) 117 | { 118 | return macro { 119 | var propsAreEqual = react.ReactUtil.shallowCompare(this.props, $i{prevProps}); 120 | var statesAreEqual = react.ReactUtil.shallowCompare(this.state, $i{prevState}); 121 | 122 | if (propsAreEqual && statesAreEqual) 123 | { 124 | // Using Object.create(null) to avoid prototype for clean output 125 | var debugProps = untyped Object.create(null); 126 | debugProps.currentProps = this.props; 127 | debugProps.prevProps = $i{prevProps}; 128 | 129 | js.Browser.console.warn( 130 | 'Warning: avoidable re-render of `${$v{inClass.name}}`.\n', 131 | debugProps 132 | ); 133 | 134 | if (react.ReactDebugMacro.firstRenderWarning) 135 | { 136 | react.ReactDebugMacro.firstRenderWarning = false; 137 | 138 | js.Browser.console.warn( 139 | 'Make sure your props are flattened, or implement shouldComponentUpdate.\n' + 140 | 'See https://facebook.github.io/react/docs/optimizing-performance.html#shouldcomponentupdate-in-action' + 141 | '\n\nAlso note that legacy context API can trigger false positives if children ' + 142 | 'rely on context. You can hide this warning for a specific component by adding ' + 143 | '`@${IGNORE_RENDER_WARNING_META}` meta to its class.' 144 | ); 145 | } 146 | } 147 | } 148 | } 149 | #end 150 | } 151 | -------------------------------------------------------------------------------- /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/ReactMacro.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.jsx.JsxParser; 4 | import react.jsx.JsxSanitize; 5 | 6 | #if macro 7 | import haxe.macro.Context; 8 | import haxe.macro.Expr; 9 | import haxe.macro.ExprTools; 10 | import haxe.macro.Type; 11 | import react.jsx.JsxStaticMacro; 12 | 13 | #if !haxe4 14 | typedef ObjectField = {field:String, expr:Expr}; 15 | #end 16 | 17 | typedef ComponentInfo = { 18 | isExtern:Bool, 19 | props:Array 20 | } 21 | #end 22 | 23 | /** 24 | Provides a simple macro for parsing jsx into Haxe expressions. 25 | **/ 26 | class ReactMacro 27 | { 28 | public static macro function jsx(expr:ExprOf):Expr 29 | { 30 | if (Context.defined('display')) 31 | return switch(expr) { 32 | case macro @:markup $v{(s:String)}: macro @:pos(expr.pos) untyped $v{s}; 33 | case _: macro @:pos(expr.pos) untyped $e{expr}; 34 | }; 35 | else 36 | return parseJsx(switch(expr) { 37 | case macro @:markup $v{(s:String)}: s; 38 | case _: ExprTools.getValue(expr); 39 | }, expr.pos); 40 | } 41 | 42 | public static macro function sanitize(expr:ExprOf):Expr 43 | { 44 | return macro $v{JsxSanitize.process(ExprTools.getValue(expr))}; 45 | } 46 | 47 | /* PARSER */ 48 | 49 | #if macro 50 | static var componentsMap:Map = new Map(); 51 | 52 | static function parseJsx(jsx:String, pos:Position):Expr 53 | { 54 | jsx = JsxSanitize.process(jsx); 55 | var xml = 56 | try 57 | Xml.parse(jsx) 58 | #if (haxe_ver >= 3.3) 59 | catch(err:haxe.xml.Parser.XmlParserException) 60 | { 61 | var posInfos = Context.getPosInfos(pos); 62 | var realPos = Context.makePosition({ 63 | file: posInfos.file, 64 | min: posInfos.min + err.position, 65 | max: posInfos.max + err.position, 66 | }); 67 | Context.fatalError('Invalid JSX: ' + err.message, realPos); 68 | } 69 | #end 70 | catch(err:Dynamic) 71 | Context.fatalError('Invalid JSX: ' + err, err.pos ? err.pos : pos); 72 | 73 | var ast = JsxParser.process(xml); 74 | var expr = parseJsxNode(ast, pos); 75 | return expr; 76 | } 77 | 78 | static function parseJsxNode(ast:JsxAst, pos:Position) 79 | { 80 | switch (ast) 81 | { 82 | case JsxAst.Text(value): 83 | return macro $v{value}; 84 | 85 | case JsxAst.Expr(value): 86 | return Context.parse(value, pos); 87 | 88 | case JsxAst.Node(isHtml, path, attributes, jsxChildren): 89 | // parse type 90 | var type = isHtml ? macro $v{path[0]} : macro $p{path}; 91 | type.pos = pos; 92 | 93 | // handle @:jsxStatic 94 | if (!isHtml) JsxStaticMacro.handleJsxStaticProxy(type); 95 | 96 | // parse attributes 97 | var attrs = []; 98 | var spread = []; 99 | var key = null; 100 | var ref = null; 101 | for (attr in attributes) 102 | { 103 | var expr = parseJsxAttr(attr.value, pos); 104 | var name = attr.name; 105 | if (name == 'key') key = expr; 106 | else if (name == 'ref') ref = expr; 107 | else if (name.charAt(0) == '.') spread.push(expr); 108 | else attrs.push({ field:name, expr:expr }); 109 | } 110 | 111 | // parse children 112 | var children = [for (child in jsxChildren) parseJsxNode(child, pos)]; 113 | 114 | // inline declaration or createElement? 115 | var typeInfo = getComponentInfo(type); 116 | JsxStaticMacro.injectDisplayNames(type); 117 | var useLiteral = canUseLiteral(typeInfo, ref); 118 | if (useLiteral) 119 | { 120 | if (children.length > 0) 121 | { 122 | // single child should not be placed in an Array 123 | if (children.length == 1) attrs.push({field:'children', expr:macro ${children[0]}}); 124 | else attrs.push({field:'children', expr:macro ($a{children} :Array)}); 125 | } 126 | if (!isHtml) 127 | { 128 | var defaultProps = getDefaultProps(typeInfo, attrs); 129 | if (defaultProps != null) 130 | { 131 | var obj = {expr: EObjectDecl(defaultProps), pos: pos}; 132 | spread.unshift(obj); 133 | } 134 | } 135 | var props = makeProps(spread, attrs, pos); 136 | return genLiteral(type, props, ref, key, pos); 137 | } 138 | else 139 | { 140 | if (ref != null) attrs.unshift({field:'ref', expr:ref}); 141 | if (key != null) attrs.unshift({field:'key', expr:key}); 142 | 143 | var props = makeProps(spread, attrs, pos); 144 | 145 | var args = [type, props].concat(children); 146 | return macro @:pos(pos) react.React.createElement($a{args}); 147 | } 148 | } 149 | } 150 | 151 | static function parseJsxAttr(value:String, pos:Position) 152 | { 153 | var ast = JsxParser.parseText(value); 154 | return switch (ast) 155 | { 156 | case JsxAst.Text(value): 157 | return macro $v{value}; 158 | 159 | case JsxAst.Expr(value): 160 | return Context.parse(value, pos); 161 | 162 | default: null; 163 | } 164 | } 165 | 166 | static function genLiteral(type:Expr, props:Expr, ref:Expr, key:Expr, pos:Position) 167 | { 168 | if (key == null) key = macro null; 169 | if (ref == null) ref = macro null; 170 | 171 | var fields = [ 172 | #if !haxe4 173 | {field: "@$__hx__$$typeof", expr: macro untyped __js__("$$tre")}, 174 | #else 175 | {field: "$$typeof", expr: macro js.Syntax.code("$$tre")}, 176 | #end 177 | {field: 'type', expr: type}, 178 | {field: 'props', expr: props} 179 | ]; 180 | if (key != null) fields.push({field: 'key', expr: key}); 181 | if (ref != null) fields.push({field: 'ref', expr: ref}); 182 | var obj = {expr: EObjectDecl(fields), pos: pos}; 183 | 184 | return macro ($obj : react.ReactComponent.ReactElement); 185 | } 186 | 187 | static function canUseLiteral(typeInfo:ComponentInfo, ref:Expr) 188 | { 189 | #if (debug || react_no_inline) 190 | return false; 191 | #end 192 | 193 | // do not use literals for externs: we don't know their defaultProps 194 | if (typeInfo != null && typeInfo.isExtern) return false; 195 | 196 | // no ref is always ok 197 | if (ref == null) return true; 198 | 199 | // only refs as functions are allowed in literals, strings require the full createElement context 200 | return switch (Context.typeof(ref)) { 201 | case TFun(_): true; 202 | default: false; 203 | } 204 | } 205 | 206 | static function makeProps(spread:Array, attrs:Array, pos:Position) 207 | { 208 | #if (!debug && !react_no_inline) 209 | flattenSpreadProps(spread, attrs); 210 | #end 211 | 212 | return spread.length > 0 213 | ? makeSpread(spread, attrs, pos) 214 | : attrs.length == 0 ? macro {} : {pos:pos, expr:EObjectDecl(attrs)} 215 | } 216 | 217 | /** 218 | * Attempt flattening spread/default props into the user-defined props 219 | */ 220 | static function flattenSpreadProps(spread:Array, attrs:Array) 221 | { 222 | function hasAttr(name:String) { 223 | for (prop in attrs) if (prop.field == name) return true; 224 | return false; 225 | } 226 | var mergeProps = getSpreadProps(spread, []); 227 | if (mergeProps.length > 0) 228 | { 229 | for (prop in mergeProps) 230 | if (!hasAttr(prop.field)) attrs.push(prop); 231 | } 232 | } 233 | 234 | static function makeSpread(spread:Array, attrs:Array, pos:Position) 235 | { 236 | // single spread, no props 237 | if (spread.length == 1 && attrs.length == 0) 238 | return spread[0]; 239 | 240 | // combine using Object.assign 241 | var args = [macro {}].concat(spread); 242 | if (attrs.length > 0) args.push({pos:pos, expr:EObjectDecl(attrs)}); 243 | return macro (untyped Object).assign($a{args}); 244 | } 245 | 246 | /** 247 | * Flatten literal objects into the props 248 | */ 249 | static function getSpreadProps(spread:Array, props:Array) 250 | { 251 | if (spread.length == 0) return props; 252 | var last = spread[spread.length - 1]; 253 | return switch (last.expr) { 254 | case EObjectDecl(fields): 255 | spread.pop(); 256 | var newProps = props.concat(fields); 257 | // push props and recurse in case another literal object is in the list 258 | getSpreadProps(spread, newProps); 259 | default: 260 | props; 261 | } 262 | } 263 | 264 | /* METADATA */ 265 | 266 | /** 267 | * Process React components 268 | */ 269 | public static function buildComponent(inClass:ClassType, fields:Array):Array 270 | { 271 | var pos = Context.currentPos(); 272 | 273 | #if (!debug && !react_no_inline) 274 | storeComponentInfos(fields, inClass, pos); 275 | #end 276 | 277 | if (!inClass.isExtern) 278 | tagComponent(fields, inClass, pos); 279 | 280 | return fields; 281 | } 282 | 283 | /** 284 | * Extract component default props 285 | */ 286 | static function storeComponentInfos(fields:Array, inClass:ClassType, pos:Position) 287 | { 288 | var key = getClassKey(inClass); 289 | for (field in fields) 290 | if (field.name == 'defaultProps') 291 | { 292 | switch (field.kind) { 293 | case FieldType.FVar(_, _.expr => EObjectDecl(props)): 294 | componentsMap.set(key, { 295 | isExtern: inClass.isExtern, 296 | props: props.copy() 297 | }); 298 | return; 299 | default: 300 | break; 301 | } 302 | } 303 | componentsMap.set(key, { 304 | props:null, 305 | isExtern:inClass.isExtern 306 | }); 307 | } 308 | 309 | /** 310 | * For a given type, resolve default props and filter user-defined props out 311 | */ 312 | static function getDefaultProps(typeInfo:ComponentInfo, attrs:Array) 313 | { 314 | if (typeInfo == null) return null; 315 | 316 | if (typeInfo.props != null) 317 | return typeInfo.props.filter(function(defaultProp) { 318 | var name = defaultProp.field; 319 | for (prop in attrs) if (prop.field == name) return false; 320 | return true; 321 | }); 322 | return null; 323 | } 324 | 325 | /** 326 | * Annotate React components for run-time JS reflection 327 | */ 328 | static function tagComponent(fields:Array, inClass:ClassType, pos:Position) 329 | { 330 | #if !debug 331 | return 332 | #end 333 | 334 | addDisplayName(fields, inClass, pos); 335 | 336 | #if react_hot 337 | addTagSource(fields, inClass, pos); 338 | #end 339 | } 340 | 341 | static function addTagSource(fields:Array, inClass:ClassType, pos:Position) 342 | { 343 | // add a __fileName__ static field 344 | var className = inClass.name; 345 | var fileName = Context.getPosInfos(inClass.pos).file; 346 | 347 | fields.push({ 348 | name:'__fileName__', 349 | access:[Access.AStatic], 350 | kind:FieldType.FVar(null, macro $v{fileName}), 351 | pos:pos 352 | }); 353 | } 354 | 355 | static function addDisplayName(fields:Array, inClass:ClassType, pos:Position) 356 | { 357 | for (field in fields) 358 | if (field.name == 'displayName') return; 359 | 360 | // add 'displayName' static property to see class names in React inspector panel 361 | var className = macro $v{inClass.name}; 362 | var field:Field = { 363 | name:'displayName', 364 | access:[Access.AStatic, Access.APrivate], 365 | kind:FieldType.FVar(null, className), 366 | pos:pos 367 | } 368 | fields.push(field); 369 | return; 370 | } 371 | 372 | static function getComponentInfo(expr:Expr):ComponentInfo 373 | { 374 | var key = getExprKey(expr); 375 | return key != null ? componentsMap.get(key) : null; 376 | } 377 | 378 | static function getClassKey(inClass:ClassType) 379 | { 380 | var qname = inClass.pack.concat([inClass.name]).join('.'); 381 | return 'Class<$qname>'; 382 | } 383 | 384 | static function getExprKey(expr:Expr) 385 | { 386 | return try switch (Context.typeof(expr)) { 387 | case Type.TType(_.get() => t, _): t.name; 388 | default: null; 389 | } 390 | } 391 | #end 392 | } 393 | -------------------------------------------------------------------------------- /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/ReactPropTypes.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.extern.EitherType; 4 | import react.ReactComponent; 5 | #if haxe4 6 | import js.lib.Error; 7 | #else 8 | import js.Error; 9 | #end 10 | 11 | /** 12 | https://reactjs.org/docs/typechecking-with-proptypes.html 13 | **/ 14 | #if (!react_global) 15 | @:jsRequire('prop-types') 16 | #end 17 | @:native('PropTypes') 18 | extern class ReactPropTypes 19 | { 20 | static var any:ChainableTypeChecker; 21 | static var array:ChainableTypeChecker; 22 | static var bool:ChainableTypeChecker; 23 | static var func:ChainableTypeChecker; 24 | static var number:ChainableTypeChecker; 25 | static var object:ChainableTypeChecker; 26 | static var string:ChainableTypeChecker; 27 | static var symbol:ChainableTypeChecker; 28 | static var element:ChainableTypeChecker; 29 | static var node:ChainableTypeChecker; 30 | 31 | static var arrayOf:ArrayOfTypeChecker -> ChainableTypeChecker; 32 | static var instanceOf:Class -> ChainableTypeChecker; 33 | static var objectOf:ArrayOfTypeChecker -> ChainableTypeChecker; 34 | static var oneOf:Array -> ChainableTypeChecker; 35 | static var oneOfType:Array -> ChainableTypeChecker; 36 | static var shape:TypeShape->ChainableTypeChecker; 37 | static var exact:TypeShape->ChainableTypeChecker; 38 | 39 | static function checkPropTypes( 40 | typeSpecs:Dynamic, 41 | values:Dynamic, 42 | location:PropTypesLocation, 43 | componentName:String, 44 | ?getStack:Void -> Dynamic 45 | ):Dynamic; 46 | } 47 | 48 | typedef TypeChecker = EitherType; 49 | typedef ArrayOfTypeChecker = EitherType; 50 | typedef CustomTypeChecker = Dynamic -> String -> String -> Null; 51 | typedef CustomArrayOfTypeChecker = Array -> String -> String -> PropTypesLocation -> String -> Null; 52 | typedef TypeShape = Dynamic; 53 | 54 | @:enum abstract PropTypesLocation(String) from String { 55 | var Prop = 'prop'; 56 | var Context = 'context'; 57 | var ChildContext = 'child context'; 58 | } 59 | 60 | private typedef ChainableTypeChecker = { 61 | @:optional var isRequired:Dynamic; 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/react/ReactRef.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.Constraints.Function; 4 | 5 | #if react_ref_api 6 | @:callable 7 | abstract ReactRef(Function) { 8 | public var current(get, never):T; 9 | 10 | public function get_current():T { 11 | return untyped this.current; 12 | } 13 | } 14 | #end 15 | -------------------------------------------------------------------------------- /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/ReactTypeMacro.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import haxe.macro.ComplexTypeTools; 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | import haxe.macro.TypeTools; 8 | 9 | class ReactTypeMacro 10 | { 11 | static public inline var ALTER_SIGNATURES_BUILDER = 'AlterSignatures'; 12 | static public inline var ENSURE_RENDER_OVERRIDE_BUILDER = 'EnsureRenderOverride'; 13 | 14 | #if macro 15 | 16 | // define React feature flags based on -D react_ver (default to latest) 17 | public static function setFlags() { 18 | var ver = Context.defined("react_ver") ? Context.definedValue("react_ver") : "16.12"; 19 | var match = ~/([0-9]+).([0-9]+)/; 20 | if (!match.match(ver)) { 21 | Context.fatalError("Invalid `react_ver` specified: " + Context.definedValue("react_ver"), Context.currentPos()); 22 | } 23 | var version = [Std.parseInt(match.matched(1)), Std.parseInt(match.matched(2))]; 24 | 25 | if (semver(version, [16, 2])) { 26 | define("react_fragments"); 27 | } 28 | if (semver(version, [16, 3])) { 29 | define("react_context_api"); 30 | define("react_ref_api"); 31 | define("react_snapshot_api"); 32 | } 33 | if (semver(version, [16, 9])) { 34 | define("react_unsafe_lifecycle"); 35 | } 36 | } 37 | 38 | static function semver(target: Array, required: Array) { 39 | if (target[0] != required[0]) return target[0] > required[0]; 40 | return target[1] >= required[1]; 41 | } 42 | 43 | static function define(flag: String) { 44 | if (!Context.defined(flag)) { 45 | haxe.macro.Compiler.define(flag); 46 | } 47 | } 48 | 49 | public static function alterComponentSignatures(inClass:ClassType, fields:Array):Array 50 | { 51 | var propsType:ComplexType = macro :Dynamic; 52 | var stateType:ComplexType = macro :Dynamic; 53 | 54 | switch (inClass.superClass) 55 | { 56 | case {params: params, t: _.toString() => cls} 57 | if (cls == 'react.ReactComponentOf' || cls == 'react.PureComponentOf'): 58 | propsType = TypeTools.toComplexType(params[0]); 59 | stateType = TypeTools.toComplexType(params[1]); 60 | 61 | default: 62 | } 63 | 64 | // auto-declare arguments of `componentDidUpdate` for convenience 65 | var updateArgs = Context.defined("react_snapshot_api") ? 3 : 2; 66 | for (field in fields) { 67 | if (field.name == 'componentDidUpdate') { 68 | switch (field.kind) { 69 | case FFun(f): 70 | while (f.args.length < updateArgs) { 71 | var index = f.args.length; 72 | f.args.push({ 73 | name: '_', 74 | opt: index == 2, 75 | type: macro :Dynamic 76 | }); 77 | } 78 | default: 79 | } 80 | break; 81 | } 82 | } 83 | 84 | // Only alter setState signature for non-dynamic states 85 | if (!Context.defined('display')) 86 | switch (ComplexTypeTools.toType(stateType)) 87 | { 88 | case TType(_) if (!hasSetState(fields)): 89 | addSetStateType(fields, inClass, propsType, stateType); 90 | 91 | default: 92 | } 93 | 94 | return fields; 95 | } 96 | 97 | public static function ensureRenderOverride(inClass:ClassType, fields:Array):Array 98 | { 99 | if (!inClass.isExtern) 100 | if (!Lambda.exists(fields, function(f) return f.name == 'render')) 101 | Context.warning( 102 | 'Component ${inClass.name}: ' 103 | + 'No `render` method found: you may have forgotten to ' 104 | + 'override `render` from `ReactComponent`.', 105 | inClass.pos 106 | ); 107 | 108 | return fields; 109 | } 110 | 111 | static function hasSetState(fields:Array) { 112 | for (field in fields) 113 | { 114 | if (field.name == 'setState') 115 | { 116 | return switch (field.kind) { 117 | case FFun(f): true; 118 | default: false; 119 | } 120 | } 121 | } 122 | 123 | return false; 124 | } 125 | 126 | static function addSetStateType( 127 | fields:Array, 128 | inClass:ClassType, 129 | propsType:ComplexType, 130 | stateType:ComplexType 131 | ) { 132 | var partialStateType = switch (ComplexTypeTools.toType(stateType)) { 133 | case TType(_): 134 | TPath({ 135 | name: 'Partial', 136 | pack: ['react'], 137 | params: [TPType(stateType)] 138 | }); 139 | 140 | default: 141 | macro :Dynamic; 142 | }; 143 | 144 | var setStateArgs:Array = [ 145 | { 146 | name: 'nextState', 147 | // TState -> Partial 148 | type: TFunction([stateType], partialStateType), 149 | opt: false 150 | }, 151 | { 152 | name: 'callback', 153 | type: macro :Void->Void, 154 | opt: true 155 | } 156 | ]; 157 | 158 | fields.push({ 159 | name: 'setState', 160 | access: [APublic, AOverride], 161 | meta: [ 162 | { 163 | // Add @:extern meta so that this code only exist at compile time 164 | name: ':extern', 165 | params: null, 166 | pos: Context.currentPos() 167 | }, 168 | { 169 | // First overload: 170 | // function(nextState:TState -> TProps -> Partial, ?callback:Void -> Void):Void {} 171 | name: ':overload', 172 | params: [generateSetStateOverload( 173 | TFunction([stateType, propsType], partialStateType) 174 | )], 175 | pos: Context.currentPos() 176 | }, 177 | { 178 | // Second overload: 179 | // function(nextState:Partial, ?callback:Void -> Void):Void {} 180 | name: ':overload', 181 | params: [generateSetStateOverload(partialStateType)], 182 | pos: Context.currentPos() 183 | } 184 | ], 185 | kind: FFun({ 186 | args: setStateArgs, 187 | ret: macro :Void, 188 | #if haxe4 189 | expr: null 190 | #else 191 | expr: macro { super.setState(nextState, callback); } 192 | #end 193 | }), 194 | pos: inClass.pos 195 | }); 196 | } 197 | 198 | static function generateSetStateOverload(nextStateType:ComplexType) { 199 | return { 200 | expr: EFunction(null, { 201 | args: [ 202 | { 203 | name: 'nextState', 204 | type: nextStateType, 205 | opt: false 206 | }, 207 | { 208 | name: 'callback', 209 | type: macro :Void->Void, 210 | opt: true 211 | } 212 | ], 213 | expr: macro {}, 214 | params: null, 215 | ret: macro :Void 216 | }), 217 | pos: Context.currentPos() 218 | }; 219 | } 220 | #end 221 | } 222 | -------------------------------------------------------------------------------- /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 | class ReactUtil 6 | { 7 | public static function cx(arrayOrObject:Dynamic) 8 | { 9 | var array:Array>; 10 | if (Std.is(arrayOrObject, Array)) array = arrayOrObject; 11 | else array = [arrayOrObject]; 12 | var classes:Array = []; 13 | for (value in array) 14 | { 15 | if (value == null) continue; 16 | if (Std.is(value, String)) 17 | { 18 | classes.push(cast value); 19 | } 20 | else 21 | { 22 | for (field in Reflect.fields(value)) 23 | if (Reflect.field(value, field) == true) 24 | classes.push(field); 25 | } 26 | } 27 | return classes.join(' '); 28 | } 29 | 30 | public static function assign(target:Dynamic, sources:Array):Dynamic 31 | { 32 | for (source in sources) 33 | if (source != null) 34 | for (field in Reflect.fields(source)) 35 | Reflect.setField(target, field, Reflect.field(source, field)); 36 | return target; 37 | } 38 | 39 | public static function copy(source1:Dynamic, ?source2:Dynamic):Dynamic 40 | { 41 | var target = {}; 42 | for (field in Reflect.fields(source1)) 43 | Reflect.setField(target, field, Reflect.field(source1, field)); 44 | if (source2 != null) 45 | for (field in Reflect.fields(source2)) 46 | Reflect.setField(target, field, Reflect.field(source2, field)); 47 | return target; 48 | } 49 | 50 | public static function copyWithout(source1:Dynamic, source2:Dynamic, fields:Array) 51 | { 52 | var target = {}; 53 | for (field in Reflect.fields(source1)) 54 | if (!Lambda.has(fields, field)) 55 | Reflect.setField(target, field, Reflect.field(source1, field)); 56 | if (source2 != null) 57 | for (field in Reflect.fields(source2)) 58 | if (!Lambda.has(fields, field)) 59 | Reflect.setField(target, field, Reflect.field(source2, field)); 60 | return target; 61 | } 62 | 63 | public static function mapi(items:Array
    , map:Int -> A -> B):Array 64 | { 65 | if (items == null) return null; 66 | var newItems = []; 67 | for (i in 0...items.length) 68 | newItems.push(map(i, items[i])); 69 | return newItems; 70 | } 71 | 72 | /** 73 | Clone opaque children structure, providing additional props to merge: 74 | - as a object 75 | - or as a function (child->props) 76 | **/ 77 | public static function cloneChildren(children:Dynamic, props:Dynamic):Dynamic 78 | { 79 | if (Reflect.isFunction(props)) 80 | return React.Children.map(children, function(child) { 81 | return React.cloneElement(child, props(child)); 82 | }); 83 | else 84 | return React.Children.map(children, function(child) { 85 | return React.cloneElement(child, props); 86 | }); 87 | } 88 | 89 | /** 90 | https://facebook.github.io/react/docs/pure-render-mixin.html 91 | 92 | Implementing a simple shallow compare of next props and next state 93 | similar to the PureRenderMixin react addon 94 | **/ 95 | public static function shouldComponentUpdate(component:Dynamic, nextProps:Dynamic, nextState:Dynamic):Bool 96 | { 97 | return !shallowCompare(component.props, nextProps) || !shallowCompare(component.state, nextState); 98 | } 99 | 100 | public static function shallowCompare(a:Dynamic, b:Dynamic):Bool 101 | { 102 | var aFields = Reflect.fields(a); 103 | var bFields = Reflect.fields(b); 104 | if (aFields.length != bFields.length) 105 | return false; 106 | for (field in aFields) 107 | if (!Reflect.hasField(b, field) || Reflect.field(b, field) != Reflect.field(a, field)) 108 | return false; 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /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/jsx/JsxParser.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | using StringTools; 4 | 5 | #if (macro || munit || display) 6 | enum JsxAst 7 | { 8 | Node(isHtml:Bool, path:Array, attributes:Array<{name:String, value:String}>, children:Array); 9 | Expr(value:String); 10 | Text(value:String); 11 | } 12 | 13 | class JsxParser 14 | { 15 | static public function process(xml:Xml):JsxAst 16 | { 17 | var entries = parseChildren(xml); 18 | if (entries.length == 0) return null; 19 | if (entries.length > 1) throw('Syntax error: Adjacent JSX elements must be wrapped in an enclosing tag'); 20 | return entries[0]; 21 | } 22 | 23 | static function processElement(xml:Xml):JsxAst 24 | { 25 | // parse type 26 | var path = xml.nodeName.split('.'); 27 | var last = path[path.length - 1]; 28 | var isHtml = path.length == 1 && last.charAt(0) == last.charAt(0).toLowerCase(); 29 | 30 | // parse attributes 31 | var attrs = []; 32 | for (attr in xml.attributes()) 33 | { 34 | var value = xml.get(attr); 35 | if (value.length > 0 && value.charAt(0) != '{') value = replaceEntities(value); 36 | attrs.push({ name:attr, value:value }); 37 | } 38 | 39 | // parse children 40 | var children = parseChildren(xml); 41 | 42 | return JsxAst.Node(isHtml, path, attrs, children); 43 | } 44 | 45 | static function parseChildren(xml:Xml):Array 46 | { 47 | var children = []; 48 | for (node in xml) 49 | { 50 | if (node.nodeType == Xml.CData) 51 | { 52 | children.push(JsxAst.Text(node.nodeValue)); 53 | } 54 | else if (node.nodeType == Xml.PCData) 55 | { 56 | var value = node.nodeValue; 57 | if (value.length == 0) continue; 58 | 59 | var lines = ~/[\r\n]/g.split(value); 60 | for (line in lines) 61 | { 62 | if (line != lines[0]) line = line.ltrim(); 63 | if (line.length == 0) continue; 64 | ~/([^{]+|{[^}]+})/g.map(line, function (e){ 65 | var token = e.matched(0); 66 | children.push(parseText(token)); 67 | return ''; 68 | }); 69 | } 70 | } 71 | else if (node.nodeType == Xml.Element) 72 | { 73 | children.push(processElement(node)); 74 | } 75 | } 76 | return children; 77 | } 78 | 79 | static public function parseText(value:String):JsxAst 80 | { 81 | return value.charAt(0) == '{' && value.charAt(value.length - 1) == '}' 82 | ? JsxAst.Expr(value.substr(1, value.length - 2)) 83 | : JsxAst.Text(replaceEntities(value)); 84 | } 85 | 86 | static public function replaceEntities(value:String) 87 | { 88 | if (value.indexOf('&') < 0) 89 | return value; 90 | 91 | var reEntity = ~/&[a-z0-9]+;/gi; 92 | var map = html.Entities.all; 93 | var result = ''; 94 | while (reEntity.match(value)) 95 | { 96 | result += reEntity.matchedLeft(); 97 | var entity = reEntity.matched(0); 98 | if (map.exists(entity)) result += map.get(entity); 99 | else result += entity; // no match 100 | value = reEntity.matchedRight(); 101 | } 102 | return result + value; 103 | } 104 | } 105 | #end 106 | -------------------------------------------------------------------------------- /src/lib/react/jsx/JsxSanitize.hx: -------------------------------------------------------------------------------- 1 | package react.jsx; 2 | 3 | #if (macro || munit) 4 | class JsxSanitize 5 | { 6 | static public function process(jsx:String) 7 | { 8 | var reChar = ~/[a-zA-Z0-9_]/; 9 | var buf = new StringBuf(); 10 | var chars = StringTools.trim(jsx).split(''); 11 | var len = chars.length; 12 | var inTag = false; 13 | var inAttrib = false; 14 | var inExpr = false; 15 | var braceCount = 0; 16 | var spreadCount = 0; 17 | var cp = ''; 18 | var ci = ''; 19 | var cn = chars[0]; 20 | var i = 0; 21 | while (i < len) { 22 | if (ci != ' ') cp = ci; 23 | ci = cn; 24 | cn = chars[++i]; 25 | 26 | // inline blocks 27 | if (ci == '{') { 28 | // quote bidings 29 | if (braceCount == 0 && inTag) { 30 | if (cp == '=') { 31 | inAttrib = true; 32 | inExpr = true; 33 | buf.add('"'); 34 | } 35 | // spread attributes 36 | else if (cn == '.' && chars[i] == '.' && chars[i + 1] == '.') { 37 | inAttrib = true; 38 | inExpr = true; 39 | i += 3; 40 | cn = chars[i]; 41 | buf.add('.'); 42 | buf.add(spreadCount++); 43 | buf.add('="'); 44 | } 45 | } 46 | braceCount++; 47 | } 48 | if (braceCount > 0) { 49 | // escape double-quotes inside attributes 50 | if (inAttrib && ci == '"') buf.add('"'); 51 | else buf.add(ci); 52 | // close binding quote 53 | if (ci == '}') { 54 | braceCount--; 55 | if (braceCount == 0 && inAttrib && inExpr) { 56 | inAttrib = false; 57 | inExpr = false; 58 | buf.add('"'); 59 | ci = '"'; 60 | } 61 | } 62 | continue; 63 | } 64 | 65 | // xml attributes 66 | if (inAttrib) { 67 | if (ci == '"' && cn != '\\') inAttrib = false; 68 | buf.add(ci); 69 | continue; 70 | } 71 | 72 | // string interpolation 73 | if (ci == '$') { 74 | // drop $ of ${foo} or $$ 75 | if (cn == '{' || cn == '$') { 76 | ci = cp; 77 | continue; 78 | } 79 | // <$MyTag> 80 | if (inTag && cp == '<') { 81 | continue; 82 | } 83 | // 84 | if (inTag && cp == '/' && chars[i - 3] == '<') { 85 | continue; 86 | } 87 | // $foo -> {foo} 88 | if (reChar.match(cn)) { 89 | if (inTag && cp == '=') { 90 | inAttrib = true; 91 | buf.add('"'); 92 | } 93 | ci = '{'; 94 | do { 95 | buf.add(ci); 96 | cp = ci; 97 | ci = cn; 98 | cn = chars[++i]; 99 | } 100 | while (i < len && reChar.match(ci)); 101 | buf.add('}'); 102 | if (inAttrib) { 103 | inAttrib = false; 104 | buf.add('"'); 105 | } 106 | // retry last char 107 | i--; 108 | cn = ci; 109 | ci = '}'; 110 | cp = ''; 111 | continue; 112 | } 113 | } 114 | 115 | // xml tags 116 | if (inTag) { 117 | if (ci == '>') inTag = false; 118 | } 119 | else if (ci == '<') { 120 | if (cn == '>') ci = '') cn = '/react.Fragment'; 122 | inTag = true; 123 | } 124 | buf.add(ci); 125 | } 126 | return buf.toString(); 127 | } 128 | 129 | 130 | } 131 | #end -------------------------------------------------------------------------------- /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 | 7 | using haxe.macro.Tools; 8 | 9 | private typedef JsxStaticDecl = { 10 | var module:String; 11 | var className:String; 12 | var displayName:String; 13 | var fieldName:String; 14 | } 15 | 16 | private enum MetaValueType { 17 | NoMeta; 18 | NoParams(meta:MetadataEntry); 19 | WithParams(meta:MetadataEntry, params:Array); 20 | } 21 | 22 | class JsxStaticMacro 23 | { 24 | static public var META_NAME = ':jsxStatic'; 25 | static public var FIELD_NAME = '__jsxStatic'; 26 | 27 | static var decls:Array = []; 28 | 29 | static public function build():Array 30 | { 31 | var cls = Context.getLocalClass(); 32 | if (cls == null) return null; 33 | var inClass = cls.get(); 34 | 35 | if (inClass.meta.has(META_NAME)) 36 | { 37 | var fields = Context.getBuildFields(); 38 | for (f in fields) if (f.name == FIELD_NAME) return fields; 39 | 40 | var proxyName = extractMetaString(inClass.meta, META_NAME); 41 | fields.push({ 42 | access: [APublic, AStatic], 43 | name: FIELD_NAME, 44 | kind: FVar(macro :react.React.CreateElementType, macro $i{proxyName}), 45 | doc: null, 46 | meta: null, 47 | pos: inClass.pos 48 | }); 49 | 50 | return fields; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | static public function addHook() 57 | { 58 | // Add hook to generate __init__ at the end of the compilation 59 | Context.onAfterTyping(afterTypingHook); 60 | } 61 | 62 | static public function injectDisplayNames(type:Expr) 63 | { 64 | #if !debug 65 | return; 66 | #end 67 | 68 | switch (Context.typeExpr(type).expr) { 69 | case TConst(TString(_)): 70 | // HTML component, nothing to do 71 | 72 | case TTypeExpr(TClassDecl(_)): 73 | // ReactComponent, should already handle its displayName 74 | 75 | case TField(_, FStatic(clsTypeRef, _.get() => {kind: FMethod(_), name: fieldName})): 76 | var clsType = clsTypeRef.get(); 77 | var displayName = handleJsxStaticMeta(clsType, fieldName); 78 | 79 | addDisplayNameDecl({ 80 | module: clsType.module, 81 | className: clsType.name, 82 | displayName: displayName, 83 | fieldName: fieldName 84 | }); 85 | 86 | case TCall({expr: TField(_, FStatic(clsTypeRef, clsField))}, _): 87 | var clsType = clsTypeRef.get(); 88 | var fieldName = clsField.get().name; 89 | var displayName = StringTools.startsWith(fieldName, 'get_') 90 | ? handleJsxStaticMeta(clsType, fieldName.substr(4)) 91 | : fieldName; 92 | 93 | addDisplayNameDecl({ 94 | module: clsType.module, 95 | className: clsType.name, 96 | displayName: displayName, 97 | fieldName: fieldName 98 | }); 99 | 100 | case TLocal({name: varName}): 101 | // Local vars not handled at the moment 102 | 103 | case TField(_, FInstance(_, _, _)): 104 | // Instance fields not handled at the moment 105 | 106 | case TField(_, FStatic(_, _)): 107 | // Static variables not handled at the moment 108 | 109 | default: 110 | // Unknown type, not handled 111 | // trace(typedExpr); 112 | } 113 | } 114 | 115 | static public function handleJsxStaticProxy(type:Expr) 116 | { 117 | var typedExpr = Context.typeExpr(type); 118 | 119 | switch (typedExpr.expr) 120 | { 121 | case TTypeExpr(TClassDecl(_.get() => c)): 122 | if (c.meta.has(META_NAME)) 123 | type.expr = EField( 124 | {expr: EConst(CIdent(c.name)), pos: type.pos}, 125 | extractMetaString(c.meta, META_NAME) 126 | ); 127 | 128 | default: 129 | } 130 | } 131 | 132 | static function extractMeta(meta:MetaAccess, name:String):MetaValueType 133 | { 134 | if (!meta.has(name)) return NoMeta; 135 | 136 | var metas = meta.extract(name); 137 | if (metas.length == 0) return NoMeta; 138 | 139 | var meta = metas.pop(); 140 | var params = meta.params; 141 | if (params.length == 0) return NoParams(meta); 142 | 143 | return WithParams(meta, params); 144 | } 145 | 146 | static public function extractMetaString(meta:MetaAccess, name:String):String 147 | { 148 | return switch(extractMeta(meta, name)) { 149 | case NoMeta: null; 150 | case WithParams(_, params): extractMetaName(params.pop()); 151 | case NoParams(meta): 152 | Context.fatalError( 153 | "Parameter required for @:jsxStatic('name-of-static-function')", 154 | meta.pos 155 | ); 156 | }; 157 | } 158 | 159 | static public function extractMetaName(metaExpr:Expr):String 160 | { 161 | return switch (metaExpr.expr) { 162 | case EConst(CString(str)): str; 163 | case EConst(CIdent(ident)): ident; 164 | 165 | default: 166 | Context.fatalError( 167 | "@:jsxStatic: invalid parameter. Expected static function name.", 168 | metaExpr.pos 169 | ); 170 | }; 171 | } 172 | 173 | static function handleJsxStaticMeta(clsType:ClassType, displayName:String) 174 | { 175 | var jsxStatic = extractMetaString(clsType.meta, META_NAME); 176 | if (jsxStatic != null && jsxStatic == displayName) return clsType.name; 177 | return displayName; 178 | } 179 | 180 | static function addDisplayNameDecl(decl:JsxStaticDecl) 181 | { 182 | var previousDecl = Lambda.find(decls, function(d) { 183 | return d.module == decl.module 184 | && d.className == decl.className 185 | && d.fieldName == decl.fieldName; 186 | }); 187 | 188 | if (previousDecl == null) decls.push(decl); 189 | } 190 | 191 | static function afterTypingHook(modules:Array) 192 | { 193 | var initModule = "JsxStaticInit__"; 194 | 195 | try { 196 | // Could also loop through modules, but it's easier like this 197 | Context.getModule(initModule); 198 | } catch(e:Dynamic) { 199 | var exprs = decls.map(function(decl) { 200 | var fName = decl.fieldName; 201 | return macro { 202 | untyped $i{decl.className}.$fName.displayName = 203 | $i{decl.className}.$fName.displayName || $v{decl.displayName}; 204 | }; 205 | }); 206 | 207 | var cls = macro class $initModule { 208 | static function __init__() { 209 | $a{exprs}; 210 | } 211 | }; 212 | 213 | var imports = decls.map(function(decl) return generatePath(decl.module)); 214 | Context.defineModule(initModule, [cls], imports); 215 | } 216 | } 217 | 218 | static function generatePath(module:String) 219 | { 220 | var parts = module.split('.'); 221 | 222 | return { 223 | mode: ImportMode.INormal, 224 | path: parts.map(function(part) return {pos: (macro null).pos, name: part}) 225 | }; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/lib/react/wrap/ReactWrapperMacro.hx: -------------------------------------------------------------------------------- 1 | package react.wrap; 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 | class ReactWrapperMacro 12 | { 13 | static public inline var WRAP_BUILDER = 'Wrap'; 14 | static public inline var WRAP_META = ':wrap'; 15 | static inline var WRAPPED_META = ':wrapped_by_macro'; 16 | 17 | static public function buildComponent(inClass:ClassType, fields:Array):Array 18 | { 19 | if (inClass.meta.has(WRAPPED_META)) return fields; 20 | 21 | if (inClass.meta.has(WRAP_META)) 22 | { 23 | if (inClass.meta.has(JsxStaticMacro.META_NAME)) 24 | Context.fatalError( 25 | 'Cannot use @${WRAP_META} and @${JsxStaticMacro.META_NAME} on the same component', 26 | inClass.pos 27 | ); 28 | 29 | var prevPos = null; 30 | var wrapperExpr = null; 31 | var wrappersMeta = inClass.meta.extract(WRAP_META); 32 | wrappersMeta.reverse(); 33 | for (m in wrappersMeta) 34 | { 35 | if (m.params.length == 0) 36 | Context.fatalError('Invalid number of parameters for @${WRAP_META}; expected 1 parameter.', m.pos); 37 | 38 | var e = m.params[0]; 39 | wrapperExpr = wrapperExpr == null ? macro ${e}($i{inClass.name}) : macro ${e}(${wrapperExpr}); 40 | prevPos = m.pos; 41 | } 42 | 43 | var fieldName = '_renderWrapper'; 44 | fields.push({ 45 | access: [APublic, AStatic], 46 | name: fieldName, 47 | kind: FVar(macro :react.React.CreateElementType, wrapperExpr), 48 | doc: null, 49 | meta: null, 50 | pos: inClass.pos 51 | }); 52 | 53 | inClass.meta.add(JsxStaticMacro.META_NAME, [macro $v{fieldName}], inClass.pos); 54 | inClass.meta.add(WRAPPED_META, [], inClass.pos); 55 | } 56 | 57 | return fields; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /tagRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | VERSION=$1 5 | 6 | # Validate version 7 | if [ -z "$VERSION" ]; then 8 | echo "No version specified" 9 | exit 1 10 | fi 11 | if [ $(git tag -l "$VERSION") ]; then 12 | echo "Tag $VERSION already exists" 13 | exit 1 14 | fi 15 | 16 | # Change version in haxelib.json and package.json 17 | echo "Update version to $VERSION" 18 | PATTERN="s/\"version\": \"[0-9]+.[0-9]+.[0-9]+\"/\"version\": \"$VERSION\"/g" 19 | 20 | case "$(uname -s)" in 21 | Darwin) 22 | sed -E -i "" "$PATTERN" ./haxelib.json 23 | sed -E -i "" "$PATTERN" ./mdk/info.json 24 | ;; 25 | *) 26 | sed -E -i "$PATTERN" ./haxelib.json 27 | sed -E -i "$PATTERN" ./mdk-info.json 28 | ;; 29 | esac 30 | 31 | # Tag, commit and push to trigger a new CI release 32 | git commit -am "Release version $VERSION" 33 | git push origin master 34 | git tag $VERSION 35 | git push origin $VERSION 36 | -------------------------------------------------------------------------------- /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/JsxParserTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Json; 4 | import massive.munit.Assert; 5 | import react.jsx.JsxParser; 6 | 7 | class JsxParserTest 8 | { 9 | public function new() { } 10 | 11 | @Test 12 | public function jsx_text_literal_should_return_text() 13 | { 14 | var jsx = 'text'; 15 | var xml = Xml.parse(jsx); 16 | var ast = JsxParser.process(xml); 17 | assertDeepEqual(JsxAst.Text('text'), ast); 18 | } 19 | 20 | @Test 21 | public function jsx_cdata_should_return_text() 22 | { 23 | var jsx = ''; 24 | var xml = Xml.parse(jsx); 25 | var ast = JsxParser.process(xml); 26 | assertDeepEqual(JsxAst.Text('test {test}'), ast); 27 | } 28 | 29 | @Test 30 | public function jsx_empty_html_tag_should_return_html_tag_with_no_children() 31 | { 32 | var jsx = '
    '; 33 | var xml = Xml.parse(jsx); 34 | var ast = JsxParser.process(xml); 35 | assertDeepEqual(JsxAst.Node(true, ['div'], [], []), ast); 36 | } 37 | 38 | @Test 39 | public function jsx_html_tag_with_whitespace_should_return_html_tag_with_text_child() 40 | { 41 | var jsx = '
    '; 42 | var xml = Xml.parse(jsx); 43 | var ast = JsxParser.process(xml); 44 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text(' ')]), ast); 45 | } 46 | 47 | @Test 48 | public function jsx_html_tag_with_text_should_return_html_tag_with_text_child() 49 | { 50 | var jsx = '
    test
    '; 51 | var xml = Xml.parse(jsx); 52 | var ast = JsxParser.process(xml); 53 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text('test')]), ast); 54 | } 55 | 56 | @Test 57 | public function jsx_empty_html_tag_with_attribute_should_return_html_tag_with_attributes() 58 | { 59 | var jsx = '
    '; 60 | var xml = Xml.parse(jsx); 61 | var ast = JsxParser.process(xml); 62 | assertDeepEqual(JsxAst.Node(true, ['div'], [{name:'className', value:'foo'}], []), ast); 63 | } 64 | 65 | @Test 66 | public function jsx_empty_html_tag_with_attribute_binding_should_return_html_tag_with_binding() 67 | { 68 | var jsx = '
    '; 69 | var xml = Xml.parse(jsx); 70 | var ast = JsxParser.process(xml); 71 | assertDeepEqual(JsxAst.Node(true, ['div'], [{name:'className', value:'{foo}'}], []), ast); 72 | } 73 | 74 | @Test 75 | public function jsx_empty_html_tag_with_spread_binding_should_return_html_tag_with_spread() 76 | { 77 | var jsx = '
    '; 78 | var xml = Xml.parse(jsx); 79 | var ast = JsxParser.process(xml); 80 | assertDeepEqual(JsxAst.Node(true, ['div'], [{name:'.0', value:'{foo}'}], []), ast); 81 | } 82 | 83 | @Test 84 | public function jsx_html_tag_with_binding_should_return_one_child() 85 | { 86 | var jsx = '
    {test}
    '; 87 | var xml = Xml.parse(jsx); 88 | var ast = JsxParser.process(xml); 89 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test')]), ast); 90 | } 91 | 92 | @Test 93 | public function jsx_html_tag_with_binding_and_whitespace_in_line_should_keep_whitespace() 94 | { 95 | var jsx = '
    {test}
    '; 96 | var xml = Xml.parse(jsx); 97 | var ast = JsxParser.process(xml); 98 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text(' '), JsxAst.Expr('test'), JsxAst.Text(' ')]), ast); 99 | } 100 | 101 | @Test 102 | public function jsx_html_tag_with_binding_and_whitespace_multilinne_should_NOT_keep_whitespace_1() 103 | { 104 | var jsx = '
    105 | {test}
    '; 106 | var xml = Xml.parse(jsx); 107 | var ast = JsxParser.process(xml); 108 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test'), JsxAst.Text(' ')]), ast); 109 | } 110 | 111 | @Test 112 | public function jsx_html_tag_with_binding_and_whitespace_multilinne_should_NOT_keep_whitespace_2() 113 | { 114 | var jsx = '
    {test} 115 |
    '; 116 | var xml = Xml.parse(jsx); 117 | var ast = JsxParser.process(xml); 118 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text(' '), JsxAst.Expr('test')]), ast); 119 | } 120 | 121 | @Test 122 | public function jsx_html_tag_with_binding_and_whitespace_multilinne_should_NOT_keep_whitespace_3() 123 | { 124 | var jsx = '
    125 | {test} 126 |
    '; 127 | var xml = Xml.parse(jsx); 128 | var ast = JsxParser.process(xml); 129 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test')]), ast); 130 | } 131 | 132 | @Test 133 | public function jsx_html_tag_with_bindings_and_whitespace_in_line_should_keep_whitespace() 134 | { 135 | var jsx = '
    {test} {foo}
    '; 136 | var xml = Xml.parse(jsx); 137 | var ast = JsxParser.process(xml); 138 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test'), JsxAst.Text(' '), JsxAst.Expr('foo')]), ast); 139 | } 140 | 141 | @Test 142 | public function jsx_html_tag_with_bindings_and_whitespace_multiline_should_NOT_keep_whitespace() 143 | { 144 | var jsx = '
    145 | {test} 146 | {foo} 147 |
    '; 148 | var xml = Xml.parse(jsx); 149 | var ast = JsxParser.process(xml); 150 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test'), JsxAst.Expr('foo')]), ast); 151 | } 152 | 153 | @Test 154 | public function jsx_html_tag_with_bindings_and_text_multiline_should_NOT_keep_whitespace_excepted_in_line() 155 | { 156 | var jsx = '
    157 | {test} and {foo} 158 |
    '; 159 | var xml = Xml.parse(jsx); 160 | var ast = JsxParser.process(xml); 161 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('test'), JsxAst.Text(' and '), JsxAst.Expr('foo')]), ast); 162 | } 163 | 164 | @Test 165 | public function jsx_tag_starting_with_uppercase_should_return_class_node() 166 | { 167 | var jsx = ''; 168 | var xml = Xml.parse(jsx); 169 | var ast = JsxParser.process(xml); 170 | assertDeepEqual(JsxAst.Node(false, ['Test'], [], []), ast); 171 | } 172 | 173 | @Test 174 | public function jsx_tag_with_qualified_name_should_return_class_node() 175 | { 176 | var jsx = ''; 177 | var xml = Xml.parse(jsx); 178 | var ast = JsxParser.process(xml); 179 | assertDeepEqual(JsxAst.Node(false, ['com', 'Test'], [], []), ast); 180 | } 181 | 182 | @Test 183 | public function jsx_tag_starting_with_uppercase_should_return_class_node_with_children() 184 | { 185 | var jsx = 'foo'; 186 | var xml = Xml.parse(jsx); 187 | var ast = JsxParser.process(xml); 188 | assertDeepEqual(JsxAst.Node(false, ['Test'], [], [JsxAst.Text('foo')]), ast); 189 | } 190 | 191 | @Test 192 | public function jsx_tag_with_qualified_name_should_return_class_node_with_children() 193 | { 194 | var jsx = 'foo'; 195 | var xml = Xml.parse(jsx); 196 | var ast = JsxParser.process(xml); 197 | assertDeepEqual(JsxAst.Node(false, ['com', 'Test'], [], [JsxAst.Text('foo')]), ast); 198 | } 199 | 200 | @Test 201 | public function jsx_tag_starting_with_uppercase_should_return_class_node_with_attribute() 202 | { 203 | var jsx = ''; 204 | var xml = Xml.parse(jsx); 205 | var ast = JsxParser.process(xml); 206 | assertDeepEqual(JsxAst.Node(false, ['Test'], [{name:'id', value:'foo'}], []), ast); 207 | } 208 | 209 | @Test 210 | public function jsx_tag_with_qualified_name_should_return_class_node_with_attribute() 211 | { 212 | var jsx = ''; 213 | var xml = Xml.parse(jsx); 214 | var ast = JsxParser.process(xml); 215 | assertDeepEqual(JsxAst.Node(false, ['com', 'Test'], [{name:'id', value:'foo'}], []), ast); 216 | } 217 | 218 | @Test 219 | public function jsx_nested_tags_should_return_nested_nodes_1() 220 | { 221 | var jsx = '
    '; 222 | var xml = Xml.parse(jsx); 223 | var ast = JsxParser.process(xml); 224 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Node(false, ['Test'], [], [])]), ast); 225 | } 226 | 227 | @Test 228 | public function jsx_nested_tags_should_return_nested_nodes_2() 229 | { 230 | var jsx = '
    231 | 232 |
    '; 233 | var xml = Xml.parse(jsx); 234 | var ast = JsxParser.process(xml); 235 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Node(false, ['Test'], [], [])]), ast); 236 | } 237 | 238 | @Test 239 | public function jsx_nested_tags_combined_with_text_should_return_nested_nodes_1() 240 | { 241 | var jsx = '
    test
    '; 242 | var xml = Xml.parse(jsx); 243 | var ast = JsxParser.process(xml); 244 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text('test '), JsxAst.Node(false, ['Test'], [], [])]), ast); 245 | } 246 | 247 | @Test 248 | public function jsx_nested_tags_combined_with_text_should_return_nested_nodes_2() 249 | { 250 | var jsx = '
    test
    '; 251 | var xml = Xml.parse(jsx); 252 | var ast = JsxParser.process(xml); 253 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Node(false, ['Test'], [], []), JsxAst.Text(' test')]), ast); 254 | } 255 | 256 | @Test 257 | public function jsx_with_multiple_roots_should_fail_1() 258 | { 259 | var jsx = 'text {test}'; 260 | var xml = Xml.parse(jsx); 261 | try { 262 | var ast = JsxParser.process(xml); 263 | } 264 | catch (err:Dynamic) { 265 | return; 266 | } 267 | Assert.fail('Parser should raise an exception'); 268 | } 269 | 270 | @Test 271 | public function jsx_with_multiple_roots_should_fail_2() 272 | { 273 | var jsx = '
    test'; 274 | var xml = Xml.parse(jsx); 275 | try { 276 | var ast = JsxParser.process(xml); 277 | } 278 | catch (err:Dynamic) { 279 | return; 280 | } 281 | Assert.fail('Parser should raise an exception'); 282 | } 283 | 284 | @Test 285 | public function jsx_with_multiple_roots_should_fail_3() 286 | { 287 | var jsx = '
    test'; 288 | var xml = Xml.parse(jsx); 289 | try { 290 | var ast = JsxParser.process(xml); 291 | } 292 | catch (err:Dynamic) { 293 | return; 294 | } 295 | Assert.fail('Parser should raise an exception'); 296 | } 297 | 298 | @Test 299 | public function test_replace_entities() 300 | { 301 | var list = [ 302 | '&' => '&', 303 | '&foo' => '&foo', 304 | 'foo&' => 'foo&', 305 | '&&' => '&&', 306 | '&foo&' => '&foo&', 307 | '&foo;' => '&foo;' 308 | ]; 309 | for (key in list.keys()) 310 | { 311 | Assert.areEqual(list.get(key), JsxParser.replaceEntities(key)); 312 | } 313 | } 314 | 315 | @Test 316 | public function jsx_entities_in_text_should_be_replaced() 317 | { 318 | var jsx = '
    a × b
    '; 319 | var xml = Xml.parse(jsx); 320 | var ast = JsxParser.process(xml); 321 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Text('a × b')]), ast); 322 | } 323 | 324 | @Test 325 | public function jsx_entities_in_attributes_should_be_replaced() 326 | { 327 | var jsx = '
    '; 328 | var xml = Xml.parse(jsx); 329 | var ast = JsxParser.process(xml); 330 | assertDeepEqual(JsxAst.Node(true, ['div'], [{name:'ref', value:'a × b'}], []), ast); 331 | } 332 | 333 | @Test 334 | public function jsx_entities_in_code_blocks_should_NOT_be_replaced() 335 | { 336 | var jsx = '
    {"a × b"}
    '; 337 | var xml = Xml.parse(jsx); 338 | var ast = JsxParser.process(xml); 339 | assertDeepEqual(JsxAst.Node(true, ['div'], [], [JsxAst.Expr('"a × b"')]), ast); 340 | } 341 | 342 | @Test 343 | public function jsx_entities_in_binded_attributes_should_NOT_be_replaced() 344 | { 345 | var jsx = '
    '; 346 | var xml = Xml.parse(jsx); 347 | var ast = JsxParser.process(xml); 348 | assertDeepEqual(JsxAst.Node(true, ['div'], [{name:'ref', value:'{\'a × b\'}'}], []), ast); 349 | } 350 | 351 | 352 | /* TOOLS */ 353 | 354 | function assertDeepEqual(expected:JsxAst, actual:JsxAst) 355 | { 356 | switch (expected) 357 | { 358 | case JsxAst.Text(value): 359 | switch (actual) { 360 | case JsxAst.Text(actualValue): Assert.areEqual(value, actualValue); 361 | default: Assert.fail('Expected $expected but found $actual'); 362 | } 363 | 364 | case JsxAst.Expr(value): 365 | switch (actual) { 366 | case JsxAst.Expr(actualValue): Assert.areEqual(value, actualValue); 367 | default: Assert.fail('Expected $expected but found $actual'); 368 | } 369 | 370 | case JsxAst.Node(isHtml, path, attributes, children): 371 | switch (actual) { 372 | case JsxAst.Node(actualIsHtml, actualPath, actualAttributes, actualChildren): 373 | Assert.areEqual(isHtml, actualIsHtml); 374 | Assert.areEqual(path.toString(), actualPath.toString()); 375 | Assert.areEqual(Json.stringify(attributes), Json.stringify(actualAttributes)); 376 | Assert.areEqual(children.length, actualChildren.length); 377 | for (i in 0...children.length) 378 | assertDeepEqual(children[i], actualChildren[i]); 379 | default: Assert.fail('Expected $expected but found $actual'); 380 | } 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /test/src/JsxSanitizeTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import react.ReactMacro; 4 | import massive.munit.Assert; 5 | 6 | class JsxSanitizeTest 7 | { 8 | public function new() { } 9 | 10 | @Test 11 | public function sanitize_trims_input() 12 | { 13 | var jsx = ReactMacro.sanitize('
    '); 14 | Assert.areEqual("
    ", jsx); 15 | } 16 | 17 | @Test 18 | public function sanitize_removes_tag_interpolation_selfclosing() 19 | { 20 | var jsx = ReactMacro.sanitize('<$Tag/>'); 21 | Assert.areEqual("", jsx); 22 | jsx = ReactMacro.sanitize('foo<$Tag/>'); 23 | Assert.areEqual("foo", jsx); 24 | jsx = ReactMacro.sanitize('<$Tag foo/>'); 25 | Assert.areEqual("", jsx); 26 | } 27 | 28 | @Test 29 | public function sanitize_removes_tag_interpolation_closed() 30 | { 31 | var jsx = ReactMacro.sanitize('<$Tag>'); 32 | Assert.areEqual("", jsx); 33 | jsx = ReactMacro.sanitize('<$Tag>foo'); 34 | Assert.areEqual("foo", jsx); 35 | jsx = ReactMacro.sanitize('<$Tag foo="foo">foo'); 36 | Assert.areEqual("foo", jsx); 37 | } 38 | 39 | @Test 40 | public function sanitize_removes_tag_interpolation_selfclosing_with_sub() 41 | { 42 | var jsx = ReactMacro.sanitize('<$Tag><$Sub/>'); 43 | Assert.areEqual("", jsx); 44 | jsx = ReactMacro.sanitize('<$Tag foo="foo"><$Sub/>'); 45 | Assert.areEqual("", jsx); 46 | } 47 | 48 | @Test 49 | public function sanitize_remove_block_interpolation() 50 | { 51 | var jsx = ReactMacro.sanitize('${foo ? "a" : "b"}'); 52 | Assert.areEqual("{foo ? \"a\" : \"b\"}", jsx); 53 | } 54 | 55 | @Test 56 | public function sanitize_remove_block_interpolation_nested() 57 | { 58 | var jsx = ReactMacro.sanitize('${{"a" : "b"}}'); 59 | Assert.areEqual("{{\"a\" : \"b\"}}", jsx); 60 | } 61 | 62 | @Test 63 | public function sanitize_remove_block_interpolation_and_quote_attribute() 64 | { 65 | var jsx = ReactMacro.sanitize(''); 66 | Assert.areEqual("", jsx); 67 | } 68 | 69 | @Test 70 | public function sanitize_sanitizes_quotes_in_attribute_block() 71 | { 72 | var jsx = ReactMacro.sanitize(''); 73 | Assert.areEqual("", jsx); 74 | } 75 | 76 | @Test 77 | public function sanitize_makes_block_from_var_interpolation() 78 | { 79 | var jsx = ReactMacro.sanitize('$foo'); 80 | Assert.areEqual("{foo}", jsx); 81 | } 82 | 83 | @Test 84 | public function sanitize_makes_block_from_multiple_var_interpolation() 85 | { 86 | var jsx = ReactMacro.sanitize('$foo$bar$baz'); 87 | Assert.areEqual("{foo}{bar}{baz}", jsx); 88 | } 89 | 90 | @Test 91 | public function sanitize_makes_quoted_block_from_var_interpolation_attribute() 92 | { 93 | var jsx = ReactMacro.sanitize(''); 94 | Assert.areEqual("", jsx); 95 | } 96 | 97 | @Test 98 | public function sanitize_extract_spread_attributes() 99 | { 100 | var jsx = ReactMacro.sanitize(''); 101 | Assert.areEqual("", jsx); 102 | } 103 | 104 | @Test 105 | public function sanitize_extract_spread_attributes_multiple() 106 | { 107 | var jsx = ReactMacro.sanitize(''); 108 | Assert.areEqual("", jsx); 109 | } 110 | 111 | @Test 112 | public function sanitize_extract_spread_attributes_complex() 113 | { 114 | var jsx = ReactMacro.sanitize(''); 115 | Assert.areEqual("", jsx); 116 | } 117 | 118 | @Test 119 | public function sanitize_replaces_empty_tags_with_fragments() 120 | { 121 | var jsx = ReactMacro.sanitize('<>Text'); 122 | Assert.areEqual("Text", jsx); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/src/ReactMacroInlineMarkupTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import massive.munit.Assert; 4 | import react.ReactComponent; 5 | import react.ReactMacro.jsx; 6 | import support.sub.CompExternModule; 7 | import support.sub.CompModule; 8 | import AssertTools.assertHasProps; 9 | 10 | #if haxe4 11 | class CompBasic1 extends ReactComponent { 12 | override function render() { 13 | return jsx(
    ); 14 | } 15 | } 16 | 17 | class CompDefaults1 extends ReactComponent { 18 | static public var defaultProps = { 19 | defA:'A', 20 | defB:42 21 | } 22 | override function render() { 23 | return jsx(
    ); 24 | } 25 | } 26 | 27 | extern class CompExtern1 extends ReactComponent { 28 | } 29 | #end 30 | 31 | class ReactMacroInlineMarkupTest 32 | { 33 | public function new() {} 34 | 35 | #if haxe4 36 | 37 | @BeforeClass 38 | public function setup() 39 | { 40 | untyped __js__("$global.CompExtern1 = function() {}"); 41 | untyped __js__("$global.CompExternModule = function() {}"); 42 | } 43 | 44 | @Test 45 | public function DOM_without_props() 46 | { 47 | var e = jsx(
    ); 48 | Assert.areEqual('div', e.type); 49 | assertHasProps(e.props, []); 50 | } 51 | 52 | @Test 53 | public function DOM_with_const_props() 54 | { 55 | var e = jsx(
    ); 56 | Assert.areEqual('div', e.type); 57 | assertHasProps(e.props, ['a'], ['foo']); 58 | } 59 | 60 | @Test 61 | public function DOM_with_const_and_binding_props() 62 | { 63 | var foo = 12; 64 | var e = jsx(
    ); 65 | Assert.areEqual('div', e.type); 66 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 67 | } 68 | 69 | @Test 70 | public function function_with_props() 71 | { 72 | var e = jsx(); 73 | Assert.areEqual(RenderFunction, e.type); 74 | assertHasProps(e.props, ['a'], ['foo']); 75 | } 76 | 77 | @Test 78 | public function component_with_props() 79 | { 80 | var e = jsx(); 81 | Assert.areEqual(CompBasic1, e.type); 82 | assertHasProps(e.props, ['a'], ['foo']); 83 | } 84 | 85 | @Test 86 | public function extern_component_qualified_module_should_DEOPT() 87 | { 88 | var e = jsx(); 89 | Assert.areEqual('NATIVE', e.type); 90 | } 91 | 92 | @Test 93 | public function extern_component_module_should_DEOPT() 94 | { 95 | var e = jsx(); 96 | Assert.areEqual('NATIVE', e.type); 97 | } 98 | 99 | @Test 100 | public function extern_component_should_DEOPT() 101 | { 102 | var e = jsx(); 103 | Assert.areEqual('NATIVE', e.type); 104 | } 105 | 106 | @Test 107 | public function DOM_with_spread() 108 | { 109 | var o = { 110 | a:'foo', 111 | b:12 112 | } 113 | var e = jsx(
    ); 114 | Assert.areEqual('div', e.type); 115 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 116 | } 117 | 118 | @Test 119 | public function DOM_with_spread_and_prop() 120 | { 121 | var o = { 122 | a:'foo', 123 | b:12 124 | } 125 | var e = jsx(
    ); 126 | Assert.areEqual('div', e.type); 127 | assertHasProps(e.props, ['a', 'b', 'c'], (['foo', 12, 'bar']:Array)); 128 | } 129 | 130 | @Test 131 | public function DOM_with_spread_and_prop_override() 132 | { 133 | var o = { 134 | a:'foo', 135 | b:12 136 | } 137 | var e = jsx(
    ); 138 | Assert.areEqual('div', e.type); 139 | assertHasProps(e.props, ['a', 'b'], (['bar', 12]:Array)); 140 | } 141 | 142 | @Test 143 | public function component_with_defaultProps() 144 | { 145 | var e = jsx(); 146 | Assert.areEqual(CompDefaults1, e.type); 147 | assertHasProps(e.props, ['defA', 'defB'], (['A', 42]:Array)); 148 | } 149 | 150 | @Test 151 | public function component_in_sub_package_with_defaultProps() 152 | { 153 | var e = jsx(); 154 | Assert.areEqual(CompModule, e.type); 155 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 156 | } 157 | 158 | @Test 159 | public function qualified_component_in_sub_package_with_defaultProps() 160 | { 161 | var e = jsx(); 162 | Assert.areEqual(CompModule, e.type); 163 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 164 | } 165 | 166 | @Test 167 | public function component_with_defaultProps_and_spread() 168 | { 169 | var o = { 170 | a:'foo', 171 | b:12 172 | } 173 | var e = jsx(); 174 | Assert.areEqual(CompDefaults1, e.type); 175 | assertHasProps(e.props, ['defA', 'defB', 'a', 'b'], (['A', 42, 'foo', 12]:Array)); 176 | } 177 | 178 | @Test 179 | public function component_with_defaultProps_and_prop_override() 180 | { 181 | var e = jsx(); 182 | Assert.areEqual(CompDefaults1, e.type); 183 | assertHasProps(e.props, ['defA', 'defB'], (['foo', 42]:Array)); 184 | } 185 | 186 | @Test 187 | public function component_with_defaultProps_and_spread_override() 188 | { 189 | var o = { 190 | defA:'foo', 191 | b:12 192 | } 193 | var e = jsx(); 194 | Assert.areEqual(CompDefaults1, e.type); 195 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['foo', 42, 12]:Array)); 196 | } 197 | 198 | @Test 199 | public function component_with_defaultProps_and_spread_and_prop_override() 200 | { 201 | var o = { 202 | defA:'foo', 203 | b:12 204 | } 205 | var e = jsx(); 206 | Assert.areEqual(CompDefaults1, e.type); 207 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['bar', 42, 12]:Array)); 208 | } 209 | 210 | @Test 211 | public function DOM_with_ref_function_should_be_inlined() 212 | { 213 | function setRef() {}; 214 | var e = jsx(
    ); 215 | Assert.areEqual('div', e.type); 216 | Assert.areEqual(setRef, e.ref); 217 | assertHasProps(e.props, []); 218 | } 219 | 220 | @Test 221 | public function DOM_with_ref_const_string_should_be_DEOPT() 222 | { 223 | var e = jsx(
    ); 224 | Assert.areEqual('NATIVE', e.type); 225 | } 226 | 227 | @Test 228 | public function DOM_with_ref_string_should_be_DEOPT() 229 | { 230 | var setRef = 'myRef'; 231 | var e = jsx(
    ); 232 | Assert.areEqual('NATIVE', e.type); 233 | } 234 | 235 | @Test 236 | public function DOM_with_ref_unknown_should_be_DEOPT() 237 | { 238 | var setRef:Dynamic = function() {}; 239 | var e = jsx(
    ); 240 | Assert.areEqual('NATIVE', e.type); 241 | } 242 | 243 | @Test 244 | public function DOM_with_single_child_text_should_NOT_be_array() 245 | { 246 | var e = jsx(
    hello
    ); 247 | Assert.areEqual('div', e.type); 248 | assertHasProps(e.props, ['children']); 249 | var children = e.props.children; 250 | Assert.areEqual('hello', children); 251 | } 252 | 253 | @Test 254 | public function DOM_with_single_child_node_should_NOT_be_array() 255 | { 256 | var e = jsx(
    ); 257 | Assert.areEqual('div', e.type); 258 | assertHasProps(e.props, ['children']); 259 | var children = e.props.children; 260 | Assert.isFalse(Std.is(children, Array)); 261 | Assert.areEqual('span', children.type); 262 | } 263 | 264 | @Test 265 | public function DOM_with_single_child_binding_should_NOT_be_array() 266 | { 267 | var o = { name:'o' }; 268 | var e = jsx(
    ${o}
    ); 269 | Assert.areEqual('div', e.type); 270 | Assert.areEqual(e.props.children, o); 271 | } 272 | 273 | @Test 274 | public function DOM_with_children_should_be_array() 275 | { 276 | var e = jsx(
    hello
    ); 277 | Assert.areEqual('div', e.type); 278 | assertHasProps(e.props, ['children']); 279 | var children = e.props.children; 280 | Assert.isTrue(Std.is(children, Array)); 281 | Assert.areEqual('hello ', children[0]); 282 | Assert.areEqual('span', children[1].type); 283 | } 284 | 285 | /* TOOLS */ 286 | 287 | function RenderFunction() 288 | { 289 | return jsx(
    ); 290 | } 291 | 292 | #end 293 | } 294 | -------------------------------------------------------------------------------- /test/src/ReactMacroTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import massive.munit.Assert; 4 | import react.ReactComponent; 5 | import react.ReactMacro.jsx; 6 | import support.sub.CompExternModule; 7 | import support.sub.CompModule; 8 | import AssertTools.assertHasProps; 9 | 10 | class CompBasic extends ReactComponent { 11 | override function render() { 12 | return jsx('
    '); 13 | } 14 | } 15 | class CompDefaults extends ReactComponent { 16 | static public var defaultProps = { 17 | defA:'A', 18 | defB:42 19 | } 20 | override function render() { 21 | return jsx('
    '); 22 | } 23 | } 24 | extern class CompExtern extends ReactComponent { 25 | } 26 | 27 | 28 | class ReactMacroTest 29 | { 30 | public function new() {} 31 | 32 | @BeforeClass 33 | public function setup() 34 | { 35 | untyped __js__("$global.CompExtern = function() {}"); 36 | untyped __js__("$global.CompExternModule = function() {}"); 37 | } 38 | 39 | @Test 40 | public function DOM_without_props() 41 | { 42 | var e = jsx('
    '); 43 | Assert.areEqual('div', e.type); 44 | assertHasProps(e.props, []); 45 | } 46 | 47 | @Test 48 | public function DOM_with_const_props() 49 | { 50 | var e = jsx('
    '); 51 | Assert.areEqual('div', e.type); 52 | assertHasProps(e.props, ['a'], ['foo']); 53 | } 54 | 55 | @Test 56 | public function DOM_with_const_and_binding_props() 57 | { 58 | var foo = 12; 59 | var e = jsx('
    '); 60 | Assert.areEqual('div', e.type); 61 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 62 | } 63 | 64 | @Test 65 | public function function_with_props() 66 | { 67 | var e = jsx(''); 68 | Assert.areEqual(RenderFunction, e.type); 69 | assertHasProps(e.props, ['a'], ['foo']); 70 | } 71 | 72 | @Test 73 | public function component_with_props() 74 | { 75 | var e = jsx(''); 76 | Assert.areEqual(CompBasic, e.type); 77 | assertHasProps(e.props, ['a'], ['foo']); 78 | } 79 | 80 | @Test 81 | public function extern_component_qualified_module_should_DEOPT() 82 | { 83 | var e = jsx(''); 84 | Assert.areEqual('NATIVE', e.type); 85 | } 86 | 87 | @Test 88 | public function extern_component_module_should_DEOPT() 89 | { 90 | var e = jsx(''); 91 | Assert.areEqual('NATIVE', e.type); 92 | } 93 | 94 | @Test 95 | public function extern_component_should_DEOPT() 96 | { 97 | var e = jsx(''); 98 | Assert.areEqual('NATIVE', e.type); 99 | } 100 | 101 | @Test 102 | public function DOM_with_spread() 103 | { 104 | var o = { 105 | a:'foo', 106 | b:12 107 | } 108 | var e = jsx('
    '); 109 | Assert.areEqual('div', e.type); 110 | assertHasProps(e.props, ['a', 'b'], (['foo', 12]:Array)); 111 | } 112 | 113 | @Test 114 | public function DOM_with_spread_and_prop() 115 | { 116 | var o = { 117 | a:'foo', 118 | b:12 119 | } 120 | var e = jsx('
    '); 121 | Assert.areEqual('div', e.type); 122 | assertHasProps(e.props, ['a', 'b', 'c'], (['foo', 12, 'bar']:Array)); 123 | } 124 | 125 | @Test 126 | public function DOM_with_spread_and_prop_override() 127 | { 128 | var o = { 129 | a:'foo', 130 | b:12 131 | } 132 | var e = jsx('
    '); 133 | Assert.areEqual('div', e.type); 134 | assertHasProps(e.props, ['a', 'b'], (['bar', 12]:Array)); 135 | } 136 | 137 | @Test 138 | public function component_with_defaultProps() 139 | { 140 | var e = jsx(''); 141 | Assert.areEqual(CompDefaults, e.type); 142 | assertHasProps(e.props, ['defA', 'defB'], (['A', 42]:Array)); 143 | } 144 | 145 | @Test 146 | public function component_in_sub_package_with_defaultProps() 147 | { 148 | var e = jsx(''); 149 | Assert.areEqual(CompModule, e.type); 150 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 151 | } 152 | 153 | @Test 154 | public function qualified_component_in_sub_package_with_defaultProps() 155 | { 156 | var e = jsx(''); 157 | Assert.areEqual(CompModule, e.type); 158 | assertHasProps(e.props, ['defA', 'defB'], (['B', 43]:Array)); 159 | } 160 | 161 | @Test 162 | public function component_with_defaultProps_and_spread() 163 | { 164 | var o = { 165 | a:'foo', 166 | b:12 167 | } 168 | var e = jsx(''); 169 | Assert.areEqual(CompDefaults, e.type); 170 | assertHasProps(e.props, ['defA', 'defB', 'a', 'b'], (['A', 42, 'foo', 12]:Array)); 171 | } 172 | 173 | @Test 174 | public function component_with_defaultProps_and_prop_override() 175 | { 176 | var e = jsx(''); 177 | Assert.areEqual(CompDefaults, e.type); 178 | assertHasProps(e.props, ['defA', 'defB'], (['foo', 42]:Array)); 179 | } 180 | 181 | @Test 182 | public function component_with_defaultProps_and_spread_override() 183 | { 184 | var o = { 185 | defA:'foo', 186 | b:12 187 | } 188 | var e = jsx(''); 189 | Assert.areEqual(CompDefaults, e.type); 190 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['foo', 42, 12]:Array)); 191 | } 192 | 193 | @Test 194 | public function component_with_defaultProps_and_spread_and_prop_override() 195 | { 196 | var o = { 197 | defA:'foo', 198 | b:12 199 | } 200 | var e = jsx(''); 201 | Assert.areEqual(CompDefaults, e.type); 202 | assertHasProps(e.props, ['defA', 'defB', 'b'], (['bar', 42, 12]:Array)); 203 | } 204 | 205 | @Test 206 | public function DOM_with_ref_function_should_be_inlined() 207 | { 208 | function setRef() {}; 209 | var e = jsx('
    '); 210 | Assert.areEqual('div', e.type); 211 | Assert.areEqual(setRef, e.ref); 212 | assertHasProps(e.props, []); 213 | } 214 | 215 | @Test 216 | public function DOM_with_ref_const_string_should_be_DEOPT() 217 | { 218 | var e = jsx('
    '); 219 | Assert.areEqual('NATIVE', e.type); 220 | } 221 | 222 | @Test 223 | public function DOM_with_ref_string_should_be_DEOPT() 224 | { 225 | var setRef = 'myRef'; 226 | var e = jsx('
    '); 227 | Assert.areEqual('NATIVE', e.type); 228 | } 229 | 230 | @Test 231 | public function DOM_with_ref_unknown_should_be_DEOPT() 232 | { 233 | var setRef:Dynamic = function() {}; 234 | var e = jsx('
    '); 235 | Assert.areEqual('NATIVE', e.type); 236 | } 237 | 238 | @Test 239 | public function DOM_with_single_child_text_should_NOT_be_array() 240 | { 241 | var e = jsx('
    hello
    '); 242 | Assert.areEqual('div', e.type); 243 | assertHasProps(e.props, ['children']); 244 | var children = e.props.children; 245 | Assert.areEqual('hello', children); 246 | } 247 | 248 | @Test 249 | public function DOM_with_single_child_node_should_NOT_be_array() 250 | { 251 | var e = jsx('
    '); 252 | Assert.areEqual('div', e.type); 253 | assertHasProps(e.props, ['children']); 254 | var children = e.props.children; 255 | Assert.isFalse(Std.is(children, Array)); 256 | Assert.areEqual('span', children.type); 257 | } 258 | 259 | @Test 260 | public function DOM_with_single_child_binding_should_NOT_be_array() 261 | { 262 | var o = { name:'o' }; 263 | var e = jsx('
    ${o}
    '); 264 | Assert.areEqual('div', e.type); 265 | Assert.areEqual(e.props.children, o); 266 | } 267 | 268 | @Test 269 | public function DOM_with_children_should_be_array() 270 | { 271 | var e = jsx('
    hello
    '); 272 | Assert.areEqual('div', e.type); 273 | assertHasProps(e.props, ['children']); 274 | var children = e.props.children; 275 | Assert.isTrue(Std.is(children, Array)); 276 | Assert.areEqual('hello ', children[0]); 277 | Assert.areEqual('span', children[1].type); 278 | } 279 | 280 | /* TOOLS */ 281 | 282 | function RenderFunction() 283 | { 284 | return jsx('
    '); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /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 | #if MCOVER 26 | var client = new mcover.coverage.munit.client.MCoverPrintClient(); 27 | var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient()); 28 | #else 29 | var client = new RichPrintClient(); 30 | var httpClient = new HTTPClient(new SummaryReportClient()); 31 | #end 32 | 33 | var runner:TestRunner = new TestRunner(client); 34 | runner.addResultClient(httpClient); 35 | //runner.addResultClient(new HTTPClient(new JUnitReportClient())); 36 | 37 | runner.completionHandler = completionHandler; 38 | 39 | #if (js && !nodejs) 40 | var seconds = 0; // edit here to add some startup delay 41 | function delayStartup() 42 | { 43 | if (seconds > 0) { 44 | seconds--; 45 | js.Browser.document.getElementById("munit").innerHTML = 46 | "Tests will start in " + seconds + "s..."; 47 | haxe.Timer.delay(delayStartup, 1000); 48 | } 49 | else { 50 | js.Browser.document.getElementById("munit").innerHTML = ""; 51 | runner.run(suites); 52 | } 53 | } 54 | delayStartup(); 55 | #else 56 | runner.run(suites); 57 | #end 58 | } 59 | 60 | /* 61 | updates the background color and closes the current browser 62 | for flash and html targets (useful for continous integration servers) 63 | */ 64 | function completionHandler(successful:Bool):Void 65 | { 66 | try 67 | { 68 | #if flash 69 | flash.external.ExternalInterface.call("testResult", successful); 70 | #elseif js 71 | js.Lib.eval("testResult(" + successful + ");"); 72 | #elseif sys 73 | Sys.exit(0); 74 | #end 75 | } 76 | // if run from outside browser can get error which we can ignore 77 | catch (e:Dynamic) 78 | { 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/src/TestSuite.hx: -------------------------------------------------------------------------------- 1 | import massive.munit.TestSuite; 2 | 3 | import JsxSanitizeTest; 4 | import ReactMacroInlineMarkupTest; 5 | import JsxParserTest; 6 | import ReactMacroTest; 7 | 8 | /** 9 | * Auto generated Test Suite for MassiveUnit. 10 | * Refer to munit command line tool for more information (haxelib run munit) 11 | */ 12 | class TestSuite extends massive.munit.TestSuite 13 | { 14 | public function new() 15 | { 16 | super(); 17 | 18 | add(JsxSanitizeTest); 19 | add(ReactMacroInlineMarkupTest); 20 | add(JsxParserTest); 21 | add(ReactMacroTest); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/src/react/React.hx: -------------------------------------------------------------------------------- 1 | package react; 2 | 3 | import react.ReactComponent.ReactElement; 4 | 5 | /** 6 | STUB 7 | **/ 8 | class React 9 | { 10 | /** 11 | https://facebook.github.io/react/docs/react-api.html#react.proptypes 12 | **/ 13 | public static var PropTypes(default, null):ReactPropTypes; 14 | 15 | /** 16 | https://facebook.github.io/react/docs/react-api.html#createelement 17 | **/ 18 | public static function createElement(type:CreateElementType, ?attrs:Dynamic, ?children:Dynamic):ReactElement 19 | { 20 | return untyped { type:'NATIVE' }; 21 | } 22 | 23 | /** 24 | https://facebook.github.io/react/docs/react-api.html#cloneelement 25 | **/ 26 | public static function cloneElement(element:ReactElement, ?attrs:Dynamic, ?children:Dynamic):ReactElement 27 | { 28 | return untyped { type:'NATIVE' }; 29 | } 30 | 31 | /** 32 | https://facebook.github.io/react/docs/react-api.html#isvalidelement 33 | **/ 34 | public static function isValidElement(object:Dynamic):Bool 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | https://facebook.github.io/react/docs/react-api.html#react.children 41 | **/ 42 | public static var Children:ReactChildren; 43 | } 44 | 45 | /** 46 | https://facebook.github.io/react/docs/react-api.html#react.children 47 | **/ 48 | extern interface ReactChildren 49 | { 50 | /** 51 | https://facebook.github.io/react/docs/react-api.html#react.children.map 52 | **/ 53 | function map(children:Dynamic, fn:ReactElement->ReactElement):Dynamic; 54 | 55 | /** 56 | https://facebook.github.io/react/docs/react-api.html#react.children.foreach 57 | **/ 58 | function foreach(children:Dynamic, fn:ReactElement->Void):Void; 59 | 60 | /** 61 | https://facebook.github.io/react/docs/react-api.html#react.children.count 62 | **/ 63 | function count(children:Dynamic):Int; 64 | 65 | /** 66 | https://facebook.github.io/react/docs/react-api.html#react.children.only 67 | **/ 68 | function only(children:Dynamic):ReactElement; 69 | 70 | /** 71 | https://facebook.github.io/react/docs/react-api.html#react.children.toarray 72 | **/ 73 | function toArray(children:Dynamic):Array; 74 | } 75 | 76 | typedef CreateElementType = haxe.extern.EitherType, Class>; 77 | -------------------------------------------------------------------------------- /test/src/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 | typedef ReactComponentProps = { 10 | /** 11 | Children have to be manipulated using React.Children.* 12 | **/ 13 | @:optional var children:Dynamic; 14 | } 15 | 16 | /** 17 | STUB CLASSES 18 | **/ 19 | typedef ReactComponent = ReactComponentOf; 20 | 21 | typedef ReactComponentOfProps = ReactComponentOf; 22 | typedef ReactComponentOfState = ReactComponentOf; 23 | 24 | // Keep the old ReactComponentOfPropsAndState typedef available 25 | typedef ReactComponentOfPropsAndState = ReactComponentOf; 26 | 27 | @:autoBuild(react.ReactComponentMacro.build()) 28 | class ReactComponentOf 29 | { 30 | static var defaultProps:Dynamic; 31 | static var contextTypes:Dynamic; 32 | 33 | var props(default, null):TProps; 34 | var state(default, null):TState; 35 | 36 | #if react_deprecated_context 37 | // It's better to define it in your ReactComponent subclass as needed, with the right typing. 38 | var context(default, null):Dynamic; 39 | #end 40 | 41 | function new(?props:TProps) {} 42 | 43 | /** 44 | https://facebook.github.io/react/docs/react-component.html#forceupdate 45 | **/ 46 | function forceUpdate(?callback:Void -> Void):Void {} 47 | 48 | /** 49 | https://facebook.github.io/react/docs/react-component.html#setstate 50 | **/ 51 | function setState(nextState:Dynamic, ?callback:Void -> Void):Void {}; 52 | 53 | /** 54 | https://facebook.github.io/react/docs/react-component.html#render 55 | **/ 56 | function render():ReactElement { return null; } 57 | 58 | /** 59 | https://facebook.github.io/react/docs/react-component.html#componentwillmount 60 | **/ 61 | function componentWillMount():Void {} 62 | 63 | /** 64 | https://facebook.github.io/react/docs/react-component.html#componentdidmount 65 | **/ 66 | function componentDidMount():Void {} 67 | 68 | /** 69 | https://facebook.github.io/react/docs/react-component.html#componentwillunmount 70 | **/ 71 | function componentWillUnmount():Void {} 72 | 73 | /** 74 | https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops 75 | **/ 76 | function componentWillReceiveProps(nextProps:TProps):Void {} 77 | 78 | /** 79 | https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate 80 | **/ 81 | dynamic function shouldComponentUpdate(nextProps:TProps, nextState:TState):Bool { return true; } 82 | 83 | /** 84 | https://facebook.github.io/react/docs/react-component.html#componentwillupdate 85 | **/ 86 | function componentWillUpdate(nextProps:TProps, nextState:TState):Void {} 87 | 88 | /** 89 | https://facebook.github.io/react/docs/react-component.html#componentdidupdate 90 | Note: Updated to version introduced in React 16.3 91 | **/ 92 | #if react_snapshot_api 93 | function componentDidUpdate(prevProps:TProps, prevState:TState, ?snapshot:Dynamic):Void {} 94 | #else 95 | function componentDidUpdate(prevProps:TProps, prevState:TState):Void {} 96 | #end 97 | 98 | /** 99 | https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html 100 | **/ 101 | function componentDidCatch(error:Error, info:{ componentStack:String }):Void {} 102 | 103 | /** 104 | https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate 105 | Note: this API has been introduced in React 16.3 106 | **/ 107 | #if react_snapshot_api 108 | function getSnapshotBeforeUpdate(prevProps:TProps, prevState:TState):Dynamic; 109 | #end 110 | 111 | static function __init__():Void { 112 | // required magic value to tag literal react elements 113 | untyped __js__("var $$tre = (typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\")) || 0xeac7"); 114 | } 115 | } 116 | 117 | typedef ReactElement = { 118 | type:Dynamic, 119 | props:Dynamic, 120 | ?key:Dynamic, 121 | ?ref:Dynamic 122 | } 123 | -------------------------------------------------------------------------------- /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.ReactMacro.jsx; 4 | import react.ReactComponent; 5 | 6 | class CompModule extends ReactComponent 7 | { 8 | static public var defaultProps = { 9 | defA:'B', 10 | defB:43 11 | } 12 | 13 | public function new() { 14 | super(); 15 | } 16 | 17 | override function render() { 18 | return jsx('
    '); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test.hxml: -------------------------------------------------------------------------------- 1 | -main TestMain 2 | -lib munit 3 | -lib html-entities 4 | -cp src/lib 5 | 6 | -lib hxnodejs 7 | -cp test/src 8 | -js test/build/js_test.js 9 | --------------------------------------------------------------------------------