├── .babelrc.js ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── ReadMe.md ├── examples ├── App.js └── index.html ├── flow-typed ├── rdl-types.js └── react-reconciler.js ├── package.json ├── scripts └── rollup.config.js ├── src ├── DOMComponent.js ├── DOMComponentTree.js ├── DOMConfig.js ├── DOMProperties.js ├── HTMLNodeType.js ├── Reconciler.js ├── Root.js ├── SSRHydrationDev.js ├── SSRHydrationProd.js ├── __tests__ │ ├── DOMProperties.js │ ├── SVG.js │ ├── context.js │ ├── findDOMNode.js │ ├── test-utils.js │ └── unmountComponentAtNode.js ├── events │ ├── SyntheticEvent.js │ ├── __tests__ │ │ └── events.js │ └── index.js ├── index.js └── test-utils.js ├── test ├── setup.js └── setupTests.js ├── webpack.config.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | const dev = process.env.NODE_ENV !== 'production'; 2 | const BUILD = !!process.env.ROLLUP; 3 | 4 | module.exports = { 5 | presets: [ 6 | [ 7 | '@babel/env', 8 | { 9 | modules: BUILD ? false : 'commonjs', 10 | shippedProposals: true, 11 | loose: true, 12 | targets: { 13 | browsers: [ 14 | 'last 2 versions', 15 | 'last 4 android versions', 16 | 'last 4 iOS versions', 17 | 'safari >= 7', 18 | 'not ie <= 11', 19 | 'not android <= 4.4.3', 20 | ], 21 | }, 22 | }, 23 | ], 24 | '@babel/flow', 25 | ['@babel/react', { development: dev }], 26 | ], 27 | plugins: [['@babel/plugin-proposal-class-properties', { loose: true }]], 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["flowtype"], 4 | "extends": ["jason/react", "prettier", "plugin:flowtype/recommended"], 5 | "globals": { 6 | "__DEV__": false, 7 | "__SVG__": false, 8 | "performance": false 9 | }, 10 | "env": { 11 | "browser": true 12 | }, 13 | "rules": { 14 | "flowtype/generic-spacing": "off", 15 | "flowtype/use-flow-type": "error", 16 | "no-restricted-syntax": [ 17 | "error", 18 | "AwaitExpression", 19 | "ArrowFunctionExpression[async=true]", 20 | "FunctionDeclaration[async=true]", 21 | "FunctionExpression[async=true]" 22 | ] 23 | }, 24 | "overrides": [ 25 | { 26 | "files": ["**/__tests__/**/*.js", "test/**"], 27 | "env": { 28 | "jest": true 29 | }, 30 | "rules": { 31 | "react/prop-types": "off", 32 | "global-require": "off", 33 | "no-console": "off" 34 | }, 35 | "settings": { 36 | "import/core-modules": [ 37 | "react-dom-lite", 38 | "react-dom-lite/test-utils" 39 | ] 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: "yarn" 4 | node_js: 5 | - 8 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at monastic.panic@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # React DOM Lite 2 | 3 | _"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” - Antoine de Saint-Exupery._ 4 | 5 | Compliance and amazing cross browser support led to a robust but heavy `react-dom`. React DOM Lite is an attempt to sculpt away some of the mass away and see if we can make something more low powered device friendly. 6 | 7 | ## Road map 8 | 9 | Keeping in mind the existing React ecosystem (and of course the web ecosystem too), following is the feature list to attain feature parity with the existing react-dom: 10 | 11 | * SVG and namespaced attribute support 12 | * Event normalization / polyfilling 13 | * Portals (event propagation) 14 | * Controlled inputs 15 | * Browser support matrix 16 | * SSR, hydration. 17 | 18 | The goal is to be compatible with the react ecosystem, while remaining lite. This will likely mean that the supported browsers, will be more limited than react-dom, and attempts to polyfill differences between browsers will be limited and more tightly scoped. 19 | -------------------------------------------------------------------------------- /examples/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as DomLite from 'react-dom-lite'; 3 | 4 | class Counter extends React.Component { 5 | constructor(...args) { 6 | super(...args); 7 | 8 | this.state = { count: 0 }; 9 | } 10 | handleCount() { 11 | this.setState(({ count }) => { 12 | return { 13 | count: count + 1, 14 | }; 15 | }); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | 22 | count: 23 | 24 | {` ${this.state.count}`} 25 |
26 | 29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | class Select extends React.Component { 37 | state = { value: 3 }; 38 | handleChange = (e) => { 39 | this.setState({ value: e.target.value }); 40 | }; 41 | render() { 42 | return ( 43 | 49 | ); 50 | } 51 | } 52 | 53 | function Form() { 54 | const [name, setName] = useState(''); 55 | 56 | const handleNameChange = (e) => { 57 | setName(e.target.value); 58 | DomLite.findDOMNode(this); 59 | }; 60 | 61 | return ( 62 | 63 | 65 | 66 |
69 | 70 | ); 71 | } 72 | 73 | DomLite.render(
, document.getElementById('app')); 74 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /flow-typed/rdl-types.js: -------------------------------------------------------------------------------- 1 | declare type DOMContainer = Element | Document; 2 | 3 | declare var __DEV__: boolean; 4 | declare var __SVG__: boolean; 5 | 6 | declare type Props = { [key: string]: mixed } & { 7 | autoFocus?: boolean, 8 | children?: mixed, 9 | hidden?: boolean, 10 | }; 11 | 12 | declare type HostContext = { 13 | isSvg: boolean, 14 | }; 15 | -------------------------------------------------------------------------------- /flow-typed/react-reconciler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type Fiber = any; 4 | type FiberRoot = any; 5 | 6 | type Deadline = { 7 | timeRemaining: () => number, 8 | }; 9 | 10 | export type ReactNode = 11 | | React$Element 12 | | ReactCall 13 | | ReactReturn 14 | | ReactPortal 15 | | ReactText 16 | | ReactFragment; 17 | 18 | type ReactFragment = ReactEmpty | Iterable; 19 | 20 | type ReactNodeList = ReactEmpty | React$Node; 21 | 22 | type ReactText = string | number; 23 | 24 | type ReactEmpty = null | void | boolean; 25 | 26 | type ReactCall = { 27 | $$typeof: symbol | number, 28 | type: symbol | number, 29 | key: null | string, 30 | ref: null, 31 | props: { 32 | props: any, 33 | // This should be a more specific CallHandler 34 | handler: (props: any, returns: Array) => ReactNodeList, 35 | children?: ReactNodeList, 36 | }, 37 | }; 38 | 39 | type ReactReturn = { 40 | $$typeof: symbol | number, 41 | type: symbol | number, 42 | key: null, 43 | ref: null, 44 | props: { 45 | value: V, 46 | }, 47 | }; 48 | 49 | type ReactPortal = { 50 | $$typeof: symbol, 51 | key: null | string, 52 | containerInfo: any, 53 | children: ReactNodeList, 54 | // TODO: figure out the API for cross-renderer implementation. 55 | implementation: any, 56 | }; 57 | 58 | declare module 'react-reconciler' { 59 | declare export type OpaqueHandle = Fiber; 60 | declare export type OpaqueRoot = FiberRoot; 61 | declare export type ExpirationTime = number; 62 | 63 | declare export type HostConfig = { 64 | getRootHostContext(rootContainerInstance: C): CX, 65 | getChildHostContext(parentHostContext: CX, type: T, instance: C): CX, 66 | getPublicInstance(instance: I | TI): PI, 67 | 68 | createInstance( 69 | type: T, 70 | props: P, 71 | rootContainerInstance: C, 72 | hostContext: CX, 73 | internalInstanceHandle: OpaqueHandle, 74 | ): I, 75 | appendInitialChild(parentInstance: I, child: I | TI): void, 76 | finalizeInitialChildren( 77 | parentInstance: I, 78 | type: T, 79 | props: P, 80 | rootContainerInstance: C, 81 | hostContext: CX, 82 | ): boolean, 83 | 84 | prepareUpdate( 85 | instance: I, 86 | type: T, 87 | oldProps: P, 88 | newProps: P, 89 | rootContainerInstance: C, 90 | hostContext: CX, 91 | ): null | PL, 92 | 93 | shouldSetTextContent(type: T, props: P): boolean, 94 | shouldDeprioritizeSubtree(type: T, props: P): boolean, 95 | 96 | createTextInstance( 97 | text: string, 98 | rootContainerInstance: C, 99 | hostContext: CX, 100 | internalInstanceHandle: OpaqueHandle, 101 | ): TI, 102 | 103 | scheduleDeferredCallback( 104 | callback: (deadline: Deadline) => void, 105 | options?: { timeout: number }, 106 | ): number, 107 | cancelDeferredCallback(callbackID: number): void, 108 | 109 | prepareForCommit(): void, 110 | resetAfterCommit(): void, 111 | 112 | now(): number, 113 | 114 | useSyncScheduling?: boolean, 115 | 116 | +hydration?: HydrationHostConfig, 117 | 118 | +mutation?: MutableUpdatesHostConfig, 119 | +persistence?: PersistentUpdatesHostConfig, 120 | }; 121 | 122 | declare type MutableUpdatesHostConfig = { 123 | commitUpdate( 124 | instance: I, 125 | updatePayload: PL, 126 | type: T, 127 | oldProps: P, 128 | newProps: P, 129 | internalInstanceHandle: OpaqueHandle, 130 | ): void, 131 | commitMount( 132 | instance: I, 133 | type: T, 134 | newProps: P, 135 | internalInstanceHandle: OpaqueHandle, 136 | ): void, 137 | commitTextUpdate(textInstance: TI, oldText: string, newText: string): void, 138 | resetTextContent(instance: I): void, 139 | appendChild(parentInstance: I, child: I | TI): void, 140 | appendChildToContainer(container: C, child: I | TI): void, 141 | insertBefore(parentInstance: I, child: I | TI, beforeChild: I | TI): void, 142 | insertInContainerBefore( 143 | container: C, 144 | child: I | TI, 145 | beforeChild: I | TI, 146 | ): void, 147 | removeChild(parentInstance: I, child: I | TI): void, 148 | removeChildFromContainer(container: C, child: I | TI): void, 149 | }; 150 | 151 | declare type PersistentUpdatesHostConfig = { 152 | cloneInstance( 153 | instance: I, 154 | updatePayload: null | PL, 155 | type: T, 156 | oldProps: P, 157 | newProps: P, 158 | internalInstanceHandle: OpaqueHandle, 159 | keepChildren: boolean, 160 | recyclableInstance: I, 161 | ): I, 162 | 163 | createContainerChildSet(container: C): CC, 164 | 165 | appendChildToContainerChildSet(childSet: CC, child: I | TI): void, 166 | finalizeContainerChildren(container: C, newChildren: CC): void, 167 | 168 | replaceContainerChildren(container: C, newChildren: CC): void, 169 | }; 170 | 171 | declare type HydrationHostConfig = { 172 | // Optional hydration 173 | canHydrateInstance(instance: HI, type: T, props: P): null | I, 174 | canHydrateTextInstance(instance: HI, text: string): null | TI, 175 | getNextHydratableSibling(instance: I | TI | HI): null | HI, 176 | getFirstHydratableChild(parentInstance: I | C): null | HI, 177 | hydrateInstance( 178 | instance: I, 179 | type: T, 180 | props: P, 181 | rootContainerInstance: C, 182 | hostContext: CX, 183 | internalInstanceHandle: OpaqueHandle, 184 | ): null | PL, 185 | hydrateTextInstance( 186 | textInstance: TI, 187 | text: string, 188 | internalInstanceHandle: OpaqueHandle, 189 | ): boolean, 190 | didNotMatchHydratedContainerTextInstance( 191 | parentContainer: C, 192 | textInstance: TI, 193 | text: string, 194 | ): void, 195 | didNotMatchHydratedTextInstance( 196 | parentType: T, 197 | parentProps: P, 198 | parentInstance: I, 199 | textInstance: TI, 200 | text: string, 201 | ): void, 202 | didNotHydrateContainerInstance(parentContainer: C, instance: I | TI): void, 203 | didNotHydrateInstance( 204 | parentType: T, 205 | parentProps: P, 206 | parentInstance: I, 207 | instance: I | TI, 208 | ): void, 209 | didNotFindHydratableContainerInstance( 210 | parentContainer: C, 211 | type: T, 212 | props: P, 213 | ): void, 214 | didNotFindHydratableContainerTextInstance( 215 | parentContainer: C, 216 | text: string, 217 | ): void, 218 | didNotFindHydratableInstance( 219 | parentType: T, 220 | parentProps: P, 221 | parentInstance: I, 222 | type: T, 223 | props: P, 224 | ): void, 225 | didNotFindHydratableTextInstance( 226 | parentType: T, 227 | parentProps: P, 228 | parentInstance: I, 229 | text: string, 230 | ): void, 231 | }; 232 | 233 | // 0 is PROD, 1 is DEV. 234 | // Might add PROFILE later. 235 | declare type BundleType = 0 | 1; 236 | 237 | declare type DevToolsConfig = {| 238 | bundleType: BundleType, 239 | version: string, 240 | rendererPackageName: string, 241 | // Note: this actually *does* depend on Fiber internal fields. 242 | // Used by "inspect clicked DOM element" in React DevTools. 243 | findFiberByHostInstance?: (instance: I | TI) => Fiber, 244 | // Used by RN in-app inspector. 245 | // This API is unfortunately RN-specific. 246 | // TODO: Change it to accept Fiber instead and type it properly. 247 | getInspectorDataForViewTag?: (tag: number) => Object, 248 | |}; 249 | 250 | declare export type Reconciler = { 251 | createContainer( 252 | containerInfo: C, 253 | isAsync: boolean, 254 | hydrate: boolean, 255 | ): OpaqueRoot, 256 | updateContainer( 257 | element: ReactNodeList, 258 | container: OpaqueRoot, 259 | parentComponent: ?React$Component, 260 | callback: ?Function, 261 | ): ExpirationTime, 262 | updateContainerAtExpirationTime( 263 | element: ReactNodeList, 264 | container: OpaqueRoot, 265 | parentComponent: ?React$Component, 266 | expirationTime: ExpirationTime, 267 | callback: ?Function, 268 | ): ExpirationTime, 269 | flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void, 270 | requestWork(root: OpaqueRoot, expirationTime: ExpirationTime): void, 271 | batchedUpdates(fn: () => A): A, 272 | unbatchedUpdates(fn: () => A): A, 273 | flushSync(fn: () => A): A, 274 | deferredUpdates(fn: () => A): A, 275 | injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean, 276 | computeUniqueAsyncExpiration(): ExpirationTime, 277 | 278 | // Used to extract the return value from the initial render. Legacy API. 279 | getPublicRootInstance( 280 | container: OpaqueRoot, 281 | ): React$Component | TI | I | null, 282 | 283 | // Use for findDOMNode/findHostNode. Legacy API. 284 | findHostInstance(component: Fiber): I | TI | null, 285 | 286 | // Used internally for filtering out portals. Legacy API. 287 | findHostInstanceWithNoPortals(component: Fiber): I | TI | null, 288 | }; 289 | 290 | declare export default function createReconciler< 291 | T, 292 | P, 293 | I, 294 | TI, 295 | HI, 296 | PI, 297 | C, 298 | CC, 299 | CX, 300 | PL, 301 | >( 302 | config: HostConfig, 303 | ): Reconciler; 304 | } 305 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dom-lite", 3 | "version": "0.4.0", 4 | "main": "lib/react-dom-lite.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build:dev": "ROLLUP=1 rollup -c scripts/rollup.config.js", 8 | "build:prod": "ROLLUP=1 NODE_ENV=production rollup -c scripts/rollup.config.js", 9 | "build": "npm run build:dev && npm run build:prod", 10 | "watch": "npm run babel -- --watch", 11 | "examples": "webpack-dev-server --mode development", 12 | "prettier": "prettier --write src/**/*.js", 13 | "precommit": "lint-staged", 14 | "pretest": "npm run flow && eslint src test", 15 | "prepublishOnly": "npm run test && npm run build", 16 | "test": "jest", 17 | "tdd": "jest --watch", 18 | "flow": "flow" 19 | }, 20 | "files": [ 21 | "lib" 22 | ], 23 | "lint-staged": { 24 | "src/**/*.{js}": [ 25 | "eslint" 26 | ], 27 | "*.{js,json,css,md}": [ 28 | "prettier --write", 29 | "git add" 30 | ] 31 | }, 32 | "eslintIgnore": [ 33 | "flow-typed" 34 | ], 35 | "jest": { 36 | "roots": [ 37 | "/src" 38 | ], 39 | "setupFiles": [ 40 | "./test/setup.js" 41 | ], 42 | "setupTestFrameworkScriptFile": "./test/setupTests.js", 43 | "moduleNameMapper": { 44 | "react-dom-lite/test-utils": "/src/test-utils.js", 45 | "react-dom-lite": "/src/index.js" 46 | } 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.8.4", 50 | "@babel/core": "^7.9.0", 51 | "@babel/plugin-external-helpers": "^7.8.3", 52 | "@babel/plugin-proposal-class-properties": "^7.8.3", 53 | "@babel/preset-env": "^7.9.5", 54 | "@babel/preset-flow": "^7.9.0", 55 | "@babel/preset-react": "^7.9.4", 56 | "babel-core": "^7.0.0-0", 57 | "babel-eslint": "^10.1.0", 58 | "babel-jest": "^25.3.0", 59 | "compression-webpack-plugin": "^3.1.0", 60 | "eslint": "^6.8.0", 61 | "eslint-config-jason": "^7.0.1", 62 | "eslint-config-prettier": "^6.10.1", 63 | "eslint-plugin-flow": "^2.29.1", 64 | "eslint-plugin-flowtype": "^4.7.0", 65 | "eslint-plugin-import": "^2.20.2", 66 | "eslint-plugin-react": "^7.19.0", 67 | "flow-bin": "^0.122.0", 68 | "husky": "^4.2.5", 69 | "jest": "^25.3.0", 70 | "lint-staged": "^10.1.3", 71 | "prettier": "^2.0.4", 72 | "rollup": "^2.6.1", 73 | "rollup-plugin-babel": "4.4.0", 74 | "rollup-plugin-closure-compiler-js": "^1.0.6", 75 | "rollup-plugin-commonjs": "^10.1.0", 76 | "rollup-plugin-replace": "^2.2.0", 77 | "sinon": "^9.0.2", 78 | "webpack": "^4.42.1", 79 | "webpack-atoms": "^12.1.0", 80 | "webpack-cli": "^3.3.11", 81 | "webpack-dev-server": "^3.10.3" 82 | }, 83 | "dependencies": { 84 | "dom-helpers": "^5.1.4", 85 | "global": "^4.4.0", 86 | "invariant": "^2.2.4", 87 | "react": "^16.13.1", 88 | "react-reconciler": "0.25.1", 89 | "warning": "^4.0.3" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel'); 2 | const commonjs = require('rollup-plugin-commonjs'); 3 | const closure = require('rollup-plugin-closure-compiler-js'); 4 | const replace = require('rollup-plugin-replace'); 5 | 6 | const dev = process.env.NODE_ENV !== 'production'; 7 | 8 | module.exports = { 9 | input: 'src/index.js', 10 | output: { 11 | file: dev ? 'lib/react-dom-lite.js' : 'lib/react-dom-lite.min.js', 12 | format: 'cjs', 13 | }, 14 | plugins: [ 15 | replace({ __DEV__: dev, __SVG__: true }), // Needs attention! A way/config to selectively turn this on or off 16 | babel(), 17 | commonjs(), 18 | !dev && 19 | closure({ 20 | compilationLevel: 'SIMPLE', 21 | languageIn: 'ECMASCRIPT5_STRICT', 22 | languageOut: 'ECMASCRIPT5_STRICT', 23 | env: 'CUSTOM', 24 | rewritePolyfills: false, 25 | applyInputSourceMaps: false, 26 | processCommonJsModules: false, 27 | }), 28 | ].filter(Boolean), 29 | external: [ 30 | 'warning', 31 | 'dom-helpers/ownerDocument', 32 | 'dom-helpers/style', 33 | 'dom-helpers/util/hyphenate', 34 | 'invariant', 35 | 'react-reconciler', 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /src/DOMComponent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import css from 'dom-helpers/css'; 4 | import invariant from 'invariant'; 5 | import { setValueOnElement } from './DOMProperties'; 6 | import { isEventRegex } from './DOMConfig'; 7 | import * as Events from './events'; 8 | 9 | export function setInitialProps( 10 | domElement: Element, 11 | nextProps: Props, 12 | isSvg: boolean, 13 | ) { 14 | Object.entries(nextProps).forEach(([propKey, propValue]) => { 15 | let match; 16 | 17 | // inline styles! 18 | if (propKey === 'style') { 19 | css(domElement, propValue); 20 | 21 | // Quick support for dangerousSetInnerHTML={{__html}} 22 | } else if ( 23 | propKey === 'dangerouslySetInnerHTML' && 24 | propValue && 25 | propValue.__html != null 26 | ) { 27 | invariant( 28 | typeof propValue === 'object' && typeof propValue.__html === 'string', 29 | 'The dangerouslySetInnerHTML prop value must be an object with an single __html field', 30 | ); 31 | domElement.innerHTML = propValue.__html; 32 | // Handle when `children` is a renderable (text, number, etc) 33 | } else if (propKey === 'children') { 34 | // doesn't cover an IE issue with textareas 35 | if (typeof propValue === 'string' || typeof propValue === 'number') { 36 | domElement.textContent = `${propValue}`; 37 | } 38 | 39 | // Add DOM event listeners 40 | } else if ((match = propKey.match(isEventRegex))) { 41 | Events.listenTo(domElement, match[1], (propValue: any), null); 42 | } else if (propValue != null) { 43 | setValueOnElement(domElement, propKey, propValue, isSvg); 44 | } 45 | }); 46 | } 47 | 48 | function diffStyle(lastStyle: any, nextStyle: any) { 49 | let updates = null; 50 | if (lastStyle) { 51 | for (const lastKey in lastStyle) { 52 | if (!updates) updates = {}; 53 | updates[lastKey] = ''; 54 | } 55 | } 56 | 57 | if (!updates || !nextStyle) return nextStyle; 58 | 59 | return Object.assign(updates, nextStyle); 60 | } 61 | 62 | export function diffProps( 63 | domElement: Element, 64 | lastProps: Props, 65 | nextProps: Props, 66 | ): ?Array<[string, any]> { 67 | let updatePayload: ?Array<[string, any]> = null; 68 | 69 | let add = (k, v) => { 70 | if (!updatePayload) updatePayload = []; 71 | updatePayload.push([k, v]); 72 | }; 73 | 74 | for (let propKey of Object.keys(lastProps)) { 75 | // in case the event doesn't exist in the nextProps make sure the event 76 | // in the update queue so the handler is removed in `commitUpdate` 77 | if (!nextProps.hasOwnProperty(propKey) && propKey.match(isEventRegex)) { 78 | add(propKey, null); 79 | } 80 | } 81 | 82 | for (let entry of Object.entries(nextProps)) { 83 | const [propKey: string, nextProp: any] = entry; 84 | 85 | if (propKey === 'value' || propKey === 'checked') { 86 | // Value is always a string but React accepts most any type so we need 87 | // to compare the prop value as a string 88 | if ( 89 | (propKey === 'value' ? String(nextProp) : nextProp) !== 90 | (domElement: any)[propKey] 91 | ) 92 | add(propKey, nextProp); 93 | continue; 94 | } 95 | 96 | const lastProp = lastProps[propKey]; 97 | if ( 98 | propKey === 'style' || 99 | nextProp === lastProp || 100 | (nextProp == null && lastProp == null) 101 | ) { 102 | continue; 103 | } else if (propKey === 'dangerouslySetInnerHTML') { 104 | invariant( 105 | typeof nextProp === 'object' && typeof lastProp === 'object', 106 | 'The dangerouslySetInnerHTML prop value must be an object with an single __html field', 107 | ); 108 | const nextHtml = nextProp ? nextProp.__html : undefined; 109 | const lastHtml = lastProp ? lastProp.__html : undefined; 110 | if (nextHtml != null && lastHtml !== nextHtml) { 111 | add(propKey, nextHtml); 112 | } 113 | } else if (propKey === 'children') { 114 | if (typeof nextProp === 'string' || typeof nextProp === 'number') 115 | add(propKey, nextProp); 116 | } else { 117 | add(propKey, nextProp); 118 | } 119 | } 120 | 121 | let styleUpdates = diffStyle(lastProps.style, nextProps.style); 122 | if (styleUpdates) { 123 | add('style', styleUpdates); 124 | } 125 | 126 | return updatePayload; 127 | } 128 | 129 | export function updateProps( 130 | domElement: Element, 131 | updateQueue: Array<[string, any]>, 132 | lastProps: Props, 133 | isSvg: boolean, 134 | ) { 135 | let match; 136 | 137 | for (let [propKey, propValue] of updateQueue) { 138 | // inline styles! 139 | if (propKey === 'style') { 140 | css(domElement, propValue); 141 | // 142 | } else if (propKey === 'dangerouslySetInnerHTML') { 143 | domElement.innerHTML = propValue.__html; 144 | 145 | // Handle when `children` is a renderable (text, number, etc) 146 | } else if (propKey === 'children') { 147 | if (typeof propValue === 'string' || typeof propValue === 'number') { 148 | domElement.textContent = `${propValue}`; 149 | } 150 | 151 | // Add DOM event listeners 152 | } else if ((match = propKey.match(isEventRegex))) { 153 | Events.listenTo( 154 | domElement, 155 | match[1], 156 | propValue, 157 | (lastProps[propKey]: any), 158 | ); 159 | } else if (propValue != null) { 160 | setValueOnElement(domElement, propKey, propValue, isSvg); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/DOMComponentTree.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { OpaqueHandle } from 'react-reconciler'; 4 | 5 | const HostComponent = 5; 6 | const HostText = 6; 7 | 8 | type PublicInstance = PublicInstance; 9 | 10 | const ComponentInstanceMap: WeakMap< 11 | PublicInstance, 12 | OpaqueHandle 13 | > = new WeakMap(); 14 | 15 | export function cacheHandleByInstance( 16 | instance: Element, 17 | internalHandle: OpaqueHandle 18 | ) { 19 | ComponentInstanceMap.set(instance, internalHandle); 20 | } 21 | 22 | export function getInternalHandleFromInstance( 23 | instance: PublicInstance 24 | ): ?OpaqueHandle { 25 | if (ComponentInstanceMap.has(instance)) { 26 | return ComponentInstanceMap.get(instance); 27 | } 28 | 29 | // Walk up the tree until we find an ancestor whose instance we have cached. 30 | let element = instance; 31 | while (!ComponentInstanceMap.has(element)) { 32 | if (!element.parentElement) return null; 33 | element = element.parentElement; 34 | } 35 | 36 | const inst = ComponentInstanceMap.get(element); 37 | if (inst && (inst.tag === HostComponent || inst.tag === HostText)) { 38 | return inst; 39 | } 40 | return null; 41 | } 42 | 43 | export function collectAncestors(inst: PublicInstance): Array { 44 | let handle = ComponentInstanceMap.get(inst); 45 | const path: Array = []; 46 | while (handle) { 47 | path.push(inst); 48 | do { 49 | handle = handle.return; 50 | } while (handle && handle.tag !== HostComponent); 51 | inst = handle && handle.stateNode; 52 | } 53 | return path; 54 | } 55 | -------------------------------------------------------------------------------- /src/DOMConfig.js: -------------------------------------------------------------------------------- 1 | export const isEventRegex = /^on([A-Z][a-zA-Z]+)$/; 2 | 3 | export const ReservedPropNames = new Set([ 4 | 'children', 5 | 'dangerouslySetInnerHTML', 6 | 'innerHTML', 7 | ]); 8 | 9 | export const MapPropertyToAttribute = Object.create(null); 10 | ['rowSpan', 'colSpan', 'contentEditable', 'spellCheck'].forEach(name => { 11 | MapPropertyToAttribute[name] = name.toLowerCase(); 12 | }); 13 | 14 | export const isNamespaced = /^(xml|xlink|xmlns):?/i; 15 | 16 | export const MapNamespaceToUri = Object.create(null); 17 | 18 | // All SVG attributes without dash-casing 19 | if (__SVG__) { 20 | MapNamespaceToUri.xlink = 'http://www.w3.org/1999/xlink'; 21 | MapNamespaceToUri.xml = 'http://www.w3.org/XML/1998/namespace'; 22 | 23 | [ 24 | 'allowReorder', 25 | 'attributeName', 26 | 'attributeType', 27 | 'autoReverse', 28 | 'baseFrequency', 29 | 'baseProfile', 30 | 'calcMode', 31 | 'contentScriptType', 32 | 'contentStyleType', 33 | 'diffuseConstant', 34 | 'edgeMode', 35 | 'externalResourcesRequired', 36 | 'glyphRef', 37 | 'gradientTransform', 38 | 'gradientUnits', 39 | 'kernelMatrix', 40 | 'kernelUnitLength', 41 | 'keyPoints', 42 | 'keySplines', 43 | 'keyTimes', 44 | 'lengthAdjust', 45 | 'limitingConeAngle', 46 | 'markerHeight', 47 | 'markerUnits', 48 | 'markerWidth', 49 | 'maskContentUnits', 50 | 'maskUnits', 51 | 'numOctaves', 52 | 'paintOrder', 53 | 'pathLength', 54 | 'patternContentUnits', 55 | 'patternTransform', 56 | 'patternUnits', 57 | 'pointsAtX', 58 | 'pointsAtY', 59 | 'pointsAtZ', 60 | 'preserveAlpha', 61 | 'preserveAspectRatio', 62 | 'primitiveUnits', 63 | 'refX', 64 | 'refY', 65 | 'renderingIntent', 66 | 'repeatCount', 67 | 'repeatDur', 68 | 'requiredExtensions', 69 | 'requiredFeatures', 70 | 'shapeRendering', 71 | 'specularConstant', 72 | 'specularExponent', 73 | 'spreadMethod', 74 | 'startOffset', 75 | 'stdDeviation', 76 | 'stitchTiles', 77 | 'surfaceScale', 78 | 'systemLanguage', 79 | 'tableValues', 80 | 'targetX', 81 | 'targetY', 82 | 'textLength', 83 | 'viewBox', 84 | 'viewTarget', 85 | 'yChannelSelector', 86 | 'zoomAndPan', 87 | ].forEach(name => (MapPropertyToAttribute[name] = name)); 88 | } 89 | -------------------------------------------------------------------------------- /src/DOMProperties.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import hyphenate from 'dom-helpers/hyphenate'; 4 | 5 | import { 6 | isNamespaced, 7 | ReservedPropNames, 8 | MapPropertyToAttribute, 9 | MapNamespaceToUri, 10 | } from './DOMConfig'; 11 | 12 | /** 13 | * A string attribute that accepts react boolean values. The rendered 14 | * value should be "true" or "false", 15 | * e.g `` not `` 16 | */ 17 | const isStringBoolean = (key: string) => 18 | key === 'contenteditable' || 19 | key === 'draggable' || 20 | key === 'spellcheck' || 21 | key === 'value'; 22 | 23 | export function setValueOnElement( 24 | domElement: Element, 25 | propName: string, 26 | value: any, 27 | isSvg: boolean, 28 | ) { 29 | if (ReservedPropNames.has(propName)) return; 30 | 31 | if ( 32 | !isSvg && 33 | propName !== 'list' && 34 | propName !== 'type' && 35 | propName in domElement 36 | ) { 37 | (domElement: any)[propName] = value == null ? '' : value; 38 | return; 39 | } 40 | 41 | let ns = isSvg && propName.match(isNamespaced); 42 | if (ns) { 43 | ns = MapNamespaceToUri[ns[1]]; 44 | propName = propName.replace(isNamespaced, '').toLowerCase(); 45 | } 46 | 47 | // manually map inconsistent attribute names from consistent prop names, 48 | // otherwise assume it's predictably camelCase to dash-case 49 | const attributeName = MapPropertyToAttribute[propName] || hyphenate(propName); 50 | 51 | if (value == null) { 52 | if (ns) domElement.removeAttributeNS(ns, attributeName); 53 | else domElement.removeAttribute(attributeName); 54 | } else { 55 | if ((value === true || value === false) && isStringBoolean(attributeName)) { 56 | value = String(value); 57 | } else if (value === true) { 58 | value = ''; 59 | } else if (value === false) { 60 | value = undefined; 61 | } 62 | 63 | if (ns) { 64 | if (value) domElement.setAttributeNS(ns, attributeName, value); 65 | } else domElement.setAttribute(attributeName, value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/HTMLNodeType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML nodeType values that represent the type of the node 3 | */ 4 | 5 | export const ELEMENT_NODE = 1; 6 | export const TEXT_NODE = 3; 7 | export const COMMENT_NODE = 8; 8 | export const DOCUMENT_NODE = 9; 9 | export const DOCUMENT_FRAGMENT_NODE = 11; 10 | -------------------------------------------------------------------------------- /src/Reconciler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Reconciler, { type HostConfig } from 'react-reconciler'; 4 | import getOwnerDocument from 'dom-helpers/ownerDocument'; 5 | // Caution! One one of the following modules is supposed to be imported. Avoid side effects in them. 6 | import { SSRHydrationDev } from './SSRHydrationDev.js'; 7 | import { SSRHydrationProd } from './SSRHydrationProd.js'; 8 | import * as DOMComponent from './DOMComponent'; 9 | import { 10 | cacheHandleByInstance, 11 | getInternalHandleFromInstance, 12 | } from './DOMComponentTree'; 13 | 14 | let isSvg = null; 15 | 16 | function getSvgContext(isSvg, type) { 17 | return type === 'svg' || (isSvg && type === 'foreignObject' ? false : isSvg); 18 | } 19 | 20 | function getRootSvgContext(rootContainer: Element | Document) { 21 | const type = rootContainer.tagName ? (rootContainer: any).tagName : '#other'; 22 | return getSvgContext(!!(rootContainer: any).ownerSVGElement, type); 23 | } 24 | 25 | function createElement(type, props, rootContainerElement, isSvg): Element { 26 | const ownerDocument = getOwnerDocument(rootContainerElement); 27 | let domElement = isSvg 28 | ? ownerDocument.createElementNS('http://www.w3.org/2000/svg', type) 29 | : ownerDocument.createElement(type); 30 | return domElement; 31 | } 32 | 33 | function getHydrationConfig() { 34 | if (__DEV__) { 35 | return SSRHydrationDev; 36 | } else { 37 | return SSRHydrationProd; 38 | } 39 | } 40 | 41 | const hostConfig: HostConfig< 42 | string, // T: component type 43 | Props, // P: props 44 | Element, // I: component instance 45 | Text, // TI: component text instance 46 | Element, // HI: Hydration Instance 47 | Element | Text, // PI: Public instance 48 | DOMContainer, // C: Container instance 49 | any, // Child container instance 50 | HostContext, // CX: Host context 51 | Array<[string, any]>, // PL: prepare update result 52 | > = { 53 | getRootHostContext(rootContainer): HostContext { 54 | return { isSvg: getRootSvgContext(rootContainer) }; 55 | }, 56 | 57 | getChildHostContext({ isSvg }: HostContext, type: string): HostContext { 58 | return { isSvg: getSvgContext(isSvg, type) }; 59 | }, 60 | 61 | getPublicInstance(instance): * { 62 | return instance; 63 | }, 64 | 65 | appendInitialChild(parentInstance: Element, child: Element | Text): void { 66 | parentInstance.appendChild(child); 67 | }, 68 | 69 | createInstance( 70 | type: string, 71 | props: Props, 72 | rootContainerInstance: DOMContainer, 73 | { isSvg: parentIsSvg }, 74 | internalInstanceHandle, 75 | ): Element { 76 | const instance = createElement( 77 | type, 78 | props, 79 | rootContainerInstance, 80 | parentIsSvg || type === 'svg', // in or entering an svg 81 | ); 82 | cacheHandleByInstance(instance, internalInstanceHandle); 83 | return instance; 84 | }, 85 | 86 | createTextInstance( 87 | text: string, 88 | rootContainerInstance: DOMContainer, 89 | hostContext, 90 | internalInstanceHandle, 91 | ): Text { 92 | const inst = getOwnerDocument(rootContainerInstance).createTextNode(text); 93 | cacheHandleByInstance(inst, internalInstanceHandle); 94 | return inst; 95 | }, 96 | 97 | finalizeInitialChildren( 98 | domElement: Element, 99 | type: string, 100 | props: Props, 101 | rootContainerInstance, 102 | ): boolean { 103 | if (isSvg == null) isSvg = getRootSvgContext(rootContainerInstance); 104 | 105 | DOMComponent.setInitialProps( 106 | domElement, 107 | props, 108 | (isSvg = getSvgContext(isSvg, type)), 109 | ); 110 | return false; 111 | }, 112 | 113 | getPublicInstance(inst: Element | Text): Element | Text { 114 | return inst; 115 | }, 116 | 117 | prepareForCommit() { 118 | // noop 119 | }, 120 | 121 | prepareUpdate( 122 | domElement: Element, 123 | type: string, 124 | oldProps: Props, 125 | newProps: Props, 126 | ): null | Array<[string, any]> { 127 | return DOMComponent.diffProps(domElement, oldProps, newProps) || null; 128 | }, 129 | 130 | resetAfterCommit() { 131 | // noop 132 | }, 133 | 134 | resetTextContent(domElement: Element): void { 135 | domElement.textContent = ''; 136 | }, 137 | 138 | shouldSetTextContent(type: string, props: Props): boolean { 139 | return ( 140 | type === 'textarea' || 141 | typeof props.children === 'string' || 142 | typeof props.children === 'number' || 143 | (typeof props.dangerouslySetInnerHTML === 'object' && 144 | props.dangerouslySetInnerHTML !== null && 145 | typeof props.dangerouslySetInnerHTML.__html === 'string') 146 | ); 147 | }, 148 | 149 | now() { 150 | return typeof performance === 'object' && 151 | typeof performance.now === 'function' 152 | ? performance.now() 153 | : Date.now(); 154 | }, 155 | 156 | useSyncScheduling: true, 157 | scheduleDeferredCallback: window.requestIdleCallback, 158 | cancelDeferredCallback: window.cancelIdleCallback, 159 | shouldDeprioritizeSubtree: (type: string, props: Props) => !!props.hidden, 160 | 161 | // MUTATION 162 | supportsMutation: true, 163 | 164 | commitUpdate( 165 | instance: Element, 166 | preparedUpdateQueue: Array<[string, any]>, 167 | type, 168 | oldProps, 169 | _, 170 | { isSvg }, 171 | ): void { 172 | DOMComponent.updateProps(instance, preparedUpdateQueue, oldProps, isSvg); 173 | }, 174 | commitMount() { 175 | // noop 176 | }, 177 | 178 | commitTextUpdate(textInstance: Text, oldText: string, newText: string) { 179 | textInstance.nodeValue = newText; 180 | }, 181 | 182 | resetTextContent(domElement: Element): void { 183 | domElement.textContent = ''; 184 | }, 185 | 186 | appendChild(parentInstance: Element, child: Element | Text): void { 187 | parentInstance.appendChild(child); 188 | }, 189 | 190 | appendChildToContainer( 191 | parentInstance: DOMContainer, 192 | child: Element | Text, 193 | ): void { 194 | parentInstance.appendChild(child); 195 | }, 196 | insertBefore( 197 | parentInstance: Element, 198 | child: Element | Text, 199 | beforeChild: Element | Text, 200 | ): void { 201 | parentInstance.insertBefore(child, beforeChild); 202 | }, 203 | 204 | insertInContainerBefore( 205 | container: DOMContainer, 206 | child: Element | Text, 207 | beforeChild: Element | Text, 208 | ): void { 209 | container.insertBefore(child, beforeChild); 210 | }, 211 | 212 | removeChild(parentInstance: Element, child: Element | Text): void { 213 | parentInstance.removeChild(child); 214 | }, 215 | removeChildFromContainer( 216 | parentInstance: DOMContainer, 217 | child: Element | Text, 218 | ): void { 219 | parentInstance.removeChild(child); 220 | }, 221 | 222 | // Hydration 223 | ...getHydrationConfig(), 224 | }; 225 | 226 | // $FlowFixMe 227 | const DOMLiteReconciler = Reconciler(hostConfig); 228 | 229 | DOMLiteReconciler.injectIntoDevTools({ 230 | bundleType: __DEV__ ? 1 : 0, 231 | version: '0.1.0', 232 | rendererPackageName: 'react-dom-lite', 233 | findFiberByHostInstance: getInternalHandleFromInstance, 234 | }); 235 | 236 | export { DOMLiteReconciler }; 237 | -------------------------------------------------------------------------------- /src/Root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { OpaqueRoot, Reconciler } from 'react-reconciler'; 4 | 5 | type DOMLiteRenderer = Reconciler; 6 | 7 | class Root { 8 | renderer: DOMLiteRenderer; 9 | internalRoot: OpaqueRoot; 10 | 11 | constructor( 12 | domContainer: DOMContainer, 13 | renderer: DOMLiteRenderer, 14 | isAsync: boolean, 15 | hydrate: boolean, 16 | ) { 17 | this.renderer = renderer; 18 | this.internalRoot = renderer.createContainer( 19 | domContainer, 20 | isAsync, 21 | hydrate, 22 | ); 23 | } 24 | 25 | render(children: ReactNodeList, cb: ?Function) { 26 | this.renderer.updateContainer(children, this.internalRoot, null, cb); 27 | 28 | return this.renderer.getPublicRootInstance(this.internalRoot); 29 | } 30 | 31 | unmount(cb: ?Function) { 32 | this.renderer.updateContainer(null, this.internalRoot, null, cb); 33 | } 34 | } 35 | 36 | export default Root; 37 | -------------------------------------------------------------------------------- /src/SSRHydrationDev.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { OpaqueHandle } from 'react-reconciler'; 3 | import warning from 'warning'; 4 | import * as Events from './events'; 5 | import { isEventRegex } from './DOMConfig'; 6 | import { cacheHandleByInstance } from './DOMComponentTree'; 7 | import { TEXT_NODE, ELEMENT_NODE } from './HTMLNodeType'; 8 | 9 | // HTML parsing normalizes CR and CRLF to LF. 10 | // It also can turn \u0000 into \uFFFD inside attributes. 11 | // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream 12 | // If we have a mismatch, it might be caused by that. 13 | // We will still patch up in this case but not fire the warning. 14 | const NORMALIZE_NEWLINES_REGEX = /\r\n?/g; 15 | const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g; 16 | 17 | let didWarnInvalidHydration; 18 | 19 | function normalizeMarkupForTextOrAttribute(markup: mixed): string { 20 | const markupString = typeof markup === 'string' ? markup : '' + (markup: any); 21 | return markupString 22 | .replace(NORMALIZE_NEWLINES_REGEX, '\n') 23 | .replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, ''); 24 | } 25 | 26 | function warnForTextDifference( 27 | serverText: string, 28 | clientText: string | number, 29 | ) { 30 | if (didWarnInvalidHydration) { 31 | return; 32 | } 33 | const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText); 34 | const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText); 35 | if (normalizedServerText === normalizedClientText) { 36 | return; 37 | } 38 | didWarnInvalidHydration = true; 39 | warning( 40 | false, 41 | 'Text content did not match. Server: "%s" Client: "%s"', 42 | normalizedServerText, 43 | normalizedClientText, 44 | ); 45 | } 46 | 47 | function warnForExtraAttributes(attributeNames: Set) { 48 | if (didWarnInvalidHydration) { 49 | return; 50 | } 51 | didWarnInvalidHydration = true; 52 | const names = []; 53 | attributeNames.forEach(function(name) { 54 | names.push(name); 55 | }); 56 | warning(false, 'Extra attributes from the server: %s', names); 57 | } 58 | 59 | function warnForInvalidEventListener(propKey, listener) { 60 | if (listener === false) { 61 | warning( 62 | false, 63 | 'Expected `%s` listener to be a function, instead got `false`.\n\n' + 64 | 'If you used to conditionally omit it with %s={condition && value}, ' + 65 | 'pass %s={condition ? value : undefined} instead.', 66 | propKey, 67 | propKey, 68 | propKey, 69 | ); 70 | } else { 71 | warning( 72 | false, 73 | 'Expected `%s` listener to be a function, instead got a value of `%s` type.', 74 | propKey, 75 | typeof listener, 76 | ); 77 | } 78 | } 79 | 80 | function warnForInsertedHydratedText( 81 | parentNode: Element | Document, 82 | text: string, 83 | ) { 84 | if (text === '') { 85 | // Refer comment at https://github.com/facebook/react/blob/73fa26a88b68bca77fb234fc405649d0f33a3815/packages/react-dom/src/client/ReactDOMFiberComponent.js#L1141 86 | return; 87 | } 88 | if (didWarnInvalidHydration) { 89 | return; 90 | } 91 | didWarnInvalidHydration = true; 92 | warning( 93 | false, 94 | 'Expected server HTML to contain a matching text node for "%s" in <%s>.', 95 | text, 96 | parentNode.nodeName.toLowerCase(), 97 | ); 98 | } 99 | 100 | function warnForInsertedHydratedElement( 101 | parentNode: Element | Document, 102 | tag: string, 103 | ) { 104 | if (didWarnInvalidHydration) { 105 | return; 106 | } 107 | didWarnInvalidHydration = true; 108 | warning( 109 | false, 110 | 'Expected server HTML to contain a matching <%s> in <%s>.', 111 | tag, 112 | parentNode.nodeName.toLowerCase(), 113 | ); 114 | } 115 | 116 | function warnForUnmatchedText(textNode: Text, text: string) { 117 | warnForTextDifference(textNode.nodeValue, text); 118 | } 119 | 120 | function warnForDeletedHydratableElement( 121 | parentNode: Element | Document, 122 | child: Element, 123 | ) { 124 | if (didWarnInvalidHydration) { 125 | return; 126 | } 127 | didWarnInvalidHydration = true; 128 | warning( 129 | false, 130 | 'Did not expect server HTML to contain a <%s> in <%s>.', 131 | child.nodeName.toLowerCase(), 132 | parentNode.nodeName.toLowerCase(), 133 | ); 134 | } 135 | 136 | function warnForDeletedHydratableText( 137 | parentNode: Element | Document, 138 | child: Text, 139 | ) { 140 | if (didWarnInvalidHydration) { 141 | return; 142 | } 143 | didWarnInvalidHydration = true; 144 | warning( 145 | false, 146 | 'Did not expect server HTML to contain the text node "%s" in <%s>.', 147 | child.nodeValue, 148 | parentNode.nodeName.toLowerCase(), 149 | ); 150 | } 151 | 152 | function diffHydratedProperties( 153 | domElement: Element, 154 | tag: string, 155 | rawProps: Object, 156 | // parentNamespace: string, 157 | // rootContainerElement: Element | Document, 158 | ): null | Array<[string, any]> { 159 | // Track extra attributes so that we can warn later 160 | let extraAttributeNames: Set = new Set(); 161 | const attributes = domElement.attributes; 162 | for (let i = 0; i < attributes.length; i++) { 163 | const name = attributes[i].name.toLowerCase(); 164 | switch (name) { 165 | // Built-in SSR attribute is whitelisted 166 | case 'data-reactroot': 167 | break; 168 | // Controlled attributes are not validated 169 | // TODO: Only ignore them on controlled tags. 170 | case 'value': 171 | break; 172 | case 'checked': 173 | break; 174 | case 'selected': 175 | break; 176 | default: 177 | // Intentionally use the original name. 178 | // See discussion in https://github.com/facebook/react/pull/10676. 179 | extraAttributeNames.add(attributes[i].name); 180 | } 181 | } 182 | 183 | let updatePayload = null; 184 | 185 | for (const propKey in rawProps) { 186 | if (!rawProps.hasOwnProperty(propKey)) { 187 | continue; 188 | } 189 | const nextProp = rawProps[propKey]; 190 | let match; 191 | if (propKey === 'children') { 192 | // Explanation as seen upstream 193 | // For text content children we compare against textContent. This 194 | // might match additional HTML that is hidden when we read it using 195 | // textContent. E.g. "foo" will match "foo" but that still 196 | // satisfies our requirement. Our requirement is not to produce perfect 197 | // HTML and attributes. Ideally we should preserve structure but it's 198 | // ok not to if the visible content is still enough to indicate what 199 | // even listeners these nodes might be wired up to. 200 | // TODO: Warn if there is more than a single textNode as a child. 201 | // TODO: Should we use domElement.firstChild.nodeValue to compare? 202 | if (typeof nextProp === 'string') { 203 | if (domElement.textContent !== nextProp) { 204 | warnForTextDifference(domElement.textContent, nextProp); 205 | updatePayload = [['children', nextProp]]; 206 | } 207 | } else if (typeof nextProp === 'number') { 208 | if (domElement.textContent !== '' + nextProp) { 209 | warnForTextDifference(domElement.textContent, nextProp); 210 | updatePayload = [['children', '' + nextProp]]; 211 | } 212 | } 213 | } else if ((match = propKey.match(isEventRegex))) { 214 | if (nextProp != null) { 215 | if (typeof nextProp !== 'function') { 216 | warnForInvalidEventListener(propKey, nextProp); 217 | } 218 | Events.listenTo(((domElement: any): Element), match[1], nextProp); // Attention! 219 | } 220 | } 221 | // TODO shouldIgnoreAttribute && shouldRemoveAttribute 222 | } 223 | 224 | // $FlowFixMe - Should be inferred as not undefined. 225 | if (extraAttributeNames.size > 0) { 226 | // $FlowFixMe - Should be inferred as not undefined. 227 | warnForExtraAttributes(extraAttributeNames); 228 | } 229 | 230 | return updatePayload; 231 | } 232 | 233 | function diffHydratedText(textNode: Text, text: string): boolean { 234 | const isDifferent = textNode.nodeValue !== text; 235 | return isDifferent; 236 | } 237 | 238 | export const SSRHydrationDev = { 239 | canHydrateInstance(instance: Element, type: string): null | Element { 240 | if ( 241 | instance.nodeType !== ELEMENT_NODE || 242 | type.toLowerCase() !== instance.nodeName.toLowerCase() 243 | ) { 244 | return null; 245 | } 246 | return instance; 247 | }, 248 | 249 | canHydrateTextInstance(instance: Element, text: string): null | Text { 250 | if (text === '' || instance.nodeType !== TEXT_NODE) { 251 | // Empty strings are not parsed by HTML so there won't be a correct match here. 252 | return null; 253 | } 254 | return ((instance: any): Text); 255 | }, 256 | 257 | getNextHydratableSibling(instance: Element | Text): null | Element { 258 | let node = instance.nextSibling; 259 | // Skip non-hydratable nodes. 260 | while ( 261 | node && 262 | node.nodeType !== ELEMENT_NODE && 263 | node.nodeType !== TEXT_NODE 264 | ) { 265 | node = node.nextSibling; 266 | } 267 | return (node: any); 268 | }, 269 | 270 | getFirstHydratableChild( 271 | parentInstance: DOMContainer | Element, 272 | ): null | Element { 273 | let next = parentInstance.firstChild; 274 | // Skip non-hydratable nodes. 275 | while ( 276 | next && 277 | next.nodeType !== ELEMENT_NODE && 278 | next.nodeType !== TEXT_NODE 279 | ) { 280 | next = next.nextSibling; 281 | } 282 | return ((next: any): Element); 283 | }, 284 | 285 | hydrateInstance( 286 | instance: Element, 287 | type: string, 288 | props: Props, 289 | rootContainerInstance: DOMContainer, 290 | hostContext: HostContext, 291 | internalInstanceHandle: OpaqueHandle, 292 | ): null | Array<[string, any]> { 293 | cacheHandleByInstance(instance, internalInstanceHandle); 294 | return diffHydratedProperties( 295 | instance, 296 | type, 297 | props, 298 | /* hostContext, */ 299 | /* rootContainerInstance,*/ 300 | ); 301 | }, 302 | 303 | hydrateTextInstance( 304 | textInstance: Text, 305 | text: string, 306 | internalInstanceHandle: OpaqueHandle, 307 | ): boolean { 308 | cacheHandleByInstance( 309 | ((textInstance: any): Element), 310 | internalInstanceHandle, 311 | ); 312 | return diffHydratedText(textInstance, text); 313 | }, 314 | 315 | didNotMatchHydratedContainerTextInstance( 316 | parentContainer: DOMContainer, 317 | textInstance: Text, 318 | text: string, 319 | ) { 320 | warnForUnmatchedText(textInstance, text); 321 | }, 322 | 323 | didNotMatchHydratedTextInstance( 324 | parentType: string, 325 | parentProps: Props, 326 | parentInstance: Element, 327 | textInstance: Text, 328 | text: string, 329 | ) { 330 | warnForUnmatchedText(textInstance, text); 331 | }, 332 | 333 | didNotHydrateContainerInstance( 334 | parentContainer: DOMContainer, 335 | instance: Element | Text, 336 | ) { 337 | if (instance.nodeType === 1) { 338 | warnForDeletedHydratableElement(parentContainer, (instance: any)); 339 | } else { 340 | warnForDeletedHydratableText(parentContainer, (instance: any)); 341 | } 342 | }, 343 | 344 | didNotHydrateInstance( 345 | parentType: string, 346 | parentProps: Props, 347 | parentInstance: Element, 348 | instance: Element | Text, 349 | ) { 350 | if (instance.nodeType === 1) { 351 | warnForDeletedHydratableElement(parentInstance, (instance: any)); 352 | } else { 353 | warnForDeletedHydratableText(parentInstance, (instance: any)); 354 | } 355 | }, 356 | 357 | didNotFindHydratableContainerInstance( 358 | parentContainer: DOMContainer, 359 | type: string, 360 | ) { 361 | warnForInsertedHydratedElement(parentContainer, type); 362 | }, 363 | 364 | didNotFindHydratableContainerTextInstance( 365 | parentContainer: DOMContainer, 366 | text: string, 367 | ) { 368 | warnForInsertedHydratedText(parentContainer, text); 369 | }, 370 | 371 | didNotFindHydratableInstance( 372 | parentType: string, 373 | parentProps: Props, 374 | parentInstance: Element, 375 | type: string, 376 | ) { 377 | warnForInsertedHydratedElement(parentInstance, type); 378 | }, 379 | 380 | didNotFindHydratableTextInstance( 381 | parentType: string, 382 | parentProps: Props, 383 | parentInstance: Element, 384 | text: string, 385 | ) { 386 | warnForInsertedHydratedText(parentInstance, text); 387 | }, 388 | }; 389 | -------------------------------------------------------------------------------- /src/SSRHydrationProd.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { OpaqueHandle } from 'react-reconciler'; 3 | import * as Events from './events'; 4 | import { isEventRegex } from './DOMConfig'; 5 | import { cacheHandleByInstance } from './DOMComponentTree'; 6 | import { TEXT_NODE, ELEMENT_NODE } from './HTMLNodeType'; 7 | 8 | function diffHydratedProperties( 9 | domElement: Element, 10 | tag: string, 11 | rawProps: Object, 12 | // parentNamespace: string, 13 | // rootContainerElement: Element | Document, 14 | ): null | Array<[string, any]> { 15 | let updatePayload = null; 16 | 17 | for (const propKey in rawProps) { 18 | if (!rawProps.hasOwnProperty(propKey)) { 19 | continue; 20 | } 21 | const nextProp = rawProps[propKey]; 22 | let match; 23 | if (propKey === 'children') { 24 | // Explanation as seen upstream 25 | // For text content children we compare against textContent. This 26 | // might match additional HTML that is hidden when we read it using 27 | // textContent. E.g. "foo" will match "foo" but that still 28 | // satisfies our requirement. Our requirement is not to produce perfect 29 | // HTML and attributes. Ideally we should preserve structure but it's 30 | // ok not to if the visible content is still enough to indicate what 31 | // even listeners these nodes might be wired up to. 32 | if (typeof nextProp === 'string') { 33 | if (domElement.textContent !== nextProp) { 34 | updatePayload = [['children', nextProp]]; 35 | } 36 | } else if (typeof nextProp === 'number') { 37 | if (domElement.textContent !== '' + nextProp) { 38 | updatePayload = [['children', '' + nextProp]]; 39 | } 40 | } 41 | } else if ((match = propKey.match(isEventRegex))) { 42 | if (nextProp != null) { 43 | Events.listenTo(((domElement: any): Element), match[1], nextProp); // Attention! 44 | } 45 | } 46 | } 47 | return updatePayload; 48 | } 49 | 50 | function diffHydratedText(textNode: Text, text: string): boolean { 51 | const isDifferent = textNode.nodeValue !== text; 52 | return isDifferent; 53 | } 54 | 55 | export const SSRHydrationProd = { 56 | canHydrateInstance(instance: Element, type: string): null | Element { 57 | if ( 58 | instance.nodeType !== ELEMENT_NODE || 59 | type.toLowerCase() !== instance.nodeName.toLowerCase() 60 | ) { 61 | return null; 62 | } 63 | return instance; 64 | }, 65 | 66 | canHydrateTextInstance(instance: Element, text: string): null | Text { 67 | if (text === '' || instance.nodeType !== TEXT_NODE) { 68 | // Empty strings are not parsed by HTML so there won't be a correct match here. 69 | return null; 70 | } 71 | return ((instance: any): Text); 72 | }, 73 | 74 | getNextHydratableSibling(instance: Element | Text): null | Element { 75 | let node = instance.nextSibling; 76 | // Skip non-hydratable nodes. 77 | while ( 78 | node && 79 | node.nodeType !== ELEMENT_NODE && 80 | node.nodeType !== TEXT_NODE 81 | ) { 82 | node = node.nextSibling; 83 | } 84 | return (node: any); 85 | }, 86 | 87 | getFirstHydratableChild( 88 | parentInstance: DOMContainer | Element, 89 | ): null | Element { 90 | let next = parentInstance.firstChild; 91 | // Skip non-hydratable nodes. 92 | while ( 93 | next && 94 | next.nodeType !== ELEMENT_NODE && 95 | next.nodeType !== TEXT_NODE 96 | ) { 97 | next = next.nextSibling; 98 | } 99 | return ((next: any): Element); 100 | }, 101 | 102 | hydrateInstance( 103 | instance: Element, 104 | type: string, 105 | props: Props, 106 | rootContainerInstance: DOMContainer, 107 | hostContext: HostContext, 108 | internalInstanceHandle: OpaqueHandle, 109 | ): null | Array<[string, any]> { 110 | cacheHandleByInstance(instance, internalInstanceHandle); 111 | return diffHydratedProperties( 112 | instance, 113 | type, 114 | props, 115 | /* hostContext, */ 116 | /* rootContainerInstance,*/ 117 | ); 118 | }, 119 | 120 | hydrateTextInstance( 121 | textInstance: Text, 122 | text: string, 123 | internalInstanceHandle: OpaqueHandle, 124 | ): boolean { 125 | cacheHandleByInstance( 126 | ((textInstance: any): Element), 127 | internalInstanceHandle, 128 | ); 129 | return diffHydratedText(textInstance, text); 130 | }, 131 | 132 | didNotMatchHydratedContainerTextInstance() {}, 133 | // parentContainer: DOMContainer, 134 | // textInstance: Text, 135 | // text: string, 136 | 137 | didNotMatchHydratedTextInstance() {}, 138 | // parentType: string, 139 | // parentProps: Props, 140 | // parentInstance: Element, 141 | // textInstance: Text, 142 | // text: string, 143 | 144 | didNotHydrateContainerInstance() {}, 145 | // parentContainer: DOMContainer, 146 | // instance: Element | Text, 147 | 148 | didNotHydrateInstance() {}, 149 | // parentType: string, 150 | // parentProps: Props, 151 | // parentInstance: Element, 152 | // instance: Element | Text, 153 | 154 | didNotFindHydratableContainerInstance() {}, 155 | // parentContainer: DOMContainer, 156 | // type: string, 157 | 158 | didNotFindHydratableContainerTextInstance() {}, 159 | // parentContainer: DOMContainer, 160 | // text: string, 161 | 162 | didNotFindHydratableInstance() {}, 163 | // parentType: string, 164 | // parentProps: Props, 165 | // parentInstance: Element, 166 | // type: string, 167 | 168 | didNotFindHydratableTextInstance() {}, 169 | // parentType: string, 170 | // parentProps: Props, 171 | // parentInstance: Element, 172 | // text: string, 173 | }; 174 | -------------------------------------------------------------------------------- /src/__tests__/DOMProperties.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom-lite'; 3 | 4 | describe('DOMProperties', () => { 5 | describe('setValueOnElement', () => { 6 | it('should set values as properties by default', () => { 7 | const container = document.createElement('div'); 8 | render(
, container); 9 | expect(container.firstChild.title).toBe('Tip!'); 10 | }); 11 | 12 | it('should set values as attributes if necessary', () => { 13 | const container = document.createElement('div'); 14 | render(
, container); 15 | expect(container.firstChild.getAttribute('role')).toBe('#'); 16 | expect(container.firstChild.role).toBeUndefined(); 17 | }); 18 | 19 | it('should set values as attributes for specific props', () => { 20 | const container = document.createElement('div'); 21 | render(, container); 22 | expect(container.firstChild.getAttribute('type')).toBe('button'); 23 | expect(container.firstChild.readOnly).toBe(true); 24 | }); 25 | 26 | it('should set boolean string attributes', () => { 27 | const container = document.createElement('div'); 28 | render(
, container); 29 | expect(container.firstChild.getAttribute('spellcheck')).toBe('true'); 30 | render(
, container); 31 | expect(container.firstChild.getAttribute('spellcheck')).toBe('false'); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/SVG.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import React from 'react'; 12 | import { render } from 'react-dom-lite'; 13 | 14 | describe('SVG', () => { 15 | it('creates elements with SVG namespace inside SVG tag during mount', () => { 16 | const node = document.createElement('div'); 17 | let div, 18 | div2, 19 | div3, 20 | foreignObject, 21 | foreignObject2, 22 | g, 23 | image, 24 | image2, 25 | image3, 26 | p, 27 | svg, 28 | svg2, 29 | svg3, 30 | svg4; 31 | 32 | render( 33 |
34 | (svg = el)} viewBox="200 200"> 35 | (g = el)} strokeWidth="5"> 36 | (svg2 = el)}> 37 | (foreignObject = el)}> 38 | (svg3 = el)}> 39 | (svg4 = el)} /> 40 | (image = el)} 42 | xlinkHref="http://i.imgur.com/w7GCRPb.png" 43 | /> 44 | 45 |
(div = el)} /> 46 | 47 | 48 | (image2 = el)} 50 | xlinkHref="http://i.imgur.com/w7GCRPb.png" 51 | /> 52 | (foreignObject2 = el)}> 53 |
(div2 = el)} /> 54 | 55 | 56 | 57 |

(p = el)}> 58 | 59 | (image3 = el)} 61 | xlinkHref="http://i.imgur.com/w7GCRPb.png" 62 | /> 63 | 64 |

65 |
(div3 = el)} /> 66 |
, 67 | node, 68 | ); 69 | [svg, svg2, svg3, svg4].forEach(el => { 70 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 71 | // SVG tagName is case sensitive. 72 | expect(el.tagName).toBe('svg'); 73 | }); 74 | expect(svg.getAttribute('viewBox')).toBe('200 200'); 75 | 76 | expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); 77 | expect(g.tagName).toBe('g'); 78 | expect(g.getAttribute('stroke-width')).toBe('5'); 79 | expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); 80 | // DOM tagName is capitalized by browsers. 81 | expect(p.tagName).toBe('P'); 82 | [image, image2, image3].forEach(el => { 83 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 84 | expect(el.tagName).toBe('image'); 85 | expect(el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe( 86 | 'http://i.imgur.com/w7GCRPb.png', 87 | ); 88 | }); 89 | [foreignObject, foreignObject2].forEach(el => { 90 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 91 | expect(el.tagName).toBe('foreignObject'); 92 | }); 93 | [div, div2, div3].forEach(el => { 94 | expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); 95 | expect(el.tagName).toBe('DIV'); 96 | }); 97 | }); 98 | 99 | it('creates elements with SVG namespace inside SVG tag during update', () => { 100 | let inst, 101 | div, 102 | div2, 103 | foreignObject, 104 | foreignObject2, 105 | g, 106 | image, 107 | image2, 108 | svg, 109 | svg2, 110 | svg3, 111 | svg4; 112 | 113 | class App extends React.Component { 114 | state = { step: 0 }; 115 | render() { 116 | inst = this; 117 | const { step } = this.state; 118 | if (step === 0) { 119 | return null; 120 | } 121 | return ( 122 | (g = el)} strokeWidth="5"> 123 | (svg2 = el)}> 124 | (foreignObject = el)}> 125 | (svg3 = el)}> 126 | (svg4 = el)} /> 127 | (image = el)} 129 | xlinkHref="http://i.imgur.com/w7GCRPb.png" 130 | /> 131 | 132 |
(div = el)} /> 133 | 134 | 135 | (image2 = el)} 137 | xlinkHref="http://i.imgur.com/w7GCRPb.png" 138 | /> 139 | (foreignObject2 = el)}> 140 |
(div2 = el)} /> 141 | 142 | 143 | ); 144 | } 145 | } 146 | 147 | const node = document.createElement('div'); 148 | render( 149 | (svg = el)}> 150 | 151 | , 152 | node, 153 | ); 154 | inst.setState({ step: 1 }); 155 | 156 | [svg, svg2, svg3, svg4].forEach(el => { 157 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 158 | // SVG tagName is case sensitive. 159 | expect(el.tagName).toBe('svg'); 160 | }); 161 | expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); 162 | expect(g.tagName).toBe('g'); 163 | expect(g.getAttribute('stroke-width')).toBe('5'); 164 | [image, image2].forEach(el => { 165 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 166 | expect(el.tagName).toBe('image'); 167 | expect(el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe( 168 | 'http://i.imgur.com/w7GCRPb.png', 169 | ); 170 | }); 171 | [foreignObject, foreignObject2].forEach(el => { 172 | expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); 173 | expect(el.tagName).toBe('foreignObject'); 174 | }); 175 | [div, div2].forEach(el => { 176 | expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); 177 | // DOM tagName is capitalized by browsers. 178 | expect(el.tagName).toBe('DIV'); 179 | }); 180 | }); 181 | 182 | it('can render SVG into a non-React SVG tree', () => { 183 | const outerSVGRoot = document.createElementNS( 184 | 'http://www.w3.org/2000/svg', 185 | 'svg', 186 | ); 187 | const container = document.createElementNS( 188 | 'http://www.w3.org/2000/svg', 189 | 'g', 190 | ); 191 | outerSVGRoot.appendChild(container); 192 | let image; 193 | render( (image = el)} />, container); 194 | expect(image.namespaceURI).toBe('http://www.w3.org/2000/svg'); 195 | expect(image.tagName).toBe('image'); 196 | }); 197 | 198 | it('can render HTML into a foreignObject in non-React SVG tree', () => { 199 | const outerSVGRoot = document.createElementNS( 200 | 'http://www.w3.org/2000/svg', 201 | 'svg', 202 | ); 203 | const container = document.createElementNS( 204 | 'http://www.w3.org/2000/svg', 205 | 'foreignObject', 206 | ); 207 | outerSVGRoot.appendChild(container); 208 | let div; 209 | render(
(div = el)} />, container); 210 | expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); 211 | expect(div.tagName).toBe('DIV'); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/__tests__/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | import { render } from 'react-dom-lite'; 10 | 11 | describe('Context', () => { 12 | it('Context must be available in the consumer', () => { 13 | let actual = 0; 14 | const Context = React.createContext(); 15 | 16 | function Consumer() { 17 | return ( 18 | 19 | {value => { 20 | actual = value; 21 | return ; 22 | }} 23 | 24 | ); 25 | } 26 | 27 | class MyNode extends React.Component { 28 | render() { 29 | return ( 30 |
31 | Noise 32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | const container = document.createElement('div'); 39 | render( 40 | 41 | 42 | , 43 | container, 44 | function() { 45 | expect(actual).toBe(5); 46 | }, 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/findDOMNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | 10 | import { render, unmountComponentAtNode, findDOMNode } from 'react-dom-lite'; 11 | import * as TestUtils from 'react-dom-lite/test-utils'; 12 | 13 | describe('findDOMNode', () => { 14 | it('findDOMNode should return null if passed null', () => { 15 | expect(findDOMNode(null)).toBe(null); 16 | }); 17 | 18 | it('findDOMNode should find dom element', () => { 19 | class MyNode extends React.Component { 20 | render() { 21 | return ( 22 |
23 | Noise 24 |
25 | ); 26 | } 27 | } 28 | 29 | const myNode = TestUtils.renderIntoDetachedNode(); 30 | const myDiv = findDOMNode(myNode); 31 | const mySameDiv = findDOMNode(myDiv); 32 | expect(myDiv.tagName).toBe('DIV'); 33 | expect(mySameDiv).toBe(myDiv); 34 | }); 35 | 36 | it('findDOMNode should find dom element after an update from null', () => { 37 | function Bar({ flag }) { 38 | if (flag) { 39 | return A; 40 | } 41 | return null; 42 | } 43 | class MyNode extends React.Component { 44 | render() { 45 | return ; 46 | } 47 | } 48 | 49 | const container = document.createElement('div'); 50 | 51 | const myNodeA = render(, container); 52 | const a = findDOMNode(myNodeA); 53 | expect(a).toBe(null); 54 | 55 | const myNodeB = render(, container); 56 | expect(myNodeA === myNodeB).toBe(true); 57 | 58 | const b = findDOMNode(myNodeB); 59 | expect(b.tagName).toBe('SPAN'); 60 | }); 61 | 62 | it('findDOMNode should reject random objects', () => { 63 | expect(function() { 64 | findDOMNode({ foo: 'bar' }); 65 | }).toThrowError('Argument appears to not be a ReactComponent. Keys: foo'); 66 | }); 67 | 68 | it('findDOMNode should reject unmounted objects with render func', () => { 69 | class Foo extends React.Component { 70 | render() { 71 | return
; 72 | } 73 | } 74 | 75 | const container = document.createElement('div'); 76 | const inst = render(, container); 77 | unmountComponentAtNode(container); 78 | 79 | expect(() => findDOMNode(inst)).toThrowError( 80 | 'Unable to find node on an unmounted component.', 81 | ); 82 | }); 83 | 84 | it('findDOMNode should not throw an error when called within a component that is not mounted', () => { 85 | class Bar extends React.Component { 86 | componentWillMount() { 87 | expect(findDOMNode(this)).toBeNull(); 88 | } 89 | 90 | render() { 91 | return
; 92 | } 93 | } 94 | 95 | expect(() => TestUtils.renderIntoDetachedNode()).not.toThrow(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/__tests__/test-utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from 'react-dom-lite'; 4 | import * as TestUtils from 'react-dom-lite/test-utils'; 5 | 6 | describe('test-utils', () => { 7 | let container; 8 | 9 | beforeEach(() => { 10 | container = document.createElement('div'); 11 | document.body.appendChild(container); 12 | }); 13 | 14 | afterEach(() => { 15 | document.body.innerHTML = ''; 16 | }); 17 | 18 | it('should dispatch synthetic events directly', () => { 19 | const captureSpy = jest.fn(e => { 20 | expect(bubbleSpy).not.toBeCalled(); 21 | expect(e.eventPhase).toEqual(e.CAPTURING_PHASE); 22 | }); 23 | const bubbleSpy = jest.fn(e => 24 | expect(e.eventPhase).toEqual(e.BUBBLING_PHASE), 25 | ); 26 | const node = render( 27 |
28 |
, 30 | container, 31 | ); 32 | 33 | TestUtils.Simulate.click(node.firstChild); 34 | 35 | expect(captureSpy).toBeCalled(); 36 | expect(bubbleSpy).toBeCalled(); 37 | }); 38 | 39 | it('should skip some mapping from native to synthetic', () => { 40 | const changeSpy = jest.fn(); 41 | const inputSpy = jest.fn(); 42 | 43 | const node = render( 44 |
45 | 46 |
, 47 | container, 48 | ); 49 | 50 | TestUtils.Simulate.change(node.firstChild); 51 | 52 | expect(changeSpy).toBeCalled(); 53 | expect(inputSpy).not.toBeCalled(); 54 | }); 55 | 56 | it('should mimic dispatching a native event', () => { 57 | const changeSpy = jest.fn(); 58 | const inputSpy = jest.fn(); 59 | 60 | const node = render( 61 |
62 | 63 |
, 64 | container, 65 | ); 66 | 67 | TestUtils.SimulateNative.change(node.firstChild); 68 | expect(changeSpy).not.toBeCalled(); 69 | 70 | TestUtils.SimulateNative.input(node.firstChild); 71 | expect(changeSpy).toBeCalled(); 72 | expect(inputSpy).toBeCalled(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/__tests__/unmountComponentAtNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | import { 10 | render, 11 | unmountComponentAtNode, 12 | unstable_batchedUpdates as batchedUpdates, 13 | } from 'react-dom-lite'; 14 | 15 | describe('unmountComponentAtNode', () => { 16 | it('throws when given a non-node', () => { 17 | const nodeArray = document.getElementsByTagName('div'); 18 | expect(function() { 19 | unmountComponentAtNode(nodeArray); 20 | }).toThrowError( 21 | 'unmountComponentAtNode(...): Target container is not a DOM element.', 22 | ); 23 | }); 24 | 25 | it('returns false on non-React containers', () => { 26 | const d = document.createElement('div'); 27 | d.innerHTML = 'hellooo'; 28 | expect(unmountComponentAtNode(d)).toBe(false); 29 | expect(d.textContent).toBe('hellooo'); 30 | }); 31 | 32 | it('returns true on React containers', () => { 33 | const d = document.createElement('div'); 34 | render(hellooo, d); 35 | expect(d.textContent).toBe('hellooo'); 36 | expect(unmountComponentAtNode(d)).toBe(true); 37 | expect(d.textContent).toBe(''); 38 | }); 39 | 40 | it('should render different components in same root', () => { 41 | const container = document.createElement('container'); 42 | document.body.appendChild(container); 43 | 44 | render(
, container); 45 | expect(container.firstChild.nodeName).toBe('DIV'); 46 | 47 | render(, container); 48 | expect(container.firstChild.nodeName).toBe('SPAN'); 49 | }); 50 | 51 | it('should unmount and remount if the key changes', () => { 52 | const container = document.createElement('container'); 53 | 54 | const mockMount = jest.fn(); 55 | const mockUnmount = jest.fn(); 56 | 57 | class Component extends React.Component { 58 | componentDidMount = mockMount; 59 | componentWillUnmount = mockUnmount; 60 | render() { 61 | return {this.props.text}; 62 | } 63 | } 64 | 65 | expect(mockMount.mock.calls.length).toBe(0); 66 | expect(mockUnmount.mock.calls.length).toBe(0); 67 | 68 | render(, container); 69 | expect(container.firstChild.innerHTML).toBe('orange'); 70 | expect(mockMount.mock.calls.length).toBe(1); 71 | expect(mockUnmount.mock.calls.length).toBe(0); 72 | 73 | // If we change the key, the component is unmounted and remounted 74 | render(, container); 75 | expect(container.firstChild.innerHTML).toBe('green'); 76 | expect(mockMount.mock.calls.length).toBe(2); 77 | expect(mockUnmount.mock.calls.length).toBe(1); 78 | 79 | // But if we don't change the key, the component instance is reused 80 | render(, container); 81 | expect(container.firstChild.innerHTML).toBe('blue'); 82 | expect(mockMount.mock.calls.length).toBe(2); 83 | expect(mockUnmount.mock.calls.length).toBe(1); 84 | }); 85 | 86 | it('should reuse markup if rendering to the same target twice', () => { 87 | const container = document.createElement('container'); 88 | const instance1 = render(
, container); 89 | const instance2 = render(
, container); 90 | 91 | expect(instance1 === instance2).toBe(true); 92 | }); 93 | 94 | it('initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch', () => { 95 | const container1 = document.createElement('div'); 96 | const container2 = document.createElement('div'); 97 | 98 | class Foo extends React.Component { 99 | state = { active: false }; 100 | componentDidMount() { 101 | this.setState({ active: true }); 102 | } 103 | render() { 104 | return ( 105 |
{this.props.children + (this.state.active ? '!' : '')}
106 | ); 107 | } 108 | } 109 | 110 | render(
1
, container1); 111 | 112 | batchedUpdates(() => { 113 | // Update. Does not flush yet. 114 | render(
2
, container1); 115 | expect(container1.textContent).toEqual('1'); 116 | 117 | // Initial mount on another root. Should flush immediately. 118 | render(a, container2); 119 | // The update did not flush yet. 120 | expect(container1.textContent).toEqual('1'); 121 | // The initial mount flushed, but not the update scheduled in cDU. 122 | expect(container2.textContent).toEqual('a'); 123 | }); 124 | // All updates have flushed. 125 | expect(container1.textContent).toEqual('2'); 126 | expect(container2.textContent).toEqual('a!'); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/events/SyntheticEvent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const returnsFalse = () => false; 4 | const returnsTrue = () => true; 5 | 6 | /** 7 | * Called SyntheticEvent for lack of a better name. This is really a 8 | * Shim object for compatibility with react components expecting the 9 | * Synthetic even system to exist. 10 | */ 11 | export default class SyntheticEvent { 12 | nativeEvent: Event; 13 | type: string; 14 | 15 | isPersistent: () => boolean; 16 | isPropagationStopped: () => boolean; 17 | isDefaultPrevented: () => boolean; 18 | defaultPrevented: boolean; 19 | 20 | constructor(event: Event, type?: string) { 21 | this.nativeEvent = event; 22 | this.type = type || event.type; 23 | 24 | for (let key in event) 25 | if (!(key in this)) { 26 | // $FlowFixMe 27 | this[key] = (event: any)[key]; // TODO maybe normalize `event.key` 28 | } 29 | 30 | this.isPersistent = returnsFalse; 31 | this.isPropagationStopped = returnsFalse; 32 | this.isDefaultPrevented = event.defaultPrevented 33 | ? returnsTrue 34 | : returnsFalse; 35 | } 36 | 37 | persist() { 38 | this.isPersistent = returnsTrue; 39 | } 40 | stopPropagation() { 41 | this.nativeEvent.stopPropagation(); 42 | this.isPropagationStopped = returnsTrue; 43 | } 44 | preventDefault() { 45 | this.defaultPrevented = true; 46 | this.isDefaultPrevented = returnsTrue; 47 | this.nativeEvent.preventDefault(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/events/__tests__/events.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom-lite'; 3 | 4 | describe('events', () => { 5 | let container; 6 | 7 | beforeEach(() => { 8 | container = document.createElement('div'); 9 | document.body.appendChild(container); 10 | }); 11 | 12 | afterEach(() => { 13 | document.body.innerHTML = ''; 14 | }); 15 | 16 | it('should listen to events', () => { 17 | const captureSpy = jest.fn(e => { 18 | expect(bubbleSpy).not.toBeCalled(); 19 | expect(e.eventPhase).toEqual(e.CAPTURING_PHASE); 20 | }); 21 | const bubbleSpy = jest.fn(e => 22 | expect(e.eventPhase).toEqual(e.BUBBLING_PHASE), 23 | ); 24 | const node = render( 25 |
26 |
, 28 | container, 29 | ); 30 | 31 | node.firstChild.click(); 32 | node.firstChild.focus(); 33 | 34 | expect(captureSpy).toBeCalled(); 35 | expect(bubbleSpy).toBeCalled(); 36 | }); 37 | 38 | test('onFocus and onBlur should bubble', () => { 39 | const focusSpy = jest.fn(); 40 | const blurSpy = jest.fn(); 41 | const node = render( 42 |
43 | 44 |
, 45 | container, 46 | ); 47 | 48 | node.firstChild.focus(); 49 | node.firstChild.blur(); 50 | 51 | expect(focusSpy).toBeCalled(); 52 | expect(blurSpy).toBeCalled(); 53 | }); 54 | 55 | it('should stop propagation', () => { 56 | const btnSpy = jest.fn(e => { 57 | e.stopPropagation(); 58 | expect(e.isPropagationStopped()).toEqual(true); 59 | }); 60 | const spy = jest.fn(); 61 | 62 | const node = render( 63 |
64 |
, 66 | container, 67 | ); 68 | 69 | node.firstChild.click(); 70 | 71 | expect(btnSpy).toBeCalled(); 72 | expect(spy).not.toBeCalled(); 73 | }); 74 | 75 | it('should have synthetic event api', () => { 76 | let event; 77 | const node = render( 78 |
, 106 | container, 107 | ); 108 | 109 | node.firstChild.click(); 110 | 111 | expect(btnSpy).toBeCalled(); 112 | expect(spy).toBeCalled(); 113 | }); 114 | 115 | describe('Alternate types', () => { 116 | it('should use original name in event', () => { 117 | const spy = jest.fn(e => { 118 | expect(e.type).toEqual('change'); 119 | expect(e.nativeEvent.type).toEqual('input'); 120 | }); 121 | const node = render(, container); 122 | 123 | node.dispatchEvent(new Event('input', {})); 124 | 125 | expect(spy).toBeCalled(); 126 | }); 127 | 128 | it('should call both handlers', () => { 129 | const changeSpy = jest.fn(); 130 | const inputSpy = jest.fn(); 131 | 132 | const node = render( 133 | , 134 | 135 | container, 136 | ); 137 | 138 | node.dispatchEvent(new Event('input', {})); 139 | 140 | expect(changeSpy).toBeCalled(); 141 | expect(inputSpy).toBeCalled(); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/events/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { collectAncestors } from '../DOMComponentTree'; 3 | import SyntheticEvent from './SyntheticEvent'; 4 | 5 | export const Phases = { 6 | CAPTURING: 1, 7 | BUBBLING: 3, 8 | }; 9 | 10 | const ElementHandlers: WeakMap< 11 | Element | Text, 12 | Map, 13 | > = new WeakMap(); 14 | 15 | function traverseTwoPhase( 16 | inst: Element, 17 | callback: Function, 18 | event: SyntheticEvent, 19 | ) { 20 | const path = collectAncestors(inst); 21 | 22 | let i; 23 | for (i = path.length; i-- > 0; ) 24 | if (callback(path[i], Phases.CAPTURING, event)) return; 25 | for (i = 0; i < path.length; i++) 26 | if (callback(path[i], Phases.BUBBLING, event)) return; 27 | } 28 | 29 | export function listenTo( 30 | domElement: Element, 31 | eventName: string, 32 | value: ?Function, 33 | lastValue: ?Function, 34 | ) { 35 | let handlers = ElementHandlers.get(domElement); 36 | if (!handlers) ElementHandlers.set(domElement, (handlers = new Map())); 37 | 38 | eventName = eventName.toLowerCase(); 39 | let phase = Phases.BUBBLING; 40 | 41 | if (eventName.endsWith('capture')) { 42 | eventName = eventName.slice(0, -7); 43 | phase = Phases.CAPTURING; 44 | } 45 | 46 | const originalName = eventName; 47 | const key = `${originalName}-${phase}`; 48 | eventName = originalName === 'change' ? 'input' : originalName; 49 | 50 | if (!value) { 51 | domElement.removeEventListener(eventName, handlerProxy, true); 52 | handlers.delete(key); 53 | } else { 54 | if (!lastValue) domElement.addEventListener(eventName, handlerProxy, true); 55 | handlers.set(key, value); 56 | } 57 | } 58 | 59 | const HandledEvents = new WeakSet(); 60 | export function handlerProxy(nativeEvent: Event) { 61 | if (HandledEvents.has(nativeEvent)) return; 62 | HandledEvents.add(nativeEvent); 63 | 64 | const target = (nativeEvent: any).target; 65 | dispatchSyntheticEvent(target, new SyntheticEvent(nativeEvent)); 66 | 67 | // if the event is an input event we need to also check for onChange events 68 | if (nativeEvent.type === 'input') { 69 | dispatchSyntheticEvent(target, new SyntheticEvent(nativeEvent, 'change')); 70 | } 71 | } 72 | 73 | export function dispatchSyntheticEvent(target: Element, event: SyntheticEvent) { 74 | // TODO: probably more traversal options for different events, 75 | // e.g. mouseenter/leave for portal compat 76 | traverseTwoPhase(target, executeHandlers, event); 77 | } 78 | 79 | function executeHandlers(element: Element, phase, event): ?boolean { 80 | const handlers = ElementHandlers.get(element); 81 | const handler = handlers && handlers.get(`${event.type}-${phase}`); 82 | if (!handler) return; 83 | 84 | event.currentTarget = element; 85 | event.eventPhase = phase; 86 | handler.call(element, event); 87 | return event.isPropagationStopped(); 88 | } 89 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant'; 4 | 5 | import Root from './Root'; 6 | import { DOMLiteReconciler } from './Reconciler'; 7 | 8 | const ContainerMap: WeakMap = new WeakMap(); 9 | 10 | const unstable_batchedUpdates = DOMLiteReconciler.batchedUpdates; 11 | 12 | function renderSubtreeIntoContainer( 13 | elements: React$Element, 14 | domContainer: DOMContainer, 15 | isAsync: boolean, 16 | hydrate: boolean, 17 | callback: ?Function, 18 | ) { 19 | let exitingRoot = ContainerMap.get(domContainer); 20 | if (exitingRoot) return exitingRoot.render(elements, callback); 21 | 22 | let root = new Root(domContainer, DOMLiteReconciler, isAsync, hydrate); 23 | ContainerMap.set(domContainer, root); 24 | // Initial render only is unbatched 25 | return DOMLiteReconciler.unbatchedUpdates(() => 26 | root.render(elements, callback), 27 | ); 28 | } 29 | 30 | function hydrate( 31 | elements: React$Element, 32 | domContainer: DOMContainer, 33 | callback: ?Function, 34 | ) { 35 | return renderSubtreeIntoContainer( 36 | elements, 37 | domContainer, 38 | false, 39 | true, 40 | callback, 41 | ); 42 | } 43 | 44 | function render( 45 | elements: React$Element, 46 | domContainer: DOMContainer, 47 | callback: ?Function, 48 | ) { 49 | return renderSubtreeIntoContainer( 50 | elements, 51 | domContainer, 52 | false, 53 | false, 54 | callback, 55 | ); 56 | } 57 | 58 | function unmountComponentAtNode(domContainer: DOMContainer): boolean { 59 | invariant( 60 | domContainer && [1, 8, 9, 11].indexOf(domContainer.nodeType) !== -1, 61 | 'unmountComponentAtNode(...): Target container is not a DOM element.', 62 | ); 63 | 64 | const root = ContainerMap.get(domContainer); 65 | 66 | if (!root) return false; 67 | 68 | DOMLiteReconciler.unbatchedUpdates(() => { 69 | root.unmount(() => { 70 | ContainerMap.delete(domContainer); 71 | }); 72 | }); 73 | 74 | return true; 75 | } 76 | 77 | function findDOMNode( 78 | componentOrElement: Element | ?React$Component, 79 | ): null | Element | Text { 80 | if (componentOrElement == null) return null; 81 | 82 | if (componentOrElement.nodeType === 1 || componentOrElement.nodeType === 3) { 83 | return (componentOrElement: any); 84 | } 85 | 86 | return DOMLiteReconciler.findHostInstance(componentOrElement); 87 | } 88 | 89 | // FIXME: Upstream needs to provide a better API for this. 90 | function createPortal( 91 | children: ReactNodeList, 92 | container: DOMContainer, 93 | key?: string, 94 | ): ReactPortal { 95 | return { 96 | $$typeof: Symbol.for('react.portal'), 97 | key: key == null ? null : String(key), 98 | children, 99 | containerInfo: container, 100 | implementation: null, 101 | }; 102 | } 103 | 104 | export { 105 | render, 106 | hydrate, 107 | unmountComponentAtNode, 108 | findDOMNode, 109 | createPortal, 110 | unstable_batchedUpdates, 111 | }; 112 | 113 | // This is match react-dom which only has default exports 114 | export default { 115 | render, 116 | hydrate, 117 | unmountComponentAtNode, 118 | findDOMNode, 119 | createPortal, 120 | unstable_batchedUpdates, 121 | }; 122 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { render } from './'; 4 | import * as Events from './events'; 5 | import SyntheticEvent from './events/SyntheticEvent'; 6 | 7 | export function renderIntoDetachedNode(children: React$Element) { 8 | const div = document.createElement('div'); 9 | return render(children, div); 10 | } 11 | 12 | export const SimulateNative = new Proxy( 13 | {}, 14 | { 15 | get(target, eventName) { 16 | eventName = eventName.toLowerCase(); 17 | 18 | return (domNode: Element, eventData: any) => { 19 | const nativeEvent = new Event(eventName); 20 | domNode.dispatchEvent(Object.assign(nativeEvent, eventData)); 21 | }; 22 | }, 23 | }, 24 | ); 25 | 26 | export const Simulate = new Proxy( 27 | {}, 28 | { 29 | get(target, eventName) { 30 | eventName = eventName.toLowerCase(); 31 | 32 | return (domNode: Element, eventData: any) => { 33 | const nativeEvent = new Event( 34 | eventName === 'change' ? 'input' : eventName, 35 | ); 36 | Object.defineProperty(nativeEvent, 'target', { 37 | value: domNode, 38 | enumerable: true, 39 | }); 40 | const event = new SyntheticEvent(nativeEvent, eventName); 41 | Events.dispatchSyntheticEvent(domNode, Object.assign(event, eventData)); 42 | }; 43 | }, 44 | }, 45 | ); 46 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.__DEV__ = true; 2 | global.__SVG__ = true; 3 | -------------------------------------------------------------------------------- /test/setupTests.js: -------------------------------------------------------------------------------- 1 | let windowErrors; 2 | 3 | beforeEach(() => { 4 | windowErrors = []; 5 | // jsdom is swallowing errors in event handlers 6 | window.onerror = err => { 7 | windowErrors.push(err); 8 | }; 9 | }); 10 | 11 | afterEach(() => { 12 | if (windowErrors.length) throw windowErrors.pop(); 13 | }); 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CompressionPlugin = require('compression-webpack-plugin'); 2 | const { rules, plugins } = require('webpack-atoms'); 3 | 4 | module.exports = { 5 | devtool: 'module-source-map', 6 | entry: './examples/App.js', 7 | output: { 8 | path: `${__dirname}/build`, 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [rules.js(), rules.css()], 13 | }, 14 | resolve: { 15 | alias: { 16 | 'react-dom-lite$': `${__dirname}/src/index.js`, 17 | }, 18 | }, 19 | plugins: [ 20 | plugins.html({ 21 | template: `${__dirname}/examples/index.html`, 22 | }), 23 | plugins.define({ 24 | __DEV__: true, 25 | __SVG__: false, 26 | }), 27 | plugins.extractCss(), 28 | new CompressionPlugin(), 29 | ], 30 | }; 31 | --------------------------------------------------------------------------------