Compare commits
384 Commits
0.3-stable
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54333842f | ||
|
|
0c1f2720b3 | ||
|
|
75f7f1e9ba | ||
|
|
09fbf8e0ca | ||
|
|
d17d0d5f50 | ||
|
|
1a7a8486ca | ||
|
|
65548db916 | ||
|
|
894bb03b23 | ||
|
|
94573545f3 | ||
|
|
7734429b89 | ||
|
|
35f092afef | ||
|
|
b837bb7bdd | ||
|
|
c629a0c5ad | ||
|
|
d889322827 | ||
|
|
156dffb961 | ||
|
|
e5befc0a73 | ||
|
|
e3f6a6d916 | ||
|
|
58fecc8cbe | ||
|
|
b5aad9479e | ||
|
|
5db3a0e481 | ||
|
|
0300f2aa22 | ||
|
|
17d36a4cc3 | ||
|
|
c222f57b00 | ||
|
|
1b64508aab | ||
|
|
f2b92d4c7b | ||
|
|
82f82c7543 | ||
|
|
602623661a | ||
|
|
ca3564898d | ||
|
|
5b662b43a0 | ||
|
|
6ba6fc149a | ||
|
|
5a3a39aba4 | ||
|
|
b9b300fcbd | ||
|
|
59f52bce04 | ||
|
|
8d3465060d | ||
|
|
d0af08190e | ||
|
|
b343fcaba3 | ||
|
|
526099c928 | ||
|
|
f0984cf789 | ||
|
|
7be14d8155 | ||
|
|
28f6f034ff | ||
|
|
3be6083ea4 | ||
|
|
f367d0e707 | ||
|
|
9694a0f7ea | ||
|
|
04bfa545f7 | ||
|
|
1971ae8cac | ||
|
|
fd3d16d379 | ||
|
|
dfd406fe4c | ||
|
|
301c571405 | ||
|
|
3376d27915 | ||
|
|
0827646695 | ||
|
|
03578e66b5 | ||
|
|
71e24455a3 | ||
|
|
83a840656c | ||
|
|
ed54fff204 | ||
|
|
5d4f903482 | ||
|
|
946029c921 | ||
|
|
f4321f8624 | ||
|
|
46d05b1191 | ||
|
|
558e8ca312 | ||
|
|
b763d7d029 | ||
|
|
2d61639f90 | ||
|
|
204796868d | ||
|
|
37014e1002 | ||
|
|
c6c4657f83 | ||
|
|
f457394362 | ||
|
|
2b97c608f8 | ||
|
|
15493530f1 | ||
|
|
dd1d49b360 | ||
|
|
e9e8934577 | ||
|
|
2397e35cdd | ||
|
|
0e585d8102 | ||
|
|
6009934176 | ||
|
|
b20c2641d4 | ||
|
|
445a0dac37 | ||
|
|
33abe80b59 | ||
|
|
d7cf1c509b | ||
|
|
7d97f26870 | ||
|
|
b2107ba80b | ||
|
|
d0c431a2a3 | ||
|
|
ab7ef4ed3b | ||
|
|
204edb4a27 | ||
|
|
3c742d50b6 | ||
|
|
1c51cc34cd | ||
|
|
64b9b55a0d | ||
|
|
f9741b0728 | ||
|
|
3085254a91 | ||
|
|
0de35588c1 | ||
|
|
21ea1ac61e | ||
|
|
4fccaa514b | ||
|
|
13ad0c500b | ||
|
|
126a7f5c11 | ||
|
|
bb3bd76fe9 | ||
|
|
4bbdcdb0b8 | ||
|
|
d294a7f30f | ||
|
|
de8d0e35a2 | ||
|
|
faa84b5b85 | ||
|
|
e1e5f17b27 | ||
|
|
0e63000b5c | ||
|
|
151997b1e1 | ||
|
|
a36bcd33c6 | ||
|
|
9194fea915 | ||
|
|
67d9891926 | ||
|
|
05341fb3b3 | ||
|
|
c82afd7e54 | ||
|
|
ed98f2ca57 | ||
|
|
462e450bb3 | ||
|
|
06e5fcc010 | ||
|
|
ac84652e50 | ||
|
|
eee3980749 | ||
|
|
cf83fbe397 | ||
|
|
e221ff7cd4 | ||
|
|
607de16d82 | ||
|
|
d762627312 | ||
|
|
10dab495f2 | ||
|
|
ee1335b6a2 | ||
|
|
8687645c50 | ||
|
|
7b68fcd408 | ||
|
|
92dab0759c | ||
|
|
5c6e59f53c | ||
|
|
067fe27699 | ||
|
|
8db2ba9130 | ||
|
|
1500e9810c | ||
|
|
a3b21b10e4 | ||
|
|
fa03e98426 | ||
|
|
203dba271b | ||
|
|
91562ba934 | ||
|
|
fc5f7e0e85 | ||
|
|
a4123a069e | ||
|
|
a5ddb07cb3 | ||
|
|
917e101c2c | ||
|
|
5c624021ea | ||
|
|
5676a486cf | ||
|
|
1658feade8 | ||
|
|
5f1eceb1ee | ||
|
|
fc3491e0d0 | ||
|
|
9f94244994 | ||
|
|
6ebdd0cfd2 | ||
|
|
858377946f | ||
|
|
826d603b05 | ||
|
|
da4b761c45 | ||
|
|
70a2f8046c | ||
|
|
51bf95f6d1 | ||
|
|
2246f530af | ||
|
|
4e04ef0769 | ||
|
|
b2bbdf8cbf | ||
|
|
9178208ba8 | ||
|
|
7d3db0e5ed | ||
|
|
2869e5b4df | ||
|
|
510146eb6d | ||
|
|
c692d9e844 | ||
|
|
32423a83fc | ||
|
|
d50148591b | ||
|
|
b6451be582 | ||
|
|
5e296d7af8 | ||
|
|
3093a476b1 | ||
|
|
00e56c5155 | ||
|
|
8b9891aa8a | ||
|
|
f7901a2380 | ||
|
|
5c4352b57b | ||
|
|
9ca7c9631a | ||
|
|
d9e99d4688 | ||
|
|
3fd56b4038 | ||
|
|
44659df598 | ||
|
|
0e9ee239a9 | ||
|
|
ce0704a491 | ||
|
|
0acc1d8c78 | ||
|
|
18ef8962f1 | ||
|
|
e748be32da | ||
|
|
0ad14fc038 | ||
|
|
dd61439061 | ||
|
|
40bebf0c86 | ||
|
|
2aa5631e2e | ||
|
|
f39a0f8e40 | ||
|
|
ca19ffb083 | ||
|
|
15272f30f4 | ||
|
|
55176116a2 | ||
|
|
738de8cfa8 | ||
|
|
bd150ec658 | ||
|
|
ee21a604f3 | ||
|
|
43358157cf | ||
|
|
0b65d7555e | ||
|
|
8bc2abd367 | ||
|
|
6556881417 | ||
|
|
c54900f63e | ||
|
|
9c35189ad1 | ||
|
|
e998041229 | ||
|
|
431e1d5608 | ||
|
|
d4c7991aee | ||
|
|
811df48756 | ||
|
|
dbd9d99bcd | ||
|
|
c032743b93 | ||
|
|
418c1fc427 | ||
|
|
fe30279ed0 | ||
|
|
b4f096364f | ||
|
|
9e6a581f68 | ||
|
|
f7e49f3b25 | ||
|
|
2fd5efd92b | ||
|
|
67cf44e7c1 | ||
|
|
9ceaff7318 | ||
|
|
703c825196 | ||
|
|
86adcd6766 | ||
|
|
5a85c5e535 | ||
|
|
cf926338bf | ||
|
|
43930455de | ||
|
|
6f04bd9410 | ||
|
|
59212a538e | ||
|
|
a9b024330c | ||
|
|
d93761af62 | ||
|
|
fb6381fb35 | ||
|
|
1d65f81b16 | ||
|
|
1c40dde782 | ||
|
|
f6c4d2d161 | ||
|
|
b282a0f4f1 | ||
|
|
c1886c6513 | ||
|
|
bd8ecc1caa | ||
|
|
6bbcbc08cf | ||
|
|
f0a4ca5f69 | ||
|
|
de40842597 | ||
|
|
76ec746341 | ||
|
|
14102e8a48 | ||
|
|
4f2d8dfe72 | ||
|
|
7c60bb3e54 | ||
|
|
a62686622b | ||
|
|
a61f4df0b9 | ||
|
|
3266818b42 | ||
|
|
c9ecbaccb3 | ||
|
|
336a0facc1 | ||
|
|
7053f59ad1 | ||
|
|
3373572e15 | ||
|
|
1839bcf109 | ||
|
|
191c0dec32 | ||
|
|
2bc2b52eaa | ||
|
|
36a724feca | ||
|
|
18352090e7 | ||
|
|
4c97ffee34 | ||
|
|
ac5320e887 | ||
|
|
91b10bd37c | ||
|
|
79a2734068 | ||
|
|
32030687ba | ||
|
|
cb01363260 | ||
|
|
0493b27222 | ||
|
|
44d6b94752 | ||
|
|
a2bc7387e4 | ||
|
|
ea5e13893e | ||
|
|
bd044fc919 | ||
|
|
870a29d9b0 | ||
|
|
0e91febb9c | ||
|
|
37ddfa0521 | ||
|
|
5bd449c157 | ||
|
|
b69d7f0d2a | ||
|
|
a0475b3c29 | ||
|
|
3156458041 | ||
|
|
48333acba6 | ||
|
|
4a0456fb8e | ||
|
|
405be0f966 | ||
|
|
000928f9dc | ||
|
|
0f87e8ee87 | ||
|
|
2195a479a8 | ||
|
|
2383fd8813 | ||
|
|
d8b6d260c9 | ||
|
|
6a41ede2d4 | ||
|
|
c04081bc56 | ||
|
|
0b1ecd8872 | ||
|
|
88e90d5601 | ||
|
|
03464dc148 | ||
|
|
1112f1a003 | ||
|
|
c1576fcf97 | ||
|
|
8592eacbf9 | ||
|
|
888cb824d7 | ||
|
|
061527df6c | ||
|
|
96b0a0253f | ||
|
|
c79a59b599 | ||
|
|
fad7d58fc9 | ||
|
|
97efa84676 | ||
|
|
22347ea54d | ||
|
|
46513c6d78 | ||
|
|
1c7d01c2f4 | ||
|
|
7a0f2d71bb | ||
|
|
4104beadbb | ||
|
|
87f4b8be67 | ||
|
|
8d729d7da2 | ||
|
|
f016479289 | ||
|
|
c81cc2e6d5 | ||
|
|
f3aac85d01 | ||
|
|
c6665e3460 | ||
|
|
c7295b9e09 | ||
|
|
9fd9f712bf | ||
|
|
a9c70bcc1c | ||
|
|
80edd6ca87 | ||
|
|
48f46b568d | ||
|
|
279792f891 | ||
|
|
792b69ba11 | ||
|
|
06cff60bc1 | ||
|
|
770ec5946a | ||
|
|
b525a0c061 | ||
|
|
34970fd785 | ||
|
|
ceb5303581 | ||
|
|
f456f8fa8d | ||
|
|
e1fe13d0cb | ||
|
|
01511ea557 | ||
|
|
802241a660 | ||
|
|
e1535fbd71 | ||
|
|
aea8e16b4a | ||
|
|
0d6bb650cb | ||
|
|
ddc4ffffa0 | ||
|
|
d7a5f137ff | ||
|
|
34173638d4 | ||
|
|
c9618587ef | ||
|
|
101bfa3112 | ||
|
|
523bde4dc5 | ||
|
|
483350905b | ||
|
|
5b72334852 | ||
|
|
d13a37ce22 | ||
|
|
0c6f4b3bcc | ||
|
|
0e6fca4a38 | ||
|
|
dfd76be568 | ||
|
|
0dc08c2115 | ||
|
|
6b572b3f25 | ||
|
|
c6c40a5fb3 | ||
|
|
fdc6beed1a | ||
|
|
731aa8ead1 | ||
|
|
8762634cf1 | ||
|
|
735a91c9d5 | ||
|
|
705ce56694 | ||
|
|
94fdf2cf5d | ||
|
|
758c21fb9c | ||
|
|
15360056bd | ||
|
|
932c45a7ab | ||
|
|
e39743f2f8 | ||
|
|
cff4d53a9e | ||
|
|
c2ce1d00cd | ||
|
|
b3e0dc47a8 | ||
|
|
93f979ae18 | ||
|
|
ca3f871646 | ||
|
|
1be6c592a6 | ||
|
|
37cde3d864 | ||
|
|
2afd7186ae | ||
|
|
c19bf9cffe | ||
|
|
7e7579e1ba | ||
|
|
582359aeea | ||
|
|
6c3c643c8e | ||
|
|
3eaed5a122 | ||
|
|
4bb966a7f0 | ||
|
|
0e9e64c550 | ||
|
|
c5998fb483 | ||
|
|
880ada0a1c | ||
|
|
a5e5f53494 | ||
|
|
81f3a5c6cd | ||
|
|
59bee8df21 | ||
|
|
796837b8c7 | ||
|
|
2dc24fc234 | ||
|
|
88923f61a7 | ||
|
|
ba6fea1bf5 | ||
|
|
36d8ce8fab | ||
|
|
153fd9246e | ||
|
|
fac24d462f | ||
|
|
83101b878e | ||
|
|
9d1055b3d2 | ||
|
|
0614d30654 | ||
|
|
9965b6b9dd | ||
|
|
a06de4bc4f | ||
|
|
11a7cb5b73 | ||
|
|
bae6100ae8 | ||
|
|
3ffbb4d096 | ||
|
|
ca5d7bc683 | ||
|
|
1457850b72 | ||
|
|
100af48f53 | ||
|
|
3e211bf662 | ||
|
|
606d6b8fd4 | ||
|
|
b581c8cfc7 | ||
|
|
54d3134da2 | ||
|
|
259392035d | ||
|
|
4b81de93d3 | ||
|
|
93fc188afb | ||
|
|
007b75f78a | ||
|
|
0435216eb6 | ||
|
|
7061d2b25b | ||
|
|
31cdb4c8a7 | ||
|
|
1e76d84569 | ||
|
|
6012e94e50 | ||
|
|
35306fa7f5 | ||
|
|
580e8f0dbb | ||
|
|
2467c0e651 | ||
|
|
292dd238e7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules
|
||||
static
|
||||
.grunt
|
||||
_SpecRunner.html
|
||||
__benchmarks__
|
||||
build/
|
||||
.module-cache
|
||||
*.gem
|
||||
|
||||
66
CHANGELOG.md
Normal file
66
CHANGELOG.md
Normal file
@@ -0,0 +1,66 @@
|
||||
## 0.4.0 (July 17, 2013)
|
||||
|
||||
### React
|
||||
|
||||
* Switch from using `id` attribute to `data-reactid` to track DOM nodes. This allows you to integrate with other JS and CSS libraries more easily.
|
||||
* Support for more DOM elements and attributes (e.g., `<canvas>`)
|
||||
* Improved server-side rendering APIs. `React.renderComponentToString(<component>, callback)` allows you to use React on the server and generate markup which can be sent down to the browser.
|
||||
* `prop` improvements: validation and default values. [Read our blog post for details...](http://facebook.github.io/react/blog/2013/07/11/react-v0-4-prop-validation-and-default-values.html)
|
||||
* Support for the `key` prop, which allows for finer control over reconciliation. [Read the docs for details...](http://facebook.github.io/react/docs/multiple-components.html)
|
||||
* Removed `React.autoBind`. [Read our blog post for details...](http://facebook.github.io/react/blog/2013/07/02/react-v0-4-autobind-by-default.html)
|
||||
* Improvements to forms. We've written wrappers around `<input>`, `<textarea>`, `<option>`, and `<select>` in order to standardize many inconsistencies in browser implementations. This includes support for `defaultValue`, and improved implementation of the `onChange` event, and circuit completion. [Read the docs for details...](http://facebook.github.io/react/docs/forms.html)
|
||||
* We've implemented an improved synthetic event system that conforms to the W3C spec.
|
||||
* Updates to your component are batched now, which may result in a significantly faster re-render of components. `this.setState` now takes an optional callback as it's second parameter. If you were using `onClick={this.setState.bind(this, state)}` previously, you'll want to make sure you add a third parameter so that the event is not treated as the callback.
|
||||
|
||||
### JSX
|
||||
|
||||
* Support for comment nodes `<div>{/* this is a comment and won't be rendered */}</div>`
|
||||
* Children are now transformed directly into arguments instead of being wrapped in an array
|
||||
E.g. `<div><Component1/><Component2></div>` is transformed into `React.DOM.div(null, Component1(null), Component2(null))`.
|
||||
Previously this would be transformed into `React.DOM.div(null, [Component1(null), Component2(null)])`.
|
||||
If you were using React without JSX previously, your code should still work.
|
||||
|
||||
### react-tools
|
||||
|
||||
* Fixed a number of bugs when transforming directories
|
||||
* No longer re-write `require()`s to be relative unless specified
|
||||
|
||||
|
||||
## 0.3.3 (June 20, 2013)
|
||||
|
||||
### React
|
||||
|
||||
* Allow reusing the same DOM node to render different components. e.g. `React.renderComponent(<div/>, domNode); React.renderComponent(<span/>, domNode);` will work now.
|
||||
|
||||
### JSX
|
||||
|
||||
* Improved the in-browser transformer so that transformed scripts will execute in the expected scope. The allows components to be defined and used from separate files.
|
||||
|
||||
### react-tools
|
||||
|
||||
* Upgrade Commoner so `require` statements are no longer relativized when passing through the transformer. This was a feature needed when building React, but doesn't translate well for other consumers of `bin/jsx`.
|
||||
* Upgraded our dependencies on Commoner and Recast so they use a different directory for their cache.
|
||||
* Freeze our esprima dependency.
|
||||
|
||||
|
||||
## 0.3.2 (May 31, 2013)
|
||||
|
||||
### JSX
|
||||
|
||||
* Improved compatability with other coding styles (specifically, multiple assignments with a single `var`).
|
||||
|
||||
### react-tools
|
||||
|
||||
* Switch from using the browserified build to shipping individual modules. This allows react-tools to be used with [browserify](https://github.com/substack/node-browserify).
|
||||
|
||||
|
||||
## 0.3.1 (May 30, 2013)
|
||||
|
||||
### react-tools
|
||||
|
||||
* Fix bug in packaging resulting in broken module.
|
||||
|
||||
|
||||
## 0.3.0 (May 29, 2013)
|
||||
|
||||
* Initial public release
|
||||
@@ -4,6 +4,7 @@ var exec = require('child_process').exec;
|
||||
var jsxTask = require('./grunt/tasks/jsx');
|
||||
var browserifyTask = require('./grunt/tasks/browserify');
|
||||
var wrapupTask = require('./grunt/tasks/wrapup');
|
||||
var populistTask = require('./grunt/tasks/populist');
|
||||
var phantomTask = require('./grunt/tasks/phantom');
|
||||
var npmTask = require('./grunt/tasks/npm');
|
||||
var releaseTasks = require('./grunt/tasks/release');
|
||||
@@ -16,6 +17,7 @@ module.exports = function(grunt) {
|
||||
jsx: require('./grunt/config/jsx/jsx'),
|
||||
browserify: require('./grunt/config/browserify'),
|
||||
wrapup: require('./grunt/config/wrapup'),
|
||||
populist: require('./grunt/config/populist'),
|
||||
phantom: require('./grunt/config/phantom'),
|
||||
npm: require('./grunt/config/npm'),
|
||||
clean: ['./build', './*.gem', './docs/_site', './examples/shared/*.js'],
|
||||
@@ -44,6 +46,8 @@ module.exports = function(grunt) {
|
||||
// defines global variables instead of using require.
|
||||
grunt.registerMultiTask('wrapup', wrapupTask);
|
||||
|
||||
grunt.registerMultiTask('populist', populistTask);
|
||||
|
||||
grunt.registerMultiTask('phantom', phantomTask);
|
||||
|
||||
grunt.registerMultiTask('npm', npmTask);
|
||||
@@ -52,11 +56,10 @@ module.exports = function(grunt) {
|
||||
grunt.registerTask('build:transformer', ['jsx:debug', 'browserify:transformer']);
|
||||
grunt.registerTask('build:min', ['jsx:release', 'browserify:min']);
|
||||
grunt.registerTask('build:test', [
|
||||
'jsx:debug',
|
||||
'jsx:jasmine',
|
||||
'jsx:test',
|
||||
'browserify:jasmine',
|
||||
'browserify:test'
|
||||
'populist:jasmine',
|
||||
'populist:test'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', ['build:test', 'phantom:run']);
|
||||
|
||||
30
bin/jsx
30
bin/jsx
@@ -3,7 +3,7 @@
|
||||
|
||||
var visitors = require('../vendor/fbtransform/visitors').transformVisitors;
|
||||
var transform = require('../vendor/fbtransform/lib/transform').transform;
|
||||
var debranch = require("../vendor/woodchipper").debranch;
|
||||
var propagate = require("../vendor/constants").propagate;
|
||||
|
||||
require("commoner").resolve(function(id) {
|
||||
var context = this;
|
||||
@@ -25,18 +25,30 @@ require("commoner").resolve(function(id) {
|
||||
|
||||
}).process(function(id, source) {
|
||||
var context = this;
|
||||
var constants = context.config.constants || {};
|
||||
|
||||
// This is where JSX, ES6, etc. desugaring happens.
|
||||
source = transform(visitors.react, source).code;
|
||||
|
||||
return context.makePromise(function(callback) {
|
||||
var constants = context.config.constants || {};
|
||||
// Constant propagation means removing any obviously dead code after
|
||||
// replacing constant expressions with literal (boolean) values.
|
||||
source = propagate(constants, source);
|
||||
|
||||
// Debranching means removing any obviously dead code after
|
||||
// replacing constant conditional expressions with literal
|
||||
// (boolean) values.
|
||||
debranch(constants, source, function(source) {
|
||||
callback(null, source);
|
||||
if (context.config.mocking) {
|
||||
// Make sure there is exactly one newline at the end of the module.
|
||||
source = source.replace(/\s+$/m, "\n");
|
||||
|
||||
return context.getProvidedP().then(function(idToPath) {
|
||||
if (id !== "mock-modules" &&
|
||||
id !== "mocks" &&
|
||||
idToPath.hasOwnProperty("mock-modules")) {
|
||||
return source + '\nrequire("mock-modules").register(' +
|
||||
JSON.stringify(id) + ', module);\n';
|
||||
}
|
||||
|
||||
return source;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return source;
|
||||
});
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
---
|
||||
markdown: redcarpet
|
||||
name: React
|
||||
description: A JavaScript library for building user interfaces
|
||||
redcarpet:
|
||||
extensions:
|
||||
- fenced_code_blocks
|
||||
react_version: 0.3.3
|
||||
pygments: true
|
||||
baseurl: /react
|
||||
url: http://facebook.github.io
|
||||
permalink: /blog/:year/:month/:day/:title.html
|
||||
exclude:
|
||||
- Gemfile
|
||||
- Gemfile.lock
|
||||
- README.md
|
||||
- Rakefile
|
||||
url: http://facebook.github.io
|
||||
baseurl: /react
|
||||
permalink: /blog/:year/:month/:day/:title.html
|
||||
redcarpet:
|
||||
extensions:
|
||||
- fenced_code_blocks
|
||||
pygments: true
|
||||
name: React
|
||||
markdown: redcarpet
|
||||
react_version: 0.4.0
|
||||
description: A JavaScript library for building user interfaces
|
||||
relative_permalinks: true
|
||||
nav_docs_sections:
|
||||
- title: Quick Start
|
||||
items:
|
||||
- id: getting-started
|
||||
title: Getting Started
|
||||
- id: tutorial
|
||||
title: Tutorial
|
||||
- title: Guides
|
||||
items:
|
||||
- id: why-react
|
||||
title: Why React?
|
||||
- id: displaying-data
|
||||
title: Displaying Data
|
||||
subitems:
|
||||
- id: jsx-in-depth
|
||||
title: JSX in Depth
|
||||
- id: jsx-gotchas
|
||||
title: JSX Gotchas
|
||||
- id: interactivity-and-dynamic-uis
|
||||
title: Interactivity and Dynamic UIs
|
||||
- id: multiple-components
|
||||
title: Multiple Components
|
||||
- id: reusable-components
|
||||
title: Reusable Components
|
||||
- id: forms
|
||||
title: Forms
|
||||
- id: working-with-the-browser
|
||||
title: Working With the Browser
|
||||
subitems:
|
||||
- id: more-about-refs
|
||||
title: More About Refs
|
||||
- id: tooling-integration
|
||||
title: Tooling integration
|
||||
- id: reference
|
||||
title: Reference
|
||||
|
||||
@@ -206,6 +206,9 @@ li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
ul ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
<div class="nav-docs">
|
||||
<div class="nav-docs-section">
|
||||
<h3>Getting started</h3>
|
||||
<ul>
|
||||
<li><a href="/react/docs/getting-started.html"{% if page.id == 'docs-getting-started' %} class="active"{% endif %}>Getting Started</a></li>
|
||||
<li><a href="/react/docs/tutorial.html"{% if page.id == 'docs-tutorial' %} class="active"{% endif %}>Tutorial</a></li>
|
||||
<li><a href="/react/docs/common-questions.html"{% if page.id == 'docs-common-questions' %} class="active"{% endif %}>Common Questions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-docs-section">
|
||||
<h3>Reference</h3>
|
||||
<ul>
|
||||
<li><a href="/react/docs/syntax.html"{% if page.id == 'docs-syntax' %} class="active"{% endif %}>JSX Syntax</a></li>
|
||||
<li><a href="/react/docs/component-basics.html"{% if page.id == 'docs-component-basics' %} class="active"{% endif %}>Component Basics</a></li>
|
||||
<li><a href="/react/docs/component-data.html"{% if page.id == 'docs-component-data' %} class="active"{% endif %}>Component Data</a></li>
|
||||
<li><a href="/react/docs/component-lifecycle.html"{% if page.id == 'docs-component-lifecycle' %} class="active"{% endif %}>Component Lifecycle</a></li>
|
||||
<li><a href="/react/docs/event-handling.html"{% if page.id == 'docs-event-handling' %} class="active"{% endif %}>Event Handling</a></li>
|
||||
<li><a href="/react/docs/advanced-components.html"{% if page.id == 'docs-advanced-components' %} class="active"{% endif %}>Advanced Components</a></li>
|
||||
<li><a href="/react/docs/mixins.html"{% if page.id == 'docs-mixins' %} class="active"{% endif %}>Mixins</a></li>
|
||||
<li><a href="/react/docs/api.html"{% if page.id == 'docs-api' %} class="active"{% endif %}>API</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-docs-section">
|
||||
<h3>Appendix</h3>
|
||||
<ul>
|
||||
<li><a href="/react/docs/jsx-is-not-html.html"{% if page.id == 'docs-jsx-is-not-html' %} class="active"{% endif %}>JSX is not HTML</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% for section in site.nav_docs_sections %}
|
||||
<div class="nav-docs-section">
|
||||
<h3>{{ section.title }}</h3>
|
||||
<ul>
|
||||
{% for item in section.items %}
|
||||
<li>
|
||||
<a href="/react/docs/{{ item.id }}.html"{% if page.id == item.id %} class="active"{% endif %}>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
{% if item.subitems %}
|
||||
<ul>
|
||||
{% for subitem in item.subitems %}
|
||||
<li>
|
||||
<a href="/react/docs/{{ subitem.id }}.html"{% if page.id == subitem.id %} class="active"{% endif %}>
|
||||
{{ subitem.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -11,14 +11,14 @@ var MarkdownEditor = React.createClass({\n\
|
||||
getInitialState: function() {\n\
|
||||
return {value: 'Type some *markdown* here!'};\n\
|
||||
},\n\
|
||||
handleKeyUp: React.autoBind(function() {\n\
|
||||
handleInput: function() {\n\
|
||||
this.setState({value: this.refs.textarea.getDOMNode().value});\n\
|
||||
}),\n\
|
||||
},\n\
|
||||
render: function() {\n\
|
||||
return (\n\
|
||||
<div className=\"MarkdownEditor\">\n\
|
||||
<h3>Input</h3>\n\
|
||||
<textarea onKeyUp={this.handleKeyUp} ref=\"textarea\">\n\
|
||||
<textarea onInput={this.handleInput} ref=\"textarea\">\n\
|
||||
{this.state.value}\n\
|
||||
</textarea>\n\
|
||||
<h3>Output</h3>\n\
|
||||
|
||||
@@ -7,9 +7,9 @@ var Timer = React.createClass({\n\
|
||||
getInitialState: function() {\n\
|
||||
return {secondsElapsed: 0};\n\
|
||||
},\n\
|
||||
tick: React.autoBind(function() {\n\
|
||||
tick: function() {\n\
|
||||
this.setState({secondsElapsed: this.state.secondsElapsed + 1});\n\
|
||||
}),\n\
|
||||
},\n\
|
||||
componentDidMount: function() {\n\
|
||||
setInterval(this.tick, 1000);\n\
|
||||
},\n\
|
||||
|
||||
40
docs/_posts/2013-07-17-react-v0-4-0.md
Normal file
40
docs/_posts/2013-07-17-react-v0-4-0.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "React v0.4.0"
|
||||
layout: post
|
||||
author: Paul O'Shannessy
|
||||
---
|
||||
|
||||
Over the past 2 months we've been taking feedback and working hard to make React even better. We've also added several features that awe didn't finish in time for the v0.3 launch, and are proud to announce the availability of React v0.4 today!
|
||||
|
||||
|
||||
This release could not have happened without the support of our growing community. Since launch day, the community has contributed blog posts, questions to the [Google Group](http://groups.google.com/group/reactjs), and issues and pull requests on GitHub. We've had contributions ranging from documentation improvements to major changes to React's rendering. We've seen people integrate React into the tools they're using and the products they're building, and we're all very excited to see what our budding community builds next!
|
||||
|
||||
React v0.4 has some big changes. We've also restructured the documentation to better communicate how to use React. We've summarized the changes below and linked to documentation where we think it will be especially useful.
|
||||
|
||||
When you're ready, [go download it](/react/downloads.html)!
|
||||
|
||||
|
||||
### React
|
||||
|
||||
* Switch from using `id` attribute to `data-reactid` to track DOM nodes. This allows you to integrate with other JS and CSS libraries more easily.
|
||||
* Support for more DOM elements and attributes (e.g., `<canvas>`)
|
||||
* Improved server-side rendering APIs. `React.renderComponentToString(<component>, callback)` allows you to use React on the server and generate markup which can be sent down to the browser.
|
||||
* `prop` improvements: validation and default values. [Read our blog post for details...](http://facebook.github.io/react/blog/2013/07/11/react-v0-4-prop-validation-and-default-values.html)
|
||||
* Support for the `key` prop, which allows for finer control over reconciliation. [Read the docs for details...](http://facebook.github.io/react/docs/multiple-components.html)
|
||||
* Removed `React.autoBind`. [Read our blog post for details...](http://facebook.github.io/react/blog/2013/07/02/react-v0-4-autobind-by-default.html)
|
||||
* Improvements to forms. We've written wrappers around `<input>`, `<textarea>`, `<option>`, and `<select>` in order to standardize many inconsistencies in browser implementations. This includes support for `defaultValue`, and improved implementation of the `onChange` event, and circuit completion. [Read the docs for details...](http://facebook.github.io/react/docs/forms.html)
|
||||
* We've implemented an improved synthetic event system that conforms to the W3C spec.
|
||||
* Updates to your component are batched now, which may result in a significantly faster re-render of components. `this.setState` now takes an optional callback as it's second parameter. If you were using `onClick={this.setState.bind(this, state)}` previously, you'll want to make sure you add a third parameter so that the event is not treated as the callback.
|
||||
|
||||
### JSX
|
||||
|
||||
* Support for comment nodes `<div>{/* this is a comment and won't be rendered */}</div>`
|
||||
* Children are now transformed directly into arguments instead of being wrapped in an array
|
||||
E.g. `<div><Component1/><Component2></div>` is transformed into `React.DOM.div(null, Component1(null), Component2(null))`.
|
||||
Previously this would be transformed into `React.DOM.div(null, [Component1(null), Component2(null)])`.
|
||||
If you were using React without JSX previously, your code should still work.
|
||||
|
||||
### react-tools
|
||||
|
||||
* Fixed a number of bugs when transforming directories
|
||||
* No longer re-write `require()`s to be relative unless specified
|
||||
30
docs/docs/01-why-react.md
Normal file
30
docs/docs/01-why-react.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
id: why-react
|
||||
title: Why React?
|
||||
layout: docs
|
||||
permalink: why-react.html
|
||||
next: displaying-data.html
|
||||
---
|
||||
React is a JavaScript library for creating user interfaces by Facebook and Instagram. Many people choose to think of React as the **V** in **[MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)**.
|
||||
|
||||
We built React to solve one problem: **building large applications with data that changes over time**. To do this, React uses two main ideas.
|
||||
|
||||
### Simple
|
||||
|
||||
Simply express how your app should look at any given point in time, and React will automatically manage all UI updates when your underlying data changes.
|
||||
|
||||
### Declarative
|
||||
|
||||
When the data changes, React conceptually hits the "refresh" button, and knows to only update the changed parts.
|
||||
|
||||
## Build Composable Components
|
||||
|
||||
React is all about building reusable components. In fact, with React the *only* thing you do is build components. Since they're so encapsulated, components make code reuse, testing, and separation of concerns easy.
|
||||
|
||||
## Give It Five Minutes
|
||||
|
||||
React challenges a lot of conventional wisdom, and at first glance some of the ideas may seem crazy. [Give it five minutes](http://37signals.com/svn/posts/3124-give-it-five-minutes) while reading this guide; those crazy ideas have worked for building thousands of components both inside and outside of Facebook and Instagram.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more about our motivations behind building React in [this blog post](http://facebook.github.io/react/blog/2013/06/05/why-react.html).
|
||||
91
docs/docs/02-displaying-data.md
Normal file
91
docs/docs/02-displaying-data.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
id: displaying-data
|
||||
title: Displaying Data
|
||||
layout: docs
|
||||
permalink: displaying-data.html
|
||||
prev: why-react.html
|
||||
next: jsx-in-depth.html
|
||||
---
|
||||
|
||||
The most basic thing you can do with a UI is display some data. React makes it easy to display data and automatically keeps the interface up-to-date when the data changes.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
Let's look at a really simple example. Create a `hello-react.html` file with the following code:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello React</title>
|
||||
<script src="http://fb.me/react-{{site.react_version}}.min.js"></script>
|
||||
<script src="http://fb.me/JSXTransformer-{{site.react_version}}.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="example"></div>
|
||||
<script type="text/jsx">
|
||||
|
||||
// ** Your code goes here! **
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
For the rest of the documentation, we'll just focus on the JavaScript code and assume it's inserted into a template like the one above. Replace the placeholder comment above with the following JS:
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var HelloWorld = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<p>
|
||||
Hello, <input type="text" placeholder="Your name here" />!
|
||||
It is {this.props.date.toTimeString()}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
React.renderComponent(
|
||||
<HelloWorld date={new Date()} />,
|
||||
document.getElementById('example')
|
||||
);
|
||||
}, 500);
|
||||
```
|
||||
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
Open `hello-react.html` in a web browser and type your name into the text field. Notice that React is only changing the time string in the UI — any input you put in the text field remains, even though you haven't written any code to manage this behavior. React figures it out for you and does the right thing.
|
||||
|
||||
The way we are able to figure this out is that React does not manipulate the DOM unless it needs to. **It uses a fast, internal mock DOM to perform diffs and computes the most efficient DOM mutation for you.**
|
||||
|
||||
The inputs to this component are called `props` — short for "properties". They're passed as attributes in JSX syntax. You should think of these as immutable within the component, that is, **never write to `this.props`**.
|
||||
|
||||
|
||||
## Components are Just Like Functions
|
||||
|
||||
React components are very simple. You can think of them as simple function that take in `props` and `state` (discussed later) and render HTML. Because they're so simple, it makes them very easy to reason about.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> **One limitation**: React components can only render a single root node. If you want to return multiple nodes they *must* be wrapped in a single root.
|
||||
|
||||
|
||||
## JSX Syntax
|
||||
|
||||
We strongly believe that components are the right way to separate concerns rather than "templates" and "display logic." We think that markup and the code that generates it are intimately tied together. Additionally, display logic is often very complex and using template languages to express it becomes cumbersome.
|
||||
|
||||
We've found that the best solution for this problem is to generate markup directly from the JavaScript code such that you can use all of the expressive power of a real programming language to build UIs. In order to make this easier, we've added a very simple, **optional** HTML-like syntax for the function calls that generate markup called JSX.
|
||||
|
||||
**JSX lets you write JavaScript function calls with HTML syntax.** To generate a link in React using pure JavaScript you'd write: `React.DOM.a({href: 'http://facebook.github.io/react/'}, 'Hello React!')`. With JSX this becomes `<a href="http://facebook.github.io/react/">Hello React!</a>`. We've found this has made building React apps easier and designers tend to prefer the syntax, but everyone has their own workflow, so **JSX is not required to use React.**
|
||||
|
||||
JSX is very small; the "hello, world" example above uses every feature of JSX. To learn more about it, see [JSX in depth](./jsx-in-depth.html). Or see the transform in action in [our live JSX compiler](/react/jsx-compiler.html).
|
||||
|
||||
JSX is similar to HTML, but not exactly the same. See [JSX gotchas](./jsx-gotchas.html) for some key differences.
|
||||
|
||||
The easiest way to get started with JSX is to use the in-browser `JSXTransformer`. We strongly recommend that you don't use this in production. You can precompile your code using our command-line [react-tools](http://npmjs.org/package/react-tools) package.
|
||||
@@ -1,18 +1,17 @@
|
||||
---
|
||||
id: docs-syntax
|
||||
title: JSX Syntax
|
||||
description: Writing JavaScript with XML syntax.
|
||||
id: jsx-in-depth
|
||||
title: JSX in Depth
|
||||
layout: docs
|
||||
prev: common-questions.html
|
||||
next: component-basics.html
|
||||
permalink: jsx-in-depth.html
|
||||
prev: displaying-data.html
|
||||
next: jsx-gotchas.html
|
||||
---
|
||||
|
||||
JSX is a JavaScript XML syntax transform recommended (but not required) for use
|
||||
JSX is a JavaScript XML syntax transform recommended for use
|
||||
with React.
|
||||
|
||||
## Why JSX?
|
||||
|
||||
First of all, **don't use JSX if you don't like it!**
|
||||
## Why JSX?
|
||||
|
||||
React works out of the box without JSX. Simply construct your markup using the
|
||||
functions on `React.DOM`. For example, here's how to construct a simple link:
|
||||
@@ -23,21 +22,22 @@ var link = React.DOM.a({href: 'http://facebook.github.io/react'}, 'React');
|
||||
|
||||
We recommend using JSX for many reasons:
|
||||
|
||||
- It's easier to visualize the structure of the DOM.
|
||||
- Designers are more comfortable making changes.
|
||||
- It's familiar for those who have used MXML or XAML.
|
||||
* It's easier to visualize the structure of the DOM.
|
||||
* Designers are more comfortable making changes.
|
||||
* It's familiar for those who have used MXML or XAML.
|
||||
|
||||
|
||||
## The Transform
|
||||
|
||||
JSX transforms XML-like syntax into native JavaScript. It turns XML elements and
|
||||
attributes into function calls and objects, respectively.
|
||||
JSX transforms from an XML-like syntax into native JavaScript. XML elements and
|
||||
attributes are transformed into function calls and objects, respectively.
|
||||
|
||||
```javascript
|
||||
var Nav;
|
||||
// Input (JSX):
|
||||
var app = <Nav color="blue" />;
|
||||
// Output (JS):
|
||||
var app = Nav({color:'blue'}, null);
|
||||
var app = Nav({color:"blue"});
|
||||
```
|
||||
|
||||
Notice that in order to use `<Nav />`, the `Nav` variable must be in scope.
|
||||
@@ -49,7 +49,7 @@ var Nav, Profile;
|
||||
// Input (JSX):
|
||||
var app = <Nav color="blue"><Profile>click</Profile></Nav>;
|
||||
// Output (JS):
|
||||
var app = Nav({color:'blue'}, Profile(null, 'click'));
|
||||
var app = Nav({color:"blue"}, Profile(null, "click"));
|
||||
```
|
||||
|
||||
Use the [JSX Compiler](/react/jsx-compiler.html) to try out JSX and see how it
|
||||
@@ -63,16 +63,17 @@ how to setup compilation.
|
||||
> Details about the code transform are given here to increase understanding, but
|
||||
> your code should not rely on these implementation details.
|
||||
|
||||
|
||||
## React and JSX
|
||||
|
||||
React and JSX are independent technologies, but JSX was primarily built with
|
||||
React in mind. The two valid uses of JSX are:
|
||||
|
||||
- To construct instances of React DOM components (`React.DOM.*`).
|
||||
- To construct instances of composite components created with
|
||||
* To construct instances of React DOM components (`React.DOM.*`).
|
||||
* To construct instances of composite components created with
|
||||
`React.createClass()`.
|
||||
|
||||
**React DOM Components**
|
||||
### React DOM Components
|
||||
|
||||
To construct a `<div>` is to create a variable that refers to `React.DOM.div`.
|
||||
|
||||
@@ -81,7 +82,7 @@ var div = React.DOM.div;
|
||||
var app = <div className="appClass">Hello, React!</div>;
|
||||
```
|
||||
|
||||
**React Component Components**
|
||||
### React Component Components
|
||||
|
||||
To construct an instance of a composite component, create a variable that
|
||||
references the class.
|
||||
@@ -114,7 +115,7 @@ var Nav;
|
||||
// Input (JSX):
|
||||
var tree = <Nav><span /></Nav>;
|
||||
// Output (JS):
|
||||
var tree = Nav(null, React.DOM.span(null, null));
|
||||
var tree = Nav(null, React.DOM.span(null));
|
||||
```
|
||||
|
||||
> Remember:
|
||||
@@ -125,7 +126,7 @@ var tree = Nav(null, React.DOM.span(null, null));
|
||||
|
||||
## JavaScript Expressions
|
||||
|
||||
#### Attribute Expressions
|
||||
### Attribute Expressions
|
||||
|
||||
To use a JavaScript expression as an attribute value, wrap the expression in a
|
||||
pair of curly braces (`{}`) instead of quotes (`""`).
|
||||
@@ -137,7 +138,7 @@ var person = <Person name={window.isLoggedIn ? window.name : ''} />;
|
||||
var person = Person({name: window.isLoggedIn ? window.name : ''});
|
||||
```
|
||||
|
||||
#### Child Expressions
|
||||
### Child Expressions
|
||||
|
||||
Likewise, JavaScript expressions may be used to express children:
|
||||
|
||||
@@ -145,16 +146,24 @@ Likewise, JavaScript expressions may be used to express children:
|
||||
// Input (JSX):
|
||||
var content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
|
||||
// Output (JS):
|
||||
var content = Container(null, window.isLoggedIn ? Nav(null, null) : Login(null, null));
|
||||
var content = Container(null, window.isLoggedIn ? Nav(null) : Login(null));
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
It's easy to add comments within your JSX; they're just JS expressions:
|
||||
|
||||
```javascript
|
||||
var content = <Container>{/* this is a comment */}<Nav /></Container>;
|
||||
```
|
||||
|
||||
## Tooling
|
||||
|
||||
Beyond the compilation step, JSX does not require any special tools.
|
||||
|
||||
- Many editors already include reasonable support for JSX (Vim, Emacs js2-mode).
|
||||
- Linting provides accurate line numbers after compiling without sourcemaps.
|
||||
- Elements use standard scoping so linters can find usage of out-of-scope
|
||||
* Many editors already include reasonable support for JSX (Vim, Emacs js2-mode).
|
||||
* Linting provides accurate line numbers after compiling without sourcemaps.
|
||||
* Elements use standard scoping so linters can find usage of out-of-scope
|
||||
components.
|
||||
|
||||
## Prior Work
|
||||
@@ -163,6 +172,8 @@ JSX is similar to several other JavaScript embedded XML language
|
||||
proposals/projects. Some of the features of JSX that distinguish it from similar
|
||||
efforts include:
|
||||
|
||||
- JSX is a simple syntactic transform.
|
||||
- JSX neither provides nor requires a runtime library.
|
||||
- JSX does not alter or add to the semantics of JavaScript.
|
||||
* JSX is a simple syntactic transform.
|
||||
* JSX neither provides nor requires a runtime library.
|
||||
* JSX does not alter or add to the semantics of JavaScript.
|
||||
|
||||
JSX is similar to HTML, but not exactly the same. See [JSX gotchas](./jsx-gotchas.html) for some key differences.
|
||||
@@ -1,14 +1,16 @@
|
||||
---
|
||||
id: docs-jsx-is-not-html
|
||||
title: JSX is not HTML
|
||||
description: Differences between JSX and HTML.
|
||||
id: jsx-gotchas
|
||||
title: JSX Gotchas
|
||||
layout: docs
|
||||
prev: api.html
|
||||
permalink: jsx-gotchas.html
|
||||
prev: jsx-in-depth.html
|
||||
next: interactivity-and-dynamic-uis.html
|
||||
---
|
||||
|
||||
JSX looks like HTML but there are some important differences you may run into.
|
||||
|
||||
## Whitespace removal
|
||||
|
||||
## Whitespace Removal
|
||||
|
||||
JSX doesn't follow the same whitespace elimination rules as HTML. JSX removes all whitespace between two curly braces expressions. If you want to have whitespace, simply add `{' '}`.
|
||||
|
||||
@@ -18,6 +20,7 @@ JSX doesn't follow the same whitespace elimination rules as HTML. JSX removes al
|
||||
|
||||
Follow [Issue #65](https://github.com/facebook/react/issues/65) for discussion on this behavior.
|
||||
|
||||
|
||||
## HTML Entities
|
||||
|
||||
You can insert HTML entities within literal text in JSX:
|
||||
@@ -58,27 +61,6 @@ As a last resort, you always have the ability to insert raw HTML.
|
||||
<div dangerouslySetInnerHTML={{'{{'}}__html: 'First · Second'}} />
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
JSX supports both single-line and multi-line JavaScript comments within a tag declaration:
|
||||
|
||||
```javascript
|
||||
<div // This is a single-line comment:
|
||||
/*
|
||||
And a multi-line
|
||||
comment
|
||||
*/
|
||||
/>
|
||||
```
|
||||
|
||||
As of React 0.3, there is no good way to insert comments within the children section. [Issue #82](https://github.com/facebook/react/issues/82) is tracking progress to enable the following:
|
||||
|
||||
```javascript
|
||||
// Note: This is not implemented yet!
|
||||
<div>
|
||||
{/* This is a comment */}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Custom HTML Attributes
|
||||
|
||||
89
docs/docs/03-interactivity-and-dynamic-uis.md
Normal file
89
docs/docs/03-interactivity-and-dynamic-uis.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: interactivity-and-dynamic-uis
|
||||
title: Interactivity and Dynamic UIs
|
||||
layout: docs
|
||||
permalink: interactivity-and-dynamic-uis.html
|
||||
prev: jsx-gotchas.html
|
||||
next: multiple-components.html
|
||||
---
|
||||
|
||||
You've already [learned how to display data](./displaying-data.html) with React. Now let's look at how to make our UIs interactive.
|
||||
|
||||
|
||||
## A Simple Example
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var LikeButton = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {liked: false};
|
||||
},
|
||||
handleClick: function(event) {
|
||||
this.setState({liked: !this.state.liked});
|
||||
},
|
||||
render: function() {
|
||||
var text = this.state.liked ? 'like' : 'unlike';
|
||||
return (
|
||||
<p onClick={this.handleClick}>
|
||||
You {text} this. Click to toggle.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
<LikeButton />,
|
||||
document.getElementById('example')
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Event Handling and Synthetic Events
|
||||
|
||||
With React you simply pass your event handler as a camelCased prop similar to how you'd do it in normal HTML. React ensures that all events behave identically in IE8 and above by implementing a synthetic event system. That is, React knows how to bubble and capture events according to the spec, and the events passed to your event handler are guaranteed to be consistent with [the W3C spec](http://www.w3.org/TR/DOM-Level-3-Events/), regardless of which browser you're using.
|
||||
|
||||
If you'd like to use React on a touch device (i.e. a phone or tablet), simply call `React.initializeTouchEvents(true);` to turn them on.
|
||||
|
||||
|
||||
## Under the Hood: autoBind and Event Delegation
|
||||
|
||||
Under the hood React does a few things to keep your code performant and easy to understand.
|
||||
|
||||
**Autobinding:** When creating callbacks in JavaScript you usually need to explicitly bind a method to its instance such that the value of `this` is correct. With React, every method is automatically bound to its component instance. React caches the bound method such that it's extremely CPU and memory efficient. It's also less typing!
|
||||
|
||||
**Event delegation:** React doesn't actually attach event handlers to the nodes themselves. When React starts up, it starts listening for all events at the top level using a single event listener. When a component is mounted or unmounted, the event handlers are simply added or removed from an internal mapping. When an event occurs, React knows how to dispatch it using this mapping. When there are no event handlers left in the mapping, React's event handlers are simple no-ops. To learn more about why this is fast, see [David Walsh's excellent blog post](http://davidwalsh.name/event-delegate).
|
||||
|
||||
|
||||
## Components are Just State Machines
|
||||
|
||||
React thinks of UIs as simple state machines. By thinking of a UI as being in various states and rendering those states, it's easy to keep your UI consistent.
|
||||
|
||||
In React, you simply update a component's state, and then render a new UI based on this new state. React takes care of updating the DOM for you in the most efficient way.
|
||||
|
||||
|
||||
## How State Works
|
||||
|
||||
A common way to inform React of a data change is by calling `setState(data, callback)`. This method merges `data` into `this.state` and re-renders the component. When the component finishes re-rendering, the optional `callback` is called. Most of the time you'll never need to provide a `callback` since React will take care of keeping your UI up-to-date for you.
|
||||
|
||||
|
||||
## What Components Should Have State?
|
||||
|
||||
Most of your components should simply take some data from `props` and render it. However, sometimes you need to respond to user input, a server request or the passage of time. For this you use state.
|
||||
|
||||
**Try to keep as many of your components as possible stateless.** By doing this you'll isolate the state to its most logical place and minimize redundancy, making it easier to reason about your application.
|
||||
|
||||
A common pattern is to create several stateless components that just render data, and have a stateful component above them in the hierarchy that passes its state to its children via `props`. The stateful component encapsulates all of the interaction logic, while the stateless components take care of rendering data in a declarative way.
|
||||
|
||||
|
||||
## What *Should* Go in State?
|
||||
|
||||
**State should contain data that a component's event handlers may change to trigger a UI update.** In real apps this data tends to be very small and JSON-serializable. When building a stateful component, think about the minimal possible representation of its state, and only store those properties in `this.state`. Inside of `render()` simply compute any other information you need based on this state. You'll find that thinking about and writing applications in this way tends to lead to the most correct application, since adding redundant or computed values to state means that you need to explicitly keep them in sync rather than rely on React computing them for you.
|
||||
|
||||
## What *Shouldn't* Go in State?
|
||||
|
||||
`this.state` should only contain the minimal amount of data needed to represent your UI's state. As such, it should not contain:
|
||||
|
||||
* **Computed data:** Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within `render()`. For example, if you have an array of list items in state and you want to render the count as a string, simply render `this.state.listItems.length + ' list items'` in your `render()` method rather than storing it on state.
|
||||
* **React components:** Build them in `render()` based on underlying props and state.
|
||||
* **Duplicated data from props:** Try to use props as the source of truth where possible. Because props can change over time, it's appropriate to store props in state to be able to know its previous values.
|
||||
151
docs/docs/04-multiple-components.md
Normal file
151
docs/docs/04-multiple-components.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
id: multiple-components
|
||||
title: Multiple Components
|
||||
layout: docs
|
||||
permalink: multiple-components.html
|
||||
prev: interactivity-and-dynamic-uis.html
|
||||
next: reusable-components.html
|
||||
---
|
||||
|
||||
So far, we've looked at how to write a single component to display data and handle user input. Next let's examine one of React's finest features: composability.
|
||||
|
||||
|
||||
## Motivation: Separation of Concerns
|
||||
|
||||
By building modular components that reuse other components with well-defined interfaces, you get much of the same benefits that you get by using functions or classes. Specifically you can *separate the different concerns* of your app however you please simply by building new components. By building a custom component library for your application, you are expressing your UI in a way that best fits your domain.
|
||||
|
||||
|
||||
## Composition Example
|
||||
|
||||
Let's create a simple Avatar component which shows a profile picture and username using the Facebook Graph API.
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var Avatar = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<ProfilePic username={this.props.username} />
|
||||
<ProfileLink username={this.props.username} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ProfilePic = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<img src={'http://graph.facebook.com/' + this.props.username + '/picture'} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ProfileLink = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<a href={'http://www.facebook.com/' + this.props.username}>
|
||||
{this.props.username}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
<Avatar username="pwh" />,
|
||||
document.getElementById('example')
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Ownership
|
||||
|
||||
In the above example, instances of `Avatar` *own* instances of `ProfilePic` and `ProfileLink`. In React, **an owner is the component that sets the `props` of other components**. More formally, if a component `X` is created in component `Y`'s `render()` method, it is said that `X` is *owned by* `Y`. As discussed earlier, a component cannot mutate its `props` — they are always consistent with what its owner sets them to. This key property leads to UIs that are guaranteed to be consistent.
|
||||
|
||||
It's important to draw a distinciton between the owner-ownee relationship and the parent-child relationship. The owner-ownee relationship is specific to React, while the parent-child relationship is simply the one you know and love from the DOM. In the example above, `Avatar` owns the `div`, `ProfilePic` and `ProfileLink` instances, and `div` is the **parent** (but not owner) of the `ProfilePic` and `ProfileLink` instances.
|
||||
|
||||
|
||||
## Children
|
||||
|
||||
When you create a React component instance, you can include additional React components or JavaScript expressions between the opening and closing tags like this:
|
||||
|
||||
```javascript
|
||||
<Parent><Child /></Parent>
|
||||
```
|
||||
|
||||
`Parent` can read its children by accessing the special `this.props.children` prop.
|
||||
|
||||
|
||||
### Child Reconciliation
|
||||
|
||||
**Reconciliation is the process by which React updates the DOM with each new render pass.** In general, children are reconciled according to the order in which they are rendered. For example, suppose two render passes generate the following respective markup:
|
||||
|
||||
```html
|
||||
// Render Pass 1
|
||||
<Card>
|
||||
<p>Paragraph 1</p>
|
||||
<p>Paragraph 2</p>
|
||||
</Card>
|
||||
// Render Pass 2
|
||||
<Card>
|
||||
<p>Paragraph 2</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
Intuitively, `<p>Paragraph 1</p>` was removed. Instead, React will reconcile the DOM by changing the text content of the first child and destroying the last child. React reconciles according to the *order* of the children.
|
||||
|
||||
|
||||
### Stateful Children
|
||||
|
||||
For most components, this is not a big deal. However, for stateful components that maintain data in `this.state` across render passes, this can be very problematic.
|
||||
|
||||
In most cases, this can be sidestepped by hiding elements instead of destroying them:
|
||||
|
||||
```html
|
||||
// Render Pass 1
|
||||
<Card>
|
||||
<p>Paragraph 1</p>
|
||||
<p>Paragraph 2</p>
|
||||
</Card>
|
||||
// Render Pass 2
|
||||
<Card>
|
||||
<p style={{'{{'}}display: 'none'}}>Paragraph 1</p>
|
||||
<p>Paragraph 2</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
|
||||
### Dynamic Children
|
||||
|
||||
The situation gets more complicated when the children are shuffled around (as in search results) or if new components are added onto the front of the list (as in streams). In these cases where the identity and state of each child must be maintained across render passes, you can uniquely identify each child by assigning it a `key`:
|
||||
|
||||
```javascript
|
||||
render: function() {
|
||||
var results = this.props.results;
|
||||
return (
|
||||
<ol>
|
||||
{this.results.map(function(result) {
|
||||
return <li key={result.id}>{result.text}</li>;
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
When React reconciles the keyed children, it will ensure that any child with `key` will be reordered (instead of clobbered) or destroyed (instead of reused).
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
In React, data flows from owner to owned component through `props` as discussed above. This is effectively one-way data binding: owners bind their owned component's props to some value the owner has computed based on its `props` or `state`. Since this process happens recursively, data changes are automatically reflected everywhere they are used.
|
||||
|
||||
|
||||
## A Note on Performance
|
||||
|
||||
You may be thinking that it's expensive to react to changing data if there are a large number of nodes under an owner. The good news is that JavaScript is fast and `render()` methods tend to be quite simple, so in most applications this is extremely fast. Additionally, the bottleneck is almost always the DOM mutation and not JS execution and React will optimize this for you using batching and change detection.
|
||||
|
||||
However, sometimes you really want to have fine-grained control over your performance. In that case, simply override `shouldComponentUpdate()` to return false when you want React to skip processing of a subtree. See [the React reference docs](./reference.html) for more information.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> If `shouldComponentUpdate()` returns false when data has actually changed, React can't keep your UI in sync. Be sure you know what you're doing while using it, and only use this function when you have a noticeable performance problem. Don't underestimate how fast JavaScript is relative to the DOM.
|
||||
143
docs/docs/05-reusable-components.md
Normal file
143
docs/docs/05-reusable-components.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
id: reusable-components
|
||||
title: Reusable Components
|
||||
layout: docs
|
||||
permalink: reusable-components.html
|
||||
prev: multiple-components.html
|
||||
next: forms.html
|
||||
---
|
||||
|
||||
When designing interfaces, break down the common design elements (buttons, form fields, layout components, etc) into reusable components with well-defined interfaces. That way, the next time you need to build some UI you can write much less code, which means faster development time, less bugs, and less bytes down the wire.
|
||||
|
||||
|
||||
## Prop Validation
|
||||
|
||||
As your app grows it's helpful to ensure that your components are used correctly. We do this by allowing you to specify `propTypes`. `React.PropTypes` exports a range of validators that can be used to make sure the data you receive is valid. When an invalid value is provided for a prop, an error will be thrown. Here is an example documenting the different validators provided:
|
||||
|
||||
```javascript
|
||||
React.createClass({
|
||||
propTypes: {
|
||||
// You can declare that a prop is a specific JS primitive. By default, these
|
||||
// are all optional.
|
||||
optionalArray: React.PropTypes.array,
|
||||
optionalBool: React.PropTypes.bool,
|
||||
optionalFunc: React.PropTypes.func,
|
||||
optionalNumber: React.PropTypes.number,
|
||||
optionalObject: React.PropTypes.object,
|
||||
optionalString: React.PropTypes.string,
|
||||
|
||||
// You can ensure that your prop is limited to specific values by treating
|
||||
// it as an enum.
|
||||
optionalEnum: React.PropTypes.oneOf(['News','Photos']),
|
||||
|
||||
// You can also declare that a prop is an instance of a class. This uses
|
||||
// JS's instanceof operator.
|
||||
someClass: React.PropTypes.instanceOf(SomeClass),
|
||||
|
||||
// You can chain any of the above with isRequired to make sure an error is
|
||||
// thrown if the prop isn't provide.
|
||||
requiredFunc: React.PropTypes.func.isRequired
|
||||
|
||||
// You can also specify a custom validator.
|
||||
customProp: function(props, propName, componentName) {
|
||||
if (!/matchme/.test(props[propName])) {
|
||||
throw new Error('Validation failed!')
|
||||
}
|
||||
}
|
||||
},
|
||||
/* ... */
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Default Prop Values
|
||||
|
||||
React lets you define default values for your `props` in a very declarative way:
|
||||
|
||||
```javascript
|
||||
var ComponentWithDefaultProps = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
value: 'default value'
|
||||
};
|
||||
}
|
||||
/* ... */
|
||||
});
|
||||
```
|
||||
|
||||
The result of `getDefaultProps()` will be cached and used to ensure that `this.props.value` will have a value if it was not specified by the parent component. This allows you to safely just use your props without having to write repetitive and fragile code to handle that yourself.
|
||||
|
||||
|
||||
## Transferring Props: A Shortcut
|
||||
|
||||
A common type of React component is one that extends a basic HTML in a simple way. Often you'll want to copy any HTML attributes passed to your component to the underlying HTML element to save typing. React provides `transferPropsTo()` to do just this.
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var CheckLink = React.createClass({
|
||||
render: function() {
|
||||
// transferPropsTo() will take any props pased to CheckLink
|
||||
// and copy them to <a>
|
||||
return this.transferPropsTo(<a>{'√ '}{this.props.children}</a>);
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
<CheckLink href="javascript:alert('Hello, world!');">
|
||||
Click here!
|
||||
</CheckLink>,
|
||||
document.getElementById('example')
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Mixins
|
||||
|
||||
Components are the best way to reuse code in React, but sometimes very different components may share some common functionality. These are sometimes called [cross-cutting concerns](http://en.wikipedia.org/wiki/Cross-cutting_concern). React provides `mixins` to solve this problem.
|
||||
|
||||
One common use case is a component wanting to update itself on a time interval. It's easy to use `setInterval()`, but it's important to cancel your interval when you don't need it anymore to save memory. React provides [lifecycle methods](./working-with-the-browser.html) that let you know when a component is about to be created or destroyed. Let's create a simple mixin that uses these methods to provide an easy `setInterval()` function that will automatically get cleaned up when your component is destroyed.
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var SetIntervalMixin = {
|
||||
componentWillMount: function() {
|
||||
this.intervals = [];
|
||||
},
|
||||
setInterval: function() {
|
||||
this.intervals.push(setInterval.apply(null, arguments));
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
this.intervals.map(clearInterval);
|
||||
}
|
||||
};
|
||||
|
||||
var TickTock = React.createClass({
|
||||
mixins: [SetIntervalMixin], // Use the mixin
|
||||
getInitialState: function() {
|
||||
return {seconds: 0};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.setInterval(this.tick, 1000); // Call a method on the mixin
|
||||
},
|
||||
tick: function() {
|
||||
this.setState({seconds: this.state.seconds + 1});
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<p>
|
||||
React has been running for {this.state.seconds} seconds.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
<TickTock />,
|
||||
document.getElementById('example')
|
||||
);
|
||||
```
|
||||
|
||||
A nice feature of mixins is that if a component is using multiple mixins and several mixins define the same lifecycle method (i.e. several mixins want to do some cleanup when the component is destroyed), all of the lifecycle methods are guaranteed to be called.
|
||||
|
||||
132
docs/docs/06-forms.md
Normal file
132
docs/docs/06-forms.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: forms
|
||||
title: Forms
|
||||
layout: docs
|
||||
permalink: forms.html
|
||||
prev: reusable-components.html
|
||||
next: working-with-the-browser.html
|
||||
---
|
||||
|
||||
Form components such as `<input>`, `<textarea>`, and `<option>` differ from other native components because they can be mutated via user interactions. These components provide interfaces that make it easier to manage forms in response to user interactions.
|
||||
|
||||
|
||||
## Interactive Props
|
||||
|
||||
Form components support a few props that are affected via user interactions:
|
||||
|
||||
* `value`, supported by `<input>` and `<textarea>` components.
|
||||
* `checked`, supported by `<input>` components of type `checkbox` or `radio`.
|
||||
* `selected`, supported by `<option>` components.
|
||||
|
||||
In HTML, the value of `<textarea>` is set via children. In React, you should use `value` instead.
|
||||
|
||||
Form components allow listening for changes by setting a callback to the `onChange` prop. The `onChange` prop works across browsers to fire in response to user interactions when:
|
||||
|
||||
* The `value` of `<input>` or `<textarea>` changes.
|
||||
* The `checked` state of `<input>` changes.
|
||||
* The `selected` state of `<option>` changes.
|
||||
|
||||
Like all DOM events, the `onChange` prop is supported on all native components and can be used to listen to bubbled change events.
|
||||
|
||||
|
||||
## Controlled Components
|
||||
|
||||
An `<input>` with `value` set is a *controlled* component. In a controlled `<input>`, the value of the rendered element will always reflect the `value` prop. For example:
|
||||
|
||||
```javascript
|
||||
render: function() {
|
||||
return <input type="text" value="Hello!" />;
|
||||
}
|
||||
```
|
||||
|
||||
This will render an input that always has a value of `Hello!`. Any user input will have no effect on the rendered element because React has declared the value to be `Hello!`. If you wanted to update the value in response to user input, you could use the `onChange` event:
|
||||
|
||||
```javascript
|
||||
getInitialState: function() {
|
||||
return {value: 'Hello!'};
|
||||
},
|
||||
handleChange: function(event) {
|
||||
this.setState({value: event.target.value});
|
||||
},
|
||||
render: function() {
|
||||
var value = this.state.value;
|
||||
return <input type="text" value={value} onChange={this.handleChange} />;
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we are simply accepting the newest value provided by the user and updating the `value` prop of the `<input>` component. This pattern makes it easy to implement interfaces that respond to or validate user interactions. For example:
|
||||
|
||||
```javascript
|
||||
handleChange: function(event) {
|
||||
this.setState({value: event.target.value.substr(0, 140)});
|
||||
}
|
||||
```
|
||||
|
||||
This would accept user input but truncate the value to the first 140 characters.
|
||||
|
||||
|
||||
## Uncontrolled Components
|
||||
|
||||
An `<input>` that does not supply a `value` (or sets it to `null`) is an *uncontrolled* component. In an uncontrolled `<input>`, the value of the rendered element will reflect the user's input. For example:
|
||||
|
||||
```javascript
|
||||
render: function() {
|
||||
return <input type="text" />;
|
||||
}
|
||||
```
|
||||
|
||||
This will render an input that starts off with an empty value. Any user input will be immediately reflected by the rendered element. If you wanted to listen to updates to the value, you could use the `onChange` event just like you can with controlled components.
|
||||
|
||||
If you want to initialize the component with a non-empty value, you can supply a `defaultValue` prop. For example:
|
||||
|
||||
```javascript
|
||||
render: function() {
|
||||
return <input type="text" defaultValue="Hello!" />;
|
||||
}
|
||||
```
|
||||
|
||||
This example will function much like the **Controlled Components** example above.
|
||||
|
||||
Likewise, `<input>` supports `defaultChecked` and `<option>` supports `defaultSelected`.
|
||||
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
|
||||
### Why Controlled Components?
|
||||
|
||||
Using form components such as `<input>` in React presents a challenge that is absent when writing traditional form HTML. For example, in HTML:
|
||||
|
||||
```html
|
||||
<input type="text" name="title" value="Untitled" />
|
||||
```
|
||||
|
||||
This renders an input *initialized* with the value, `Untitled`. When the user updates the input, the node's value *property* will change. However, `node.getAttribute('value')` will still return the value used at initialization time, `Untitled`.
|
||||
|
||||
Unlike HTML, React components must represent the state of the view at any point in time and not only at initialization time. For example, in React:
|
||||
|
||||
```javascript
|
||||
render: function() {
|
||||
return <input type="text" name="title" value="Untitled" />;
|
||||
}
|
||||
```
|
||||
|
||||
Since this method describes the view at any point in time, the value of the text input should *always* be `Untitled`.
|
||||
|
||||
|
||||
### Why Textarea Value?
|
||||
|
||||
In HTML, the value of `<textarea>` is usually set using its children:
|
||||
|
||||
```html
|
||||
<!-- counterexample: DO NOT DO THIS! -->
|
||||
<textarea name="description">This is the description.</textarea>
|
||||
```
|
||||
|
||||
For HTML, this easily allows developers to supply multiline values. However, since React is JavaScript, we do not have string limitations and can use `\n` if we want newlines. In a world where we have `value` and `defaultValue`, it is ambiguous what role children play. For this reason, you should not use children when setting `<textarea>` values:
|
||||
|
||||
```javascript
|
||||
<textarea name="description" value="This is a description." />
|
||||
```
|
||||
|
||||
If you *do* decide to use children, they will behave like `defaultValue`.
|
||||
132
docs/docs/07-working-with-the-browser.md
Normal file
132
docs/docs/07-working-with-the-browser.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: working-with-the-browser
|
||||
title: Working With the Browser
|
||||
layout: docs
|
||||
permalink: working-with-the-browser.html
|
||||
prev: forms.html
|
||||
next: more-about-refs.html
|
||||
---
|
||||
|
||||
React provides powerful abstractions that free you from touching the DOM directly in most cases, but sometimes you simply need to access the underlying API, perhaps to work with a third-party library or existing code.
|
||||
|
||||
|
||||
## The Mock DOM
|
||||
|
||||
React is so fast because it never talks to the DOM directly. React maintains a fast in-memory representation of the DOM. `render()` methods return a *description* of the DOM, and React can diff this description with the in-memory representation to compute the fastest way to update the browser.
|
||||
|
||||
Additionally, React implements a full synthetic event system such that all event objects are guaranteed to conform to the W3C spec despite browser quirks, and everything bubbles consistently and in a performant way cross-browser. You can even use some HTML5 events in IE8!
|
||||
|
||||
Most of the time you should stay within React's "faked browser" world since it's more performant and easier to reason about. However, sometimes you simply need to access the underlying API, perhaps to work with a third-party library like a jQuery plugin. React provides escape hatches for you to use the underlying DOM API directly.
|
||||
|
||||
|
||||
## Refs and getDOMNode()
|
||||
|
||||
To interact with the browser, you'll need a reference to a DOM node. Every mounted React component has a `getDOMNode()` function which you can call to get a reference to it.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> `getDOMNode()` only works on mounted components (that is, components that have been placed in the DOM). If you try to call this on a component that has not been mounted yet (like calling `getDOMNode()` in `render()` on a component that has yet to be created) an exception will be thrown.
|
||||
|
||||
In order to get a reference to a React component, you can either use `this` to get the current React component, or you can use refs to refer to a component you own. They work like this:
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var MyComponent = React.createClass({
|
||||
handleClick: function() {
|
||||
// Explicitly focus the text input using the raw DOM API.
|
||||
this.refs.myTextInput.getDOMNode().focus();
|
||||
},
|
||||
render: function() {
|
||||
// The ref attribute adds a reference to the component to
|
||||
// this.refs when the component is mounted.
|
||||
return (
|
||||
<div>
|
||||
<input type="text" ref="myTextInput" />
|
||||
<input
|
||||
type="button"
|
||||
value="Focus the text input"
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
<MyComponent />,
|
||||
document.getElementById('example')
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## More About Refs
|
||||
|
||||
To learn more about refs, including ways to use them effectively, see our [more about refs](./more-about-refs.html) documentation.
|
||||
|
||||
|
||||
## Component Lifecycle
|
||||
|
||||
Components have three main parts of their lifecycle:
|
||||
|
||||
* **Mounting:** A component is being inserted into the DOM.
|
||||
* **Updating:** A component is being re-rendered to determine if the DOM should be updated.
|
||||
* **Unmounting:** A component is being removed from the DOM.
|
||||
|
||||
React provides lifecycle methods that you can specify to hook into this process. We provide **will** methods, which are called right before something happens, and **did** methods which are called right after something happens.
|
||||
|
||||
|
||||
### Mounting
|
||||
|
||||
* `getInitialState(): object` is invoked before a component is mounted. Stateful components should implement this and return the initial state data.
|
||||
* `componentWillMount()` is invoked immediately before mounting occurs.
|
||||
* `componentDidMount(DOMElement rootNode)` is invoked immediately after mounting occurs. Initialization that requires DOM nodes should go here.
|
||||
|
||||
|
||||
### Updating
|
||||
|
||||
* `componentWillReceiveProps(object nextProps)` is invoked when a mounted component receives new props. This method should be used to compare `this.props` and `nextProps` to perform state transitions using `this.setState()`.
|
||||
* `shouldComponentUpdate(object nextProps, object nextState): boolean` is invoked when a component decides whether any changes warrant an update to the DOM. Implement this as an optimization to compare `this.props` with `nextProps` and `this.state` with `nextState` and return false if React should skip updating.
|
||||
* `componentWillUpdate(object nextProps, object nextState)` is invoked immediately before updating occurs. You cannot call `this.setState()` here.
|
||||
* `componentDidUpdate(object prevProps, object prevState, DOMElement rootNode)` is invoked immediately after updating occurs.
|
||||
|
||||
|
||||
### Unmounting
|
||||
|
||||
* `componentWillUnmount()` is invoked immediately before a component is unmounted and destroyed. Cleanup should go here.
|
||||
|
||||
|
||||
### Mounted Methods
|
||||
|
||||
_Mounted_ composite components also support the following methods:
|
||||
|
||||
* `getDOMNode(): DOMElement` can be invoked on any mounted component in order to obtain a reference to its rendered DOM node.
|
||||
* `forceUpdate()` can be invoked on any mounted component when you know that some deeper aspect of the component's state has changed without using `this.setState()`.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> The `DOMElement rootNode` argument of `componentDidMount()` and
|
||||
> `componentDidUpdate()` is a convenience. The same node can be obtained by
|
||||
> calling `this.getDOMNode()`.
|
||||
|
||||
|
||||
## Browser Suppport and Polyfills
|
||||
|
||||
At Facebook, we support older browsers, including IE8. We've had polyfills in place for a long time to allow us to write forward-thinking JS. This means we don't have a bunch of hacks scattered throughout our codebase and we can still expect our code to "just work". For example, instead of seeing `+new Date()`, we can just write `Date.now()`. Since the open source React is the same as what we use internally, we've carried over this philosophy of using forward thinking JS.
|
||||
|
||||
In addition to that philosphy, we've also taken the stance that we, as authors of a JS library, should not be shipping polyfills as a part of our library. If every library did this, there's a good chance you'd be sending down the same polyfill multiple times, which could be a sizable chunk of dead code. If your product needs to support older browsers, chances are you're already using something like [es5-shim](https://github.com/kriskowal/es5-shim).
|
||||
|
||||
|
||||
### Polyfills Needed to Support Older Browsers
|
||||
|
||||
* `Array.isArray`
|
||||
* `Array.prototype.forEach`
|
||||
* `Array.prototype.indexOf`
|
||||
* `Function.prototype.bind`
|
||||
* `Date.now`
|
||||
* `Array.prototype.some` (also in `es5-shim.js`)
|
||||
|
||||
All of these can be polyfilled using `es5-shim.js` from [https://github.com/kriskowal/es5-shim](https://github.com/kriskowal/es5-shim).
|
||||
|
||||
* `console.*` - Only needed when not using the minified build. If you need to polyfill this, try [https://github.com/paulmillr/console-polyfill](https://github.com/paulmillr/console-polyfill).
|
||||
* `Object.create` - Provided in `es5-sham.js` @ [https://github.com/kriskowal/es5-shim](https://github.com/kriskowal/es5-shim).
|
||||
135
docs/docs/07.1-more-about-refs.md
Normal file
135
docs/docs/07.1-more-about-refs.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
id: more-about-refs
|
||||
title: More About Refs
|
||||
layout: docs
|
||||
permalink: more-about-refs.html
|
||||
prev: working-with-the-browser.html
|
||||
next: tooling-integration.html
|
||||
---
|
||||
After returning the structure of your UI from the render method, you may find yourself wanting to "reach out" and invoke methods on component instances returned from render. Often, doing something like this isn't necessary for making data flow through your application, because the Reactive data flow always ensures that the most recent `props` are sent to each child that is output from `render()`. However there are a few cases, where it still might be necessary or beneficial.
|
||||
|
||||
Consider the case when you wish to tell an `<input />` element (that exists within your instances sub-hierarchy) to focus after you update its value to be the empty string, `''`.
|
||||
|
||||
```javascript
|
||||
var App = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {userInput: ''};
|
||||
},
|
||||
handleKeyUp: function(e) {
|
||||
this.setState({userInput: e.target.value});
|
||||
},
|
||||
clearAndFocusInput: function() {
|
||||
this.setState({userInput: ''}); // Clear the input
|
||||
// We wish to focus the <input /> now!
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={this.clearAndFocusInput}>
|
||||
Click To Focus and Reset
|
||||
</div>
|
||||
<input
|
||||
value={this.state.userInput}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Notice how, in this example, we want to "tell" the input something - something that it cannot infer from it's props over time. In this case we want to "tell" it that it should now become focused. However, there are some challenges. What is returned from `render()`` is not your actual composition of "child" components, it is merely a *description* of the children at a particular instance in time - a snapshot, if you will.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> Remember, what you return from `render()` is not your *actual* rendered children instances. What you return from `render()` is merely a *description* of the children instances in your component's sub-hierarchy at a particular moment in time.
|
||||
|
||||
|
||||
This means that you should never "hold onto" something that you return from `render()` and then expect it to be anything meaningful.
|
||||
|
||||
```javascript
|
||||
// counterexample: DO NOT DO THIS!
|
||||
render: function() {
|
||||
var myInput = <input />; // I'm going to try to call methods on this
|
||||
this.rememberThisInput = myInput; // input at some point in the future! YAY!
|
||||
return (
|
||||
<div>
|
||||
<div>...</div>
|
||||
{myInput}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
In this counterexample, the `<input />` is merely a *description* of an `<input />`. This description is used to create a *real* **backing instance** for the `<input />`.
|
||||
|
||||
So how do we talk to the *real* backing instance of the input?
|
||||
|
||||
## The ref Attribute
|
||||
|
||||
React supports a very special property that you can attach to any component that is output from `render()`. This special property allows you to refer to the corresponding **backing instance** of anything returned from `render()`. It is always guaranteed to be the proper instance, at any point in time.
|
||||
|
||||
It's as simple as:
|
||||
|
||||
**1.** Assign a `ref` attribute to anything returned from `render` such as:
|
||||
|
||||
```html
|
||||
<input ref="myInput" />
|
||||
```
|
||||
|
||||
**2.** In some other code (typically event handler code), access the **backing instance** via `this.refs` as in:
|
||||
|
||||
```javascript
|
||||
this.refs.myInput
|
||||
```
|
||||
|
||||
## Completing the Example
|
||||
|
||||
```javascript
|
||||
var App = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {userInput: ''};
|
||||
},
|
||||
handleKeyUp: function(e) {
|
||||
this.setState({userInput: e.target.value});
|
||||
},
|
||||
clearAndFocusInput: function() {
|
||||
this.setState({userInput: ''}); // Clear the input
|
||||
this.refs.theInput.getDOMNode().focus(); // Boom! Focused!
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={this.clearAndFocusInput}>
|
||||
Click To Focus and Reset
|
||||
</div>
|
||||
<input
|
||||
ref="theInput"
|
||||
value={this.state.userInput}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
In this example, our render function returns a description of an `<input />` instance. But the true instance is accessed via `this.refs.theInput`. As long as a child component with `ref="theInput"` is returned from render, `this.refs.theInput` will access the the proper instance. This even works on higher level (non-DOM) components such as `<Typeahead ref="myTypeahead" />`.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
Refs are a great way to send a message to a particular child instance in a way that would be inconvenient to do via streaming Reactive `props` and `state`. They should, however, not be your go-to abstraction for flowing data through your application. By default, use the Reactive data flow and save `ref`s for use cases that are inherently non-reactive.
|
||||
|
||||
### Benefits:
|
||||
|
||||
- You can define any public method on your component classes (such as a reset method on a Typeahead) and call those public methods through refs (such as `this.refs.myTypeahead.reset()`).
|
||||
- Performing DOM measurements almost always requires reaching out to a "native" component such as `<input />` and accessing its underlying DOM node via `this.refs.myInput.getDOMNode()`. Refs are one of the only practical ways of doing this reliably.
|
||||
- Refs are automatically book-kept for you! If that child is destroyed, its ref is also destroyed for you. No worrying about memory here (unless you do something crazy to retain a reference yourself).
|
||||
|
||||
### Cautions:
|
||||
|
||||
- *Never* access refs inside of any component's render method - or while any component's render method is even running anywhere in the call stack.
|
||||
- If you want to preserve Google Closure Compiler Crushing resilience, make sure to never access as a property what was specified as a string. This means you must access using `this.refs['myRefString']` if your ref was defined as `ref="myRefString"`.
|
||||
- If you have not programmed several apps with React, your first inclination is usually going to be to try to use refs to "make things happen" in your app. If this is the case, take a moment and think more critically about where `state` should be owned in the component hierarchy. Often, it becomes clear that the proper place to "own" that state is at a higher level in the hierarchy. Placing the state there often eliminates any desire to use `ref`s to "make things happen" - instead, the data flow will usually accomplish your goal.
|
||||
44
docs/docs/08-tooling-integration.md
Normal file
44
docs/docs/08-tooling-integration.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: tooling-integration
|
||||
title: Tooling integration
|
||||
layout: docs
|
||||
permalink: tooling-integration.html
|
||||
prev: more-about-refs.html
|
||||
next: reference.html
|
||||
---
|
||||
|
||||
Every project uses a different system for building and deploying JavaScript. We've tried to make React as environment-agnostic as possible.
|
||||
|
||||
|
||||
## CDN-hosted React
|
||||
|
||||
We provide CDN-hosted versions of React [on our download page](/react/downloads.html). These prebuilt files use the UMD module format. Dropping them in with a simple `<script>` tag will inject a `React` global into your environment. It should also work out-of-the-box in CommonJS and AMD environments.
|
||||
|
||||
|
||||
## Using master
|
||||
|
||||
We have instructions for building from `master` [in our GitHub repository](https://github.com/facebook/react). We build a tree of CommonJS modules under `build/modules` which you can drop into any environment or packaging tool that supports CommonJS.
|
||||
|
||||
|
||||
## In-browser JSX Transform
|
||||
|
||||
If you like using JSX, we provide an in-browser JSX transformer for development [on our download page](/react/downloads.html). Simply include a `<script type="text/jsx">` tag to engage the JSX transformer. Be sure to include the `/** @jsx React.DOM */` comment as well, otherwise the transformer will not run the transforms.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> The in-browser JSX transformer is fairly large and results in extraneous computation client-side that can be avoided. Do not use it in production — see the next section.
|
||||
|
||||
|
||||
## Productionizing: Precompiled JSX
|
||||
|
||||
If you have [npm](http://npmjs.org/), you can simply run `npm install -g react-tools` to install our command-line `jsx` tool. This tool will translate files that use JSX syntax to plain JavaScript files that can run directly in the browser. It will also watch directories for you and automatically transform files when they are changed; for example: `jsx --watch src/ build/`. Run `jsx --help` for more information on how to use this tool.
|
||||
|
||||
|
||||
## Helpful Open-Source Projects
|
||||
|
||||
The open-source community has built tools that integrate JSX with several build systems.
|
||||
|
||||
* [reactify](https://github.com/andreypopp/reactify) - use JSX with [browserify](http://browserify.org/).
|
||||
* [grunt-react](https://github.com/ericclemmons/grunt-react) - [grunt](http://gruntjs.com/) task for JSX
|
||||
* [require-jsx](https://github.com/seiffert/require-jsx) - use JSX with [require.js](http://requirejs.org/)
|
||||
* [reactapp](https://github.com/jordwalke/reactapp) - a sample project to get up-and-running with React quickly
|
||||
209
docs/docs/09-reference.md
Normal file
209
docs/docs/09-reference.md
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
id: reference
|
||||
title: Reference
|
||||
layout: docs
|
||||
permalink: reference.html
|
||||
prev: tooling-integration.html
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
### Production Apps
|
||||
|
||||
* All of [Instagram.com](http://instagram.com/) is built on React.
|
||||
* Many components on [Facebook.com](http://www.facebook.com/), including the commenting interface, ads creation flows, and page insights.
|
||||
* [Khan Academy](http://khanacademy.org/) is using React for its question editor.
|
||||
|
||||
|
||||
### Sample Code
|
||||
|
||||
* We've included [a step-by-step comment box tutorial](./tutorial.html).
|
||||
* [The React starter kit](/react/downloads.html) includes several examples which you can [view online in our GitHub repository](https://github.com/facebook/react/tree/master/examples/).
|
||||
* [reactapp](https://github.com/jordwalke/reactapp) is a simple app template to get you up-and-running quickly with React.
|
||||
* [React one-hour email](https://github.com/petehunt/react-one-hour-email/commits/master) goes step-by-step from a static HTML mock to an interactive email reader (written in just one hour!)
|
||||
* [Rendr + React app template](https://github.com/petehunt/rendr-react-template/) demonstrates how to use React's server rendering capabilities.
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### React
|
||||
|
||||
`React` is the entry point to the React framework. If you're using one of the prebuilt packages it's available as a global; if you're using CommonJS modules you can `require()` it.
|
||||
|
||||
|
||||
#### React.DOM
|
||||
|
||||
`React.DOM` provides all of the standard HTML tags needed to build a React app. You generally don't use it directly; instead, just include it as part of the `/** @jsx React.DOM */` docblock.
|
||||
|
||||
|
||||
#### React.initializeTouchEvents
|
||||
|
||||
```javascript
|
||||
initializeTouchEvents(boolean shouldUseTouch)
|
||||
```
|
||||
|
||||
Configure React's event system to handle touch events on mobile devices.
|
||||
|
||||
|
||||
#### React.createClass
|
||||
|
||||
```javascript
|
||||
function createClass(object specification)
|
||||
```
|
||||
|
||||
Creates a component given a specification. A component implements a `render` method which returns **one single** child. That child may have an arbitrarily deep child structure. One thing that makes components different than a standard prototypal classes is that you don't need to call new on them. They are convenience wrappers that construct backing instances (via new) for you.
|
||||
|
||||
|
||||
#### React.renderComponent
|
||||
|
||||
```javascript
|
||||
ReactComponent renderComponent(ReactComponent container, DOMElement container)
|
||||
```
|
||||
|
||||
Renders a React component into the DOM in the supplied `container`.
|
||||
|
||||
If the React component was previously rendered into `container`, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React component.
|
||||
|
||||
|
||||
#### React.unmountAndReleaseReactRootNode
|
||||
|
||||
```javascript
|
||||
unmountAndReleaseReactRootNode(DOMElement container)
|
||||
```
|
||||
|
||||
Remove a mounted React component from the DOM and clean up its event handlers and state.
|
||||
|
||||
|
||||
#### React.renderComponentToString
|
||||
|
||||
```javascript
|
||||
renderComponentToString(ReactComponent component, function callback)
|
||||
```
|
||||
|
||||
Render a component to its initial HTML. This should only be used on the server. React will call `callback` with an HTML string when the markup is ready. You can use this method to create static site generators, or you can generate HTML on the server and send it down to have a very fast initial page load. If you call `React.renderComponent()` on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.
|
||||
|
||||
|
||||
### AbstractEvent
|
||||
|
||||
Your event handlers will be passed instances of `AbstractEvent`, a cross-browser wrapper around the browser's native event. It has the same interface as the browser's native event (such as `stopPropagation()` and `preventDefault()`) except they work exactly the same across all browsers.
|
||||
|
||||
If you find that you need the underlying browser event for some reason, simply use the `nativeEvent` attribute to get it.
|
||||
|
||||
|
||||
### ReactComponent
|
||||
|
||||
Component classses created by `createClass()` return instances of `ReactComponent` when called. Most of the time when you're using React you're either creating or consuming `ReactComponent`s.
|
||||
|
||||
|
||||
#### getDOMNode
|
||||
|
||||
```javascript
|
||||
DOMElement getDOMNode()
|
||||
```
|
||||
|
||||
If this component has been mounted into the DOM, this returns the corresponding native browser DOM element. This method is useful for reading values out of the DOM, such as form field values and performing DOM measurements.
|
||||
|
||||
|
||||
#### setProps
|
||||
|
||||
```javascript
|
||||
setProps(object nextProps)
|
||||
```
|
||||
|
||||
When you're integrating with an external JavaScript application you may want to signal a change to a React component rendered with `renderComponent()`. Simply call `setProps()` to change its properties and trigger a re-render.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> This method can only be called on a root-level component. That is, it's only available on the component passed directly to `renderComponent()` and none of its children. If you're inclined to use `setProps()` on a child component, instead take advantage of reactive updates and pass the new prop to the child component when it's created in `render()`.
|
||||
|
||||
|
||||
#### replaceProps
|
||||
|
||||
```javascript
|
||||
replaceProps(object nextProps)
|
||||
```
|
||||
|
||||
Like `setProps()` but deletes any pre-existing props that are not in nextProps.
|
||||
|
||||
|
||||
#### transferPropsTo
|
||||
|
||||
```javascript
|
||||
ReactComponent transferPropsTo(ReactComponent targetComponent)
|
||||
```
|
||||
|
||||
Transfer properties from this component to a target component that have not already been set on the target component. This is usually used to pass down properties to the returned root component. `targetComponent`, now updated with some new props is returned as a convenience.
|
||||
|
||||
|
||||
#### setState
|
||||
|
||||
```javascript
|
||||
setState(object nextState[, function callback])
|
||||
```
|
||||
|
||||
Merges nextState with the current state. This is the primary method you use to trigger UI updates from event handlers and server request callbacks. In addition, you can supply an optional callback function that is executed once `setState` is completed.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> *NEVER* mutate `this.state` directly. As calling `setState()` afterwards may replace the mutation you made. Treat `this.state` as if it were immutable.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> `setState()` does not immediately mutate `this.state` but creates a pending state transition. Accessing `this.state` after calling this method can potentially return the existing value.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> There is no guarantee of synchronous operation of calls to `setState` and calls may be batched for performance gains.
|
||||
|
||||
|
||||
#### replaceState
|
||||
|
||||
```javascript
|
||||
replaceState(object nextState[, function callback])
|
||||
```
|
||||
|
||||
Like `setState()` but deletes any pre-existing state keys that are not in nextState.
|
||||
|
||||
|
||||
#### forceUpdate()
|
||||
|
||||
```javascript
|
||||
forceUpdate([function callback])
|
||||
```
|
||||
|
||||
If your `render()` method reads from something other than `this.props` or `this.state` you'll need to tell React when it needs to re-run `render()`. Use `forceUpdate()` to cause React to automatically re-render. This will cause `render()` to be called on the component and all of its children but React will only update the DOM if the markup changes.
|
||||
|
||||
Normally you should try to avoid all uses of `forceUpdate()` and only read from `this.props` and `this.state` in `render()`. This makes your application much simpler and more efficient.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> There is no guarantee of synchronous operation of calls to `forceUpdate` and calls may be batched for performance gains.
|
||||
|
||||
|
||||
#### Lifecycle methods
|
||||
|
||||
```javascript
|
||||
object getInitialState()
|
||||
componentWillMount()
|
||||
componentDidMount(DOMElement domNode)
|
||||
componentWillReceiveProps(object nextProps)
|
||||
boolean shouldComponentUpdate(object nextProps, object nextState)
|
||||
componentWillUpdate(object nextProps, object nextState)
|
||||
ReactComponent render()
|
||||
componentDidUpdate(object prevProps, object prevState, DOMElement domNode)
|
||||
componentWillUnmount()
|
||||
```
|
||||
|
||||
See the [working with the browser](./working-with-the-browser.html) documentation for more details on these lifecycle methods.
|
||||
|
||||
|
||||
## DOM Differences
|
||||
|
||||
React has implemented a browser-independent events and DOM system for performance and cross-browser compatibility reasons. We took the opportunity to clean up a few rough edges in browser DOM implementations.
|
||||
|
||||
* All events (including submit) bubble correctly per the W3C spec
|
||||
* All event objects conform to the W3C spec
|
||||
* All DOM properties and attributes (including event handlers) should be camelCased to be consistent with standard JavaScript style. We intentionally break with the spec here, since the spec is inconsistent.
|
||||
* The `style` attribute accepts a JavaScript object with camelCased properties rather than a CSS string. This is consistent with the DOM `style` JavaScript property, is more efficient, and prevents XSS security holes.
|
||||
* `onChange` behaves as you would expect it to: whenever a form field is changed this event is fired rather than inconsistently on blur. We intentionally break from existing browser behavior because `onChange` is a misnomer for its behavior and React relies on this event to react to user input in real time.
|
||||
96
docs/docs/OUTLINE.md
Normal file
96
docs/docs/OUTLINE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
id: OUTLINE
|
||||
title: Goals of the documentation
|
||||
layout: docs
|
||||
prev: 09.1-tutorial.html
|
||||
---
|
||||
- Flow of docs should mimic progression of questions a new user would ask
|
||||
- High information density -- assume the reader is adept at JS
|
||||
- Talk about best practices
|
||||
- JSFiddles for all code samples
|
||||
- Provide background for some of the design decisions
|
||||
- Less words less words less words!
|
||||
|
||||
## Outline
|
||||
|
||||
Motivation / Why React?
|
||||
- Declarative (simple)
|
||||
- Components (separation of concerns)
|
||||
- Give it 5 minutes
|
||||
|
||||
Displaying data
|
||||
- Hello world example
|
||||
- Reactive updates
|
||||
- Components are just functions
|
||||
- JSX syntax (link to separate doc?)
|
||||
- JSX gotchas
|
||||
|
||||
Interactivity and dynamic UIs
|
||||
- Click handler example
|
||||
- Event handlers / synthetic events (link to w3c docs)
|
||||
- Under the hood: autoBind and event delegation (IE8 notes)
|
||||
- React is a state machine
|
||||
- How state works
|
||||
- What components should have state?
|
||||
- What should go in state?
|
||||
- What shouldn't go in state?
|
||||
|
||||
Scaling up: using multiple components
|
||||
- Motivation: separate concerns
|
||||
- Composition example
|
||||
- Ownership (and owner vs. parent)
|
||||
- Children
|
||||
- Data flow (one-way data binding)
|
||||
- A note on performance
|
||||
|
||||
Building effective reusable components
|
||||
- You should build a reusable component library (CSS, testing etc)
|
||||
- Prop validation
|
||||
- Transferring props: a shortcut
|
||||
- Mixins
|
||||
- Testing
|
||||
|
||||
Forms
|
||||
|
||||
Working with the browser
|
||||
- The mock DOM
|
||||
- Refs / getDOMNode()
|
||||
- More about refs
|
||||
- Component lifecycle
|
||||
- Browser support and polyfills
|
||||
|
||||
Working with your environment
|
||||
- CDN-hosted React
|
||||
- Using master
|
||||
- In-browser JSX transform
|
||||
- Productionizing: precompiled JSX
|
||||
- Helpful open-source projects
|
||||
|
||||
Integrating with other UI libraries
|
||||
- Using jQuery plugins
|
||||
- Letting jQuery manage React components
|
||||
- Using with Backbone.View
|
||||
- CoffeeScript
|
||||
- Moving from Handlebars to React: an example
|
||||
|
||||
Server / static rendering
|
||||
- Motivation
|
||||
- Simple example
|
||||
- How does it work? (No DOM)
|
||||
- Rendr + React
|
||||
|
||||
Big ideas
|
||||
- Animation
|
||||
- Bootstrap bindings (responsive grids)
|
||||
- Reactive CSS
|
||||
- Web workers
|
||||
- Native views
|
||||
|
||||
Case studies
|
||||
- Comment box tutorial from scratch
|
||||
- From HTML mock to application: React one-hour email
|
||||
- Jordan's LikeToggler example
|
||||
|
||||
Reference
|
||||
- API
|
||||
- DOM differences
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
id: docs-advanced-components
|
||||
title: Advanced Components
|
||||
description: How to build advanced composite components.
|
||||
layout: docs
|
||||
prev: event-handling.html
|
||||
next: mixins.html
|
||||
---
|
||||
|
||||
Composite components extend a `ReactCompositeComponent` base class that provides
|
||||
a very powerful API that makes React flexible and able to easily work with other
|
||||
libraries and frameworks.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
Composite components can optionally implement lifecycle methods that are invoked
|
||||
at various stages in the [component lifecycle](component-lifecycle.html) that
|
||||
each have unique guarantees.
|
||||
|
||||
### Mounting
|
||||
|
||||
- `getInitialState(): object` is invoked before a component is mounted.
|
||||
Stateful components should implement this and return the initial state data.
|
||||
- `componentWillMount()` is invoked immediately before mounting occurs.
|
||||
- `componentDidMount(DOMElement rootNode)` is invoked immediately after
|
||||
mounting occurs. Initialization that requires DOM nodes should go here.
|
||||
|
||||
### Updating
|
||||
|
||||
- `componentWillReceiveProps(object nextProps)` is invoked when a mounted
|
||||
component receives new props. This method should be used to compare
|
||||
`this.props` and `nextProps` to perform state transitions using
|
||||
`this.setState()`.
|
||||
- `shouldComponentUpdate(object nextProps, object nextState): boolean` is
|
||||
invoked when a component decides whether any changes warrant an update to the
|
||||
DOM. Implement this as an optimization to compare `this.props` with
|
||||
`nextProps` and `this.state` with `nextState` and return false if React
|
||||
should skip updating.
|
||||
- `componentWillUpdate(object nextProps, object nextState)` is invoked
|
||||
immediately before updating occurs. You cannot call `this.setState()` here.
|
||||
- `componentDidUpdate(object prevProps, object prevState, DOMElement rootNode)`
|
||||
is invoked immediately after updating occurs.
|
||||
|
||||
### Unmounting
|
||||
|
||||
- `componentWillUnmount()` is invoked immediately before a component is
|
||||
unmounted and destroyed. Cleanup should go here.
|
||||
|
||||
## Mounted Methods
|
||||
|
||||
_Mounted_ composite components also support the following methods:
|
||||
|
||||
- `getDOMNode(): DOMElement` can be invoked on any mounted component in order
|
||||
to obtain a reference to its rendered DOM node.
|
||||
- `forceUpdate()` can be invoked on any mounted component when you know that
|
||||
some deeper aspect of the component's state has changed without using
|
||||
`this.setState()`.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> The `DOMElement rootNode` argument of `componentDidMount()` and
|
||||
> `componentDidUpdate()` is a convenience. The same node can be obtained by
|
||||
> calling `this.getDOMNode()`.
|
||||
|
||||
## Component Refs
|
||||
|
||||
A common use case of event callbacks or the lifecycle methods is to operate on a
|
||||
component returned by `render()`. For example, consider a search component that
|
||||
should auto-focus the input once mounted:
|
||||
|
||||
```javascript
|
||||
var SearchForm = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<form action={this.props.action}>
|
||||
<input type="search" placeholder="Search..." />
|
||||
</form>
|
||||
);
|
||||
},
|
||||
componentDidMount: function(rootNode) {
|
||||
var searchInput = rootNode.firstChild;
|
||||
searchInput.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Although this implementation works, it is fragile because `componentDidMount()`
|
||||
now relies on `render()` returning a particular DOM structure.
|
||||
|
||||
React provides a better way for composite components to reference components
|
||||
that it constructs in its `render()` method through the use of refs. A component
|
||||
can assign a `ref` to any component it constructs. This will create a reference
|
||||
to those components on `this.refs`. For example:
|
||||
|
||||
```javascript{5,10}
|
||||
var SearchForm = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<form action={this.props.action}>
|
||||
<input type="search" placeholder="Search..." ref="searchInput" />
|
||||
</form>
|
||||
);
|
||||
},
|
||||
componentDidMount: function(rootNode) {
|
||||
var searchInput = this.refs.searchInput.getDOMNode();
|
||||
searchInput.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
In this example, `this.refs.searchInput` will reference the `<input>` component
|
||||
and is available in most lifecycle methods and event callbacks. We obtain a
|
||||
reference to the `<input>`'s DOM node using `getDOMNode()`.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> If you want to preserve compatibility with Google Closure Compiler's
|
||||
> property crushing in `ADVANCED_OPTIMIZATIONS` mode, make sure to use string
|
||||
> literals with `this.refs`.
|
||||
151
docs/docs/api.md
151
docs/docs/api.md
@@ -1,151 +0,0 @@
|
||||
---
|
||||
id: docs-api
|
||||
title: React API
|
||||
layout: docs
|
||||
prev: mixins.html
|
||||
next: jsx-is-not-html.html
|
||||
---
|
||||
|
||||
## React
|
||||
|
||||
`React` is the entry point to the React framework. If you're using one of the prebuilt packages it's available as a global; if you're using CommonJS modules you can `require()` it.
|
||||
|
||||
#### React.DOM
|
||||
|
||||
`React.DOM` provides all of the standard HTML tags needed to build a React app. You generally don't use it directly; instead, just include it as part of the `/** @jsx React.DOM */` docblock.
|
||||
|
||||
#### React.initializeTouchEvents
|
||||
|
||||
```javascript
|
||||
initializeTouchEvents(boolean shouldUseTouch)
|
||||
```
|
||||
|
||||
Configure React's event system to handle touch events on mobile devices.
|
||||
|
||||
#### React.autoBind
|
||||
|
||||
```javascript
|
||||
function autoBind(function method)
|
||||
```
|
||||
|
||||
Marks the provided function to be automatically bound to each React component instance created. This allows React components to define automatically bound methods and ensure that when called they will always reference their current instance.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
React.createClass({
|
||||
click: React.autoBind(function(evt) {
|
||||
this.setState({jumping: true});
|
||||
}),
|
||||
render: function() {
|
||||
// Look: no bind!
|
||||
return <a onClick={this.click}>Jump</a>;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### React.createClass
|
||||
|
||||
```javascript
|
||||
function createClass(object specification)
|
||||
```
|
||||
|
||||
Creates a component given a specification. A component implements a `render` method which returns **one single** child. That child may have an arbitrarily deep child structure. One thing that makes components different than a standard prototypal classes is that you don't need to call new on them. They are convenience wrappers that construct backing instances (via new) for you.
|
||||
|
||||
#### React.renderComponent
|
||||
|
||||
```javascript
|
||||
ReactComponent renderComponent(ReactComponent container, DOMElement mountPoint)
|
||||
```
|
||||
|
||||
Renders a React component into the DOM in the supplied `container`.
|
||||
|
||||
If the React component was previously rendered into `container`, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React component.
|
||||
|
||||
## AbstractEvent
|
||||
|
||||
Your event handlers will be passed instances of `AbstractEvent`, a cross-browser wrapper around the browser's native event. It has the same interface as the browser's native event (such as `stopPropagation()` and `preventDefault()`) except they work exactly the same across all browsers.
|
||||
|
||||
If you find that you need the underlying browser event for some reason, simply use the `nativeEvent` attribute to get it.
|
||||
|
||||
## ReactComponent
|
||||
|
||||
Component classses created by `createClass()` return instances of `ReactComponent` when called. Most of the time when you're using React you're either creating or consuming `ReactComponent`s.
|
||||
|
||||
#### getDOMNode
|
||||
|
||||
```javascript
|
||||
DOMElement getDOMNode()
|
||||
```
|
||||
|
||||
If this component has been mounted into the DOM, this returns the corresponding native browser DOM element. This method is useful for reading values out of the DOM, such as form field values and performing DOM measurements.
|
||||
|
||||
#### setProps
|
||||
|
||||
```javascript
|
||||
setProps(object nextProps)
|
||||
```
|
||||
|
||||
When you're integrating with an external JavaScript application you may want to signal a change to a React component rendered with `renderComponent()`. Simply call `setProps()` to change its properties and trigger a re-render.
|
||||
|
||||
**Note:** This method can only be called on a root-level component. That is, it's only available on the component passed directly to `renderComponent()` and none of its children. If you're inclined to use `setProps()` on a child component, instead take advantage of reactive updates and pass the new prop to the child component when it's created in `render()`.
|
||||
|
||||
#### replaceProps
|
||||
|
||||
```javascript
|
||||
replaceProps(object nextProps)
|
||||
```
|
||||
|
||||
Like `setProps()` but deletes any pre-existing props that are not in nextProps.
|
||||
|
||||
#### transferPropsTo
|
||||
|
||||
```javascript
|
||||
ReactComponent transferPropsTo(ReactComponent targetComponent)
|
||||
```
|
||||
|
||||
Transfer properties from this component to a target component that have not already been set on the target component. This is usually used to pass down properties to the returned root component. `targetComponent`, now updated with some new props is returned as a convenience.
|
||||
|
||||
#### setState
|
||||
|
||||
```javascript
|
||||
setState(object nextState)
|
||||
```
|
||||
|
||||
Merges nextState with the current state. This is the primary method you use to trigger UI updates from event handlers and server request callbacks.
|
||||
|
||||
**Note:** *NEVER* mutate `this.state` directly. As calling `setState()` afterwards may replace the mutation you made. Treat `this.state` as if it were immutable.
|
||||
|
||||
**Note:** `setState()` does not immediately mutate `this.state` but creates a pending state transition. Accessing `this.state` after calling this method can potentially return the existing value.
|
||||
|
||||
#### replaceState
|
||||
|
||||
```javascript
|
||||
replaceState(object nextState)
|
||||
```
|
||||
|
||||
Like `setState()` but deletes any pre-existing state keys that are not in nextState.
|
||||
|
||||
#### forceUpdate()
|
||||
|
||||
```javascript
|
||||
forceUpdate()
|
||||
```
|
||||
|
||||
If your `render()` method reads from something other than `this.props` or `this.state` you'll need to tell React when it needs to re-run `render()`. Use `forceUpdate()` to cause React to automatically re-render. This will cause `render()` to be called on the component and all of its children but React will only update the DOM if the markup changes.
|
||||
|
||||
Normally you should try to avoid all uses of `forceUpdate()` and only read from `this.props` and `this.state` in `render()`. This makes your application much simpler and more efficient.
|
||||
|
||||
```javascript
|
||||
object getInitialState()
|
||||
componentWillMount()
|
||||
componentDidMount(DOMElement domNode)
|
||||
componentWillReceiveProps(object nextProps)
|
||||
boolean shouldComponentUpdate(object nextProps, object nextState)
|
||||
componentWillUpdate(object nextProps, object nextState)
|
||||
ReactComponent render()
|
||||
componentDidUpdate(object prevProps, object prevState, DOMElement domNode)
|
||||
componentWillUnmount()
|
||||
```
|
||||
|
||||
See the [advanced components](advanced-components.html) documentation for more details on these lifecycle methods.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
id: docs-common-questions
|
||||
title: Common Questions
|
||||
layout: docs
|
||||
prev: tutorial.html
|
||||
next: syntax.html
|
||||
---
|
||||
|
||||
### What browsers does React support?
|
||||
|
||||
React supports the latest two Chrome, Firefox, Safari, and Internet Explorer versions. React can work with Internet Explorer 8 with polyfills.
|
||||
|
||||
### How do I get React to support Internet Explorer 8?
|
||||
|
||||
React requires ES5 JavaScript shims to run in Internet Explorer 8. Include the [ES5 Shims](https://github.com/kriskowal/es5-shim) to implement these shims.
|
||||
|
||||
### Who uses React?
|
||||
|
||||
The [Instagram](http://instagram.com/) website is built entirely in React. The [Facebook](https://www.facebook.com/) website is also increasingly using React, including the common commenting plugin across the site.
|
||||
|
||||
### I don't get it. React is confusing!
|
||||
|
||||
[This blog post by a member of the React team](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood) talks about some of the reasons
|
||||
why React is designed the way that it is.
|
||||
|
||||
### Can I integrate with other JavaScript libraries?
|
||||
|
||||
Absolutely! In fact, we encourage it! See [our GitHub repo](http://github.com/facebook/react/) for a [TodoMVC example using Backbone](https://github.com/facebook/react/tree/master/examples/todomvc-backbone) and a [jQuery + Bootstrap modal demo](https://github.com/facebook/react/tree/master/examples/jquery-bootstrap).
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
id: docs-component-basics
|
||||
title: Component Basics
|
||||
description: What are components?
|
||||
layout: docs
|
||||
next: component-data.html
|
||||
prev: syntax.html
|
||||
---
|
||||
|
||||
_Components_ are the basic units of composition in React. Components encapsulate
|
||||
the logic necessary to take input parameters and render markup. Components can
|
||||
be rendered into an existing DOM element on the page by using
|
||||
`React.renderComponent`:
|
||||
|
||||
```javascript
|
||||
// Replaces everything in `document.body` with <div>Hello, world!</div>;
|
||||
React.renderComponent(<div>Hello, world!</div>, document.body);
|
||||
```
|
||||
|
||||
Keep in mind that `<div>` is **not** a DOM element! Keep reading...
|
||||
|
||||
## Types of Components
|
||||
|
||||
There are two types of components:
|
||||
|
||||
- **Composite Components**
|
||||
- **DOM Components**
|
||||
|
||||
### Composite Components <small>such as `TodoApp` and `Typeahead`.</small>
|
||||
|
||||
The majority of your React code will be implementing composite components.
|
||||
|
||||
Composite components are higher-level components with custom rendering logic
|
||||
that may compose other composite components or DOM components.
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
var LinkButton = React.createClass({
|
||||
render: function() {
|
||||
return <a className="btn" />;
|
||||
}
|
||||
});
|
||||
|
||||
var myButton = <LinkButton />;
|
||||
```
|
||||
|
||||
This example defines a `LinkButton` component class using `React.createClass()`,
|
||||
and its `render()` method composes the `<a>` DOM component.
|
||||
|
||||
### DOM Components <small>such as `div` and `span`.</small>
|
||||
|
||||
DOM components are the set of classes that correspond to browser DOM elements.
|
||||
They are defined in `React.DOM` and can be brought "into scope" by setting
|
||||
`@jsx React.DOM` in the docblock. See [JSX Syntax](syntax.html) for more
|
||||
details.
|
||||
|
||||
Although `React.DOM` components look like browser DOM elements, they differ in a
|
||||
few ways:
|
||||
|
||||
- All property names, including event handlers, are camelCased.
|
||||
- JavaScript identifiers should be used, namely `className` and `htmlFor`.
|
||||
- The `style` prop expects an object instead of a string. The object should map
|
||||
camelCased style properties to values, e.g. `{backgroundColor: '#fff'}`.
|
||||
|
||||
Here is an example of a React link styled as a button with a click handler:
|
||||
|
||||
```javascript
|
||||
/** @jsx React.DOM */
|
||||
var handleClick = function() {alert('Clicked!');};
|
||||
var inlineStyle = {textDecoration: 'none'};
|
||||
|
||||
var myLink = <a className="btn" onClick={handleClick} style={inlineStyle} />;
|
||||
```
|
||||
@@ -1,145 +0,0 @@
|
||||
---
|
||||
id: docs-component-data
|
||||
title: Component Data
|
||||
description: How is data passed into a component?
|
||||
layout: docs
|
||||
prev: component-basics.html
|
||||
next: component-lifecycle.html
|
||||
---
|
||||
|
||||
## Props
|
||||
|
||||
Components use data to determine what should be rendered. For example:
|
||||
|
||||
```javascript
|
||||
var LikeLink = React.createClass({
|
||||
render: function() {
|
||||
var text = this.props.liked ? 'Liked' : 'Like';
|
||||
return <a>{text}</a>;
|
||||
}
|
||||
});
|
||||
var myLikeLink = <LikeLink liked={false} />;
|
||||
```
|
||||
|
||||
In this example, `LikeLink` takes `liked` as boolean data. This type of data
|
||||
that is passed in is called a "prop". Examples of props on DOM components
|
||||
include `className` and `onClick`.
|
||||
|
||||
Whenever a component's props change, its `render()` function will be
|
||||
re-evaluated and the DOM will be updated. React will ensure that the DOM is
|
||||
always kept up-to-date.
|
||||
|
||||
## State
|
||||
|
||||
Let's build a small `LikeApp` application that makes use of the `<LikeLink>`
|
||||
component from above. It should start off unliked and we should be able to like
|
||||
it by clicking the link:
|
||||
|
||||
```javascript
|
||||
var LikeApp = React.createClass({
|
||||
render: function() {
|
||||
var isClicked = false;
|
||||
return <LikeLink liked={isClicked} onClick={this.handleClick.bind(this)} />;
|
||||
},
|
||||
handleClick: function() {
|
||||
// Somehow update `isClicked`.
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This renders a `<LikeLink>` with a click listener. However, it is not clear how
|
||||
`handleClick` should update `isClicked` to true. `LikeApp` needs a way to store
|
||||
**state** about whether or not it has been clicked.
|
||||
|
||||
### State vs. Props
|
||||
|
||||
State is data that is managed _internally_ by a composite component. Like props,
|
||||
the `render()` function will be re-evaluated whenever state changes. Props and
|
||||
state differ in that:
|
||||
|
||||
- Props are passed in from the creator.
|
||||
- State is private to and managed by the component.
|
||||
|
||||
### Managing State
|
||||
|
||||
Let's update our `LikeApp` component using state:
|
||||
|
||||
```javascript{2-4,6,10}
|
||||
var LikeApp = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {isClicked: false};
|
||||
},
|
||||
render: function() {
|
||||
var isClicked = this.state.isClicked;
|
||||
return <LikeLink liked={isClicked} onClick={this.handleClick.bind(this)} />;
|
||||
},
|
||||
handleClick: function() {
|
||||
this.setState({isClicked: true});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
There's a lot going on here, so let's work our way from top to bottom:
|
||||
|
||||
- `getInitialState()` describes what state data looks like when the component
|
||||
is created.
|
||||
- In `render()`, state data can be accessed via `this.state`.
|
||||
- When the link is clicked, we update state using `setState()`.
|
||||
|
||||
Now when we click the link, the `<LikeLink>` will get updated, right? Wrong.
|
||||
|
||||
## Transferring Props
|
||||
|
||||
If you have been following carefully, you may have noticed that although we pass
|
||||
a click handler into `<LikeLink>` as a prop, `LikeLink` does not do anything
|
||||
with `this.props.onClick`! Let's fix that.
|
||||
|
||||
```javascript{4}
|
||||
var LikeLink = React.createClass({
|
||||
render: function() {
|
||||
var text = this.props.liked ? 'Liked' : 'Like';
|
||||
return <a onClick={this.props.onClick}>{text}</a>;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Although this works, realize that this would quickly become tedious if we wanted
|
||||
to also transfer `href`, `title`, `target`, and other events from `this` to the
|
||||
rendered `<a>`. React provides a convenience method, `transferPropsTo()`, for
|
||||
transferring props:
|
||||
|
||||
```javascript{4}
|
||||
var LikeLink = React.createClass({
|
||||
render: function() {
|
||||
var text = this.props.liked ? 'Liked' : 'Like';
|
||||
return this.transferPropsTo(<a>{text}</a>);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This will transfer all props from `this` to the specified component (including
|
||||
`onClick`).
|
||||
|
||||
## Summary
|
||||
|
||||
Now we are done. `LikeApp` renders an unliked link which, when clicked, will:
|
||||
|
||||
1. Update the internal state of `LikeApp`.
|
||||
2. Change the props passed into `LikeLink`.
|
||||
3. Change the return value of `render()`.
|
||||
4. Trigger an update to the DOM.
|
||||
|
||||
It's worth noting that React will handle new return values of `render()` by
|
||||
making the minimal set of mutations necessary to bring the DOM up-to-date. In
|
||||
this case, only the `textContent` of the rendered link will be mutated.
|
||||
|
||||
In summary:
|
||||
|
||||
- Props are passed in whereas state is managed internally by a component.
|
||||
- Never mutate `this.props` or `this.state`. You should pass props into other
|
||||
components and mutate state using `setState()`.
|
||||
- State is private. Never read `state` or call `setState()` on
|
||||
anything but `this`.
|
||||
- Whenever props or state changes, `render()` will be re-evaluated and the DOM
|
||||
updated. Also, `render()` should not depend on anything besides `this.props`
|
||||
and `this.state`.
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
id: docs-component-lifecycle
|
||||
title: Component Lifecycle
|
||||
description: What happens when I render a React component?
|
||||
layout: docs
|
||||
prev: component-data.html
|
||||
next: event-handling.html
|
||||
---
|
||||
|
||||
## Mounting
|
||||
|
||||
[We have previously seen](component-basics.html) how to render components into
|
||||
existing DOM elements on the page:
|
||||
|
||||
```javascript
|
||||
React.renderComponent(<div>Hello, world!</div>, document.body);
|
||||
```
|
||||
|
||||
In this one simple line, we have accomplished the following:
|
||||
|
||||
- A `<div>` (defined by `React.DOM.div`) component is instantiated.
|
||||
- The component is **mounted** into `document.body`.
|
||||
|
||||
**Mounting** is the process of initializing a React component by creating its
|
||||
DOM nodes and inserting them into a supplied container node.
|
||||
|
||||
At this point, the entire page consists of a single `<div>` with "Hello,
|
||||
world!".
|
||||
|
||||
## Updating
|
||||
|
||||
Let's add a second call to `React.renderComponent()` after three
|
||||
seconds:
|
||||
|
||||
```javascript{2-4}
|
||||
React.renderComponent(<div>Hello, world!</div>, document.body);
|
||||
setTimeout(function() {
|
||||
React.renderComponent(<div>Goodbye, world.</div>, document.body);
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
The second call to `React.renderComponent()` will trigger the following:
|
||||
|
||||
- The `<div>` component will check the new props to see if anything changed.
|
||||
- The set of changes are used to **update** the DOM node as necessary.
|
||||
|
||||
**Updating** is the process of mutating the rendered DOM nodes and occurs
|
||||
whenever either props or state has changed. This ensures that the rendered DOM
|
||||
is consistent with the data.
|
||||
|
||||
## Unmounting
|
||||
|
||||
Let's add one final call to `React.renderComponent()` after another three
|
||||
seconds:
|
||||
|
||||
```javascript{5-7}
|
||||
React.renderComponent(<div>Hello, world!</div>, document.body);
|
||||
setTimeout(function() {
|
||||
React.renderComponent(<div>Goodbye, world.</div>, document.body);
|
||||
}, 3000);
|
||||
setTimeout(function() {
|
||||
React.renderComponent(<img src="/images/fin.png" />, document.body);
|
||||
}, 6000);
|
||||
```
|
||||
|
||||
The third call to `React.renderComponent()` will trigger the following:
|
||||
|
||||
- An `<img>` (defined by `React.DOM.img`) component is instantiated.
|
||||
- React will compare the `<div>` component with the `<img>` component.
|
||||
- Since the component class is different, the `<div>` component will be
|
||||
**unmounted**.
|
||||
- The `<img>` component will then be mounted into `document.body`.
|
||||
|
||||
**Unmounting** is the process of releasing resources that have been allocated by
|
||||
a component. This allows user interfaces built with React to live long without
|
||||
memory leaks.
|
||||
|
||||
Components can also be unmounted using
|
||||
`React.unmountAndReleaseReactRootNode()`:
|
||||
|
||||
```javascript
|
||||
React.unmountAndReleaseReactRootNode(document.body);
|
||||
```
|
||||
|
||||
This will unmount any components mounted immediately within `document.body`.
|
||||
@@ -1,224 +0,0 @@
|
||||
---
|
||||
id: docs-event-handling
|
||||
title: Event Handling
|
||||
description: How do events work with React components?
|
||||
layout: docs
|
||||
prev: component-lifecycle.html
|
||||
next: advanced-components.html
|
||||
---
|
||||
|
||||
Events in React work the way they do with HTML, except the event names are
|
||||
camelCased.
|
||||
|
||||
```javascript
|
||||
var Clicker = React.createClass({
|
||||
render: function() {
|
||||
return <span onClick={this.handleClick}>Click me!</span>;
|
||||
},
|
||||
handleClick: function(event) {
|
||||
alert('You clicked me!');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When `<Clicker>` is clicked, the `handleClick()` function will get fired. Under
|
||||
the hood, React uses top-level event delegation to achieve high performance.
|
||||
|
||||
## Automatically Binding Callbacks
|
||||
|
||||
Just like any callback in JavaScript, if you want to refer to the component as
|
||||
`this` from the callback, you need to bind the callback to the component:
|
||||
|
||||
```javascript{3}
|
||||
var Clicker = React.createClass({
|
||||
render: function() {
|
||||
var handleClick = this.handleClick.bind(this);
|
||||
return <span onClick={handleClick}>Click me!</span>;
|
||||
},
|
||||
handleClick: function(event) {
|
||||
alert(this.ALERT_MESSAGE);
|
||||
},
|
||||
ALERT_MESSAGE: 'You clicked me!'
|
||||
});
|
||||
```
|
||||
|
||||
React provides a convenient and _efficient_ way to bind methods using
|
||||
`React.autoBind()`:
|
||||
|
||||
```javascript{3,5-7}
|
||||
var Clicker = React.createClass({
|
||||
render: function() {
|
||||
return <span onClick={this.handleClick}>Click me!</span>;
|
||||
},
|
||||
handleClick: React.autoBind(function(event) {
|
||||
alert(this.ALERT_MESSAGE);
|
||||
}),
|
||||
ALERT_MESSAGE: 'You clicked me!'
|
||||
});
|
||||
```
|
||||
|
||||
> Note:
|
||||
>
|
||||
> Binding a function allocates memory to create a new bound function. Since
|
||||
> `render()` may be invoked many times, it is a bad place to bind functions.
|
||||
> `React.autoBind()` sidesteps this issue by only binding once at instantiation
|
||||
> time.
|
||||
|
||||
## DOM Events
|
||||
|
||||
React uses [top-level event delegation](http://davidwalsh.name/event-delegate)
|
||||
to achieve high performance when implementing DOM events. For each type of DOM
|
||||
event, React adds a single top-level listener and determines which event
|
||||
handlers to execute by simulating event capturing and bubbling.
|
||||
|
||||
DOM event handlers are called with a normalized `AbstractEvent` object that has
|
||||
cross-browser compatible implementations of `stopPropagation` and
|
||||
`preventDefault()`. If you need access to the raw browser event, you can use the
|
||||
`nativeEvent` property.
|
||||
|
||||
> Note:
|
||||
>
|
||||
> The `AbstractEvent` object is JSON serializable so that React applications can
|
||||
> be executed inside web workers.
|
||||
|
||||
### Touch Events
|
||||
|
||||
If you want to use touch events, you must configure React's event system to
|
||||
initialize them:
|
||||
|
||||
```javascript
|
||||
// Invoke before calling `React.renderComponent()`.
|
||||
React.initializeTouchEvents(true);
|
||||
```
|
||||
|
||||
## Custom Events
|
||||
|
||||
Notice that event listeners are attached by simply passing them into components
|
||||
as props. For DOM components, events are handled using top-level event
|
||||
delegation. For composite components, event handling is up to the component's
|
||||
implementation.
|
||||
|
||||
Here is an example of a toggle link that fires a custom `onToggle` event:
|
||||
|
||||
```javascript
|
||||
var ToggleLink = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {isEnabled: false};
|
||||
},
|
||||
render: function() {
|
||||
return <a onClick={this.handleClick}>Toggle</a>;
|
||||
},
|
||||
handleClick: React.autoBind(function() {
|
||||
var willEnable = !this.state.isEnabled;
|
||||
if (this.props.onToggle) {
|
||||
this.props.onToggle(willEnable)
|
||||
}
|
||||
this.setState({isEnabled: willEnable});
|
||||
})
|
||||
});
|
||||
|
||||
var handleToggle = function(enabled) {
|
||||
alert(enabled ? 'Enabled.' : 'Disabled.');
|
||||
};
|
||||
var myToggleLink = <ToggleLink onToggle={handleToggle} />;
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
With React your event handlers should be quite small. Large event handlers may
|
||||
be symptomatic of code that should be moved into helpers or into `render()`.
|
||||
Here are some common usage patterns for event handlers.
|
||||
|
||||
#### Updating State
|
||||
|
||||
The most common thing to do in response to a user action is to call
|
||||
`this.setState()` to update the component's state, which will in turn trigger
|
||||
an update to the rendered component.
|
||||
|
||||
#### Server Requests
|
||||
|
||||
Many event handlers will issue a server request to read or write some data in
|
||||
response to an event. The response handler for the request will often call
|
||||
`this.setState()`.
|
||||
|
||||
#### Invoke a Callback
|
||||
|
||||
Your component will often be a small, reusable building block that does not know
|
||||
how to respond to a user action. In these situations, we delegate the
|
||||
responsibility to the owner by exposing a handler on `this.props`. This is what
|
||||
the `ToggleLink` example above is doing.
|
||||
|
||||
#### Inter-component Communication
|
||||
|
||||
A common scenario involves communicating to **Component A** that a user action
|
||||
has occurred on **Component B**. To solve this problem, a common parent to
|
||||
both components should listen for the event on **Component B**, update its
|
||||
internal state, and pass that data into **Component A**.
|
||||
|
||||
For example, say we have two components: **Clicker**, a component that fires an
|
||||
`onCountChange` custom event, and **ClickCountLabel**, a component that displays
|
||||
the number of clicks that have happened:
|
||||
|
||||
```javascript
|
||||
var Clicker = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {count: 0};
|
||||
},
|
||||
render: function() {
|
||||
return <span onClick={this.handleClick}>Click me!</span>;
|
||||
},
|
||||
handleClick: React.autoBind(function() {
|
||||
this.setState({count: this.state.count + 1});
|
||||
if (this.props.onCountChange) {
|
||||
this.props.onCountChange(this.state.count);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var ClickCountLabel = React.createClass({
|
||||
render: function() {
|
||||
return <p>You have clicked <strong>{this.props.count}</strong> times.</p>;
|
||||
}
|
||||
});
|
||||
|
||||
var ClickApp = React.createClass({
|
||||
render: function() {
|
||||
var count = 0;
|
||||
return (
|
||||
<div>
|
||||
<Clicker onCountChange={this.handleCountChange} />
|
||||
<ClickCountLabel count={count} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
handleCountChange: React.autoBind(function(count) {
|
||||
// Somehow update `count`.
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
In order to communicate the click count from `Clicker` to `ClickCountLabel`, we
|
||||
modify `ClickApp` to maintain state that will be passed into `ClickCountLabel`:
|
||||
|
||||
```javascript{2-4,6,15}
|
||||
var ClickApp = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {count: 0};
|
||||
},
|
||||
render: function() {
|
||||
var count = this.state.count;
|
||||
return (
|
||||
<div>
|
||||
<Clicker onCountChange={this.handleCountChange} />
|
||||
<ClickCountLabel count={count} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
handleCountChange: React.autoBind(function(count) {
|
||||
this.setState({count: count});
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Now when `Clicker` fires the `onCountChange` event, the `ClickCountLabel` will
|
||||
get updated!
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
id: docs-getting-started
|
||||
id: getting-started
|
||||
title: Getting Started
|
||||
layout: docs
|
||||
next: tutorial.html
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
id: docs-mixins
|
||||
title: Mixins
|
||||
layout: docs
|
||||
prev: advanced-components.html
|
||||
next: api.html
|
||||
---
|
||||
|
||||
Mixins allow code to be shared between multiple React components. They are pretty similar to mixins
|
||||
in Python or traits in PHP. Let's look at a simple example:
|
||||
|
||||
```javascript
|
||||
var MyMixin = {
|
||||
getMessage: function() {
|
||||
return 'hello world';
|
||||
}
|
||||
};
|
||||
|
||||
var MyComponent = React.createClass({
|
||||
mixins: [MyMixin],
|
||||
render: function() {
|
||||
return <div>{this.getMessage()}</div>;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
A class can use multiple mixins, but no two mixins can define the same method. Two mixins can, however,
|
||||
implement the same [lifecycle method](component-lifecycle.html). In this case, each implementation will be invoked one after another.
|
||||
|
||||
The only exception is the `shouldComponentUpdate` lifecycle method. This method may only be implemented once
|
||||
(either by a mixin or by the component).
|
||||
|
||||
```javascript
|
||||
var Mixin1 = {
|
||||
componentDidMount: function() {
|
||||
console.log('Mixin1.componentDidMount()');
|
||||
}
|
||||
};
|
||||
|
||||
var Mixin2 = {
|
||||
componentDidMount: function() {
|
||||
console.log('Mixin2.componentDidMount()');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var MyComponent = React.createClass({
|
||||
mixins: [Mixin1, Mixin2],
|
||||
render: function() {
|
||||
return <div>hello world</div>;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When `MyComponent` is mounted into the page, the following text will print to the console:
|
||||
|
||||
```
|
||||
Mixin1.componentDidMount()
|
||||
Mixin2.componentDidMount()
|
||||
```
|
||||
|
||||
## When should you use mixins?
|
||||
|
||||
In general, add a mixin whenever you want a component to share some utility methods, public interface,
|
||||
or lifecycle behavior. Often it's appropriate to use them as you would use a superclass in another OOP language.
|
||||
@@ -1,11 +1,8 @@
|
||||
---
|
||||
id: docs-tutorial
|
||||
id: tutorial
|
||||
title: Tutorial
|
||||
layout: docs
|
||||
prev: getting-started.html
|
||||
next: common-questions.html
|
||||
---
|
||||
|
||||
We'll be building a simple, but realistic comments box that you can drop into a blog, similar to Disqus, LiveFyre or Facebook comments.
|
||||
|
||||
We'll provide:
|
||||
@@ -20,11 +17,11 @@ It'll also have a few neat features:
|
||||
* **Live updates:** as other users comment we'll pop them into the comment view in real time
|
||||
* **Markdown formatting:** users can use Markdown to format their text
|
||||
|
||||
## Want to skip all this and just see the source?
|
||||
### Want to skip all this and just see the source?
|
||||
|
||||
[It's all on GitHub.](https://github.com/petehunt/react-tutorial)
|
||||
|
||||
## Getting started
|
||||
### Getting started
|
||||
|
||||
For this tutorial we'll use prebuilt JavaScript files on a CDN. Open up your favorite editor and create a new HTML document:
|
||||
|
||||
@@ -50,7 +47,7 @@ For this tutorial we'll use prebuilt JavaScript files on a CDN. Open up your fav
|
||||
|
||||
For the remainder of this tutorial, we'll be writing our JavaScript code in this script tag.
|
||||
|
||||
## Your first component
|
||||
### Your first component
|
||||
|
||||
React is all about modular, composable components. For our comment box example, we'll have the following component structure:
|
||||
|
||||
@@ -80,7 +77,7 @@ React.renderComponent(
|
||||
);
|
||||
```
|
||||
|
||||
### JSX Syntax
|
||||
#### JSX Syntax
|
||||
|
||||
The first thing you'll notice is the XML-ish syntax in your JavaScript. We have a simple precompiler that translates the syntactic sugar to this plain JavaScript:
|
||||
|
||||
@@ -104,7 +101,7 @@ React.renderComponent(
|
||||
|
||||
Its use is optional but we've found JSX syntax easier to use than plain JavaScript. Read more on the [JSX Syntax article](syntax.html).
|
||||
|
||||
### What's going on
|
||||
#### What's going on
|
||||
|
||||
We pass some methods in a JavaScript object to `React.createClass()` to create a new React component. The most important of these methods is called `render` which returns a tree of React components that will eventually render to HTML.
|
||||
|
||||
@@ -114,7 +111,7 @@ You do not have to return basic HTML. You can return a tree of components that y
|
||||
|
||||
`React.renderComponent()` instantiates the root component, starts the framework, and injects the markup into a raw DOM element, provided as the second argument.
|
||||
|
||||
# Composing components
|
||||
## Composing components
|
||||
|
||||
Let's build skeletons for `CommentList` and `CommentForm` which will, again, be simple `<div>`s:
|
||||
|
||||
@@ -160,7 +157,7 @@ var CommentBox = React.createClass({
|
||||
|
||||
Notice how we're mixing HTML tags and components we've built. HTML components are regular React components, just like the ones you define, with one difference. The JSX compiler will automatically rewrite HTML tags to "React.DOM.tagName" expressions and leave everything else alone. This is to prevent the pollution of the global namespace.
|
||||
|
||||
## Component Properties
|
||||
### Component Properties
|
||||
|
||||
Let's create our third component, `Comment`. We will want to pass it the author name and comment text so we can reuse the same code for each unique comment. First let's add some comments to the `CommentList`:
|
||||
|
||||
@@ -180,7 +177,7 @@ var CommentList = React.createClass({
|
||||
|
||||
Note that we have passed some data from the parent `CommentList` component to the child `Comment` component as both XML-like children and attributes. Data passed from parent to child is called **props**, short for properties.
|
||||
|
||||
## Using props
|
||||
### Using props
|
||||
|
||||
Let's create the Comment component. It will read the data passed to it from the CommentList and render some markup:
|
||||
|
||||
@@ -202,7 +199,7 @@ var Comment = React.createClass({
|
||||
|
||||
By surrounding a JavaScript expression in braces inside JSX (as either an attribute or child), you can drop text or React components into the tree. We access named attributes passed to the component as keys on `this.props` and any nested elements as `this.props.children`.
|
||||
|
||||
## Adding Markdown
|
||||
### Adding Markdown
|
||||
|
||||
Markdown is a simple way to format your text inline. For example, surrounding text with asterisks will make it emphasized.
|
||||
|
||||
@@ -255,7 +252,7 @@ This is a special API that intentionally makes it difficult to insert raw HTML,
|
||||
|
||||
**Remember:** by using this feature you're relying on Showdown to be secure.
|
||||
|
||||
## Hook up the data model
|
||||
### Hook up the data model
|
||||
|
||||
So far we've been inserting the comments directly in the source code. Instead, let's render a blob of JSON data into the comment list. Eventually this will come from the server, but for now, write it in your source:
|
||||
|
||||
@@ -309,7 +306,7 @@ var CommentList = React.createClass({
|
||||
|
||||
That's it!
|
||||
|
||||
## Fetching from the server
|
||||
### Fetching from the server
|
||||
|
||||
Let's replace the hard-coded data with some dynamic data from the server. We will remove the data prop and replace it with a URL to fetch:
|
||||
|
||||
@@ -323,7 +320,7 @@ React.renderComponent(
|
||||
|
||||
This component is different from the prior components because it will have to re-render itself. The component won't have any data until the request from the server comes back, at which point the component may need to render some new comments.
|
||||
|
||||
## Reactive state
|
||||
### Reactive state
|
||||
|
||||
So far, each component has rendered itself once based on its props. `props` are immutable: they are passed from the parent and are "owned" by the parent. To implement interactions, we introduce mutable **state** to the component. `this.state` is private to the component and can be changed by calling `this.setState()`. When the state is updated, the component re-renders itself.
|
||||
|
||||
@@ -351,7 +348,7 @@ var CommentBox = React.createClass({
|
||||
|
||||
`getInitialState()` executes exactly once during the lifecycle of the component and sets up the initial state of the component.
|
||||
|
||||
### Updating state
|
||||
#### Updating state
|
||||
When the component is first created, we want to GET some JSON from the server and update the state to reflect the latest data. In a real application this would be a dynamic endpoint, but for this example, we will use a static JSON file to keep things simple:
|
||||
|
||||
```javascript
|
||||
@@ -437,7 +434,7 @@ React.renderComponent(
|
||||
|
||||
All we have done here is move the AJAX call to a separate method and call it when the component is first loaded and every 5 seconds after that. Try running this in your browser and changing the `comments.json` file; within 5 seconds, the changes will show!
|
||||
|
||||
## Adding new comments
|
||||
### Adding new comments
|
||||
|
||||
Now it's time to build the form. Our `CommentForm` component should ask the user for their name and comment text and send a request to the server to save the comment.
|
||||
|
||||
@@ -461,7 +458,7 @@ Let's make the form interactive. When the user submits the form, we should clear
|
||||
```javascript{3-13,16,21}
|
||||
// tutorial16.js
|
||||
var CommentForm = React.createClass({
|
||||
handleSubmit: React.autoBind(function() {
|
||||
handleSubmit: function() {
|
||||
var author = this.refs.author.getDOMNode().value.trim();
|
||||
var text = this.refs.text.getDOMNode().value.trim();
|
||||
if (!text || !author) {
|
||||
@@ -471,7 +468,7 @@ var CommentForm = React.createClass({
|
||||
this.refs.author.getDOMNode().value = '';
|
||||
this.refs.text.getDOMNode().value = '';
|
||||
return false;
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<form class="commentForm" onSubmit={this.handleSubmit}>
|
||||
@@ -488,19 +485,17 @@ var CommentForm = React.createClass({
|
||||
});
|
||||
```
|
||||
|
||||
#### Events
|
||||
##### Events
|
||||
|
||||
React attaches event handlers to components using a camelCase naming convention. We attach an `onSubmit` handler to the form that clears the form fields when the form is submitted with valid input.
|
||||
|
||||
We always return `false` from the event handler to prevent the browser's default action of submitting the form. (If you prefer, you can instead take the event as an argument and call `preventDefault()` on it – read more about [event handling](event-handling.html).)
|
||||
|
||||
`React.autoBind()` is a simple way to ensure that a method is always bound to its component. Inside the method, `this` will be bound to the component instance.
|
||||
|
||||
#### Refs
|
||||
##### Refs
|
||||
|
||||
We use the `ref` attribute to assign a name to a child component and `this.refs` to reference the component. We can call `getDOMNode()` on a component to get the native browser DOM element.
|
||||
|
||||
#### Callbacks as props
|
||||
##### Callbacks as props
|
||||
|
||||
When a user submits a comment, we will need to refresh the list of comments to include the new one. It makes sense to do all of this logic in `CommentBox` since `CommentBox` owns the state that represents the list of comments.
|
||||
|
||||
@@ -519,9 +514,9 @@ var CommentBox = React.createClass({
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
handleCommentSubmit: React.autoBind(function(comment) {
|
||||
handleCommentSubmit: function(comment) {
|
||||
// TODO: submit to the server and refresh the list
|
||||
}),
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {data: []};
|
||||
},
|
||||
@@ -551,14 +546,14 @@ Let's call the callback from the `CommentForm` when the user submits the form:
|
||||
```javascript{6}
|
||||
// tutorial18.js
|
||||
var CommentForm = React.createClass({
|
||||
handleSubmit: React.autoBind(function() {
|
||||
handleSubmit: function() {
|
||||
var author = this.refs.author.getDOMNode().value.trim();
|
||||
var text = this.refs.text.getDOMNode().value.trim();
|
||||
this.props.onCommentSubmit({author: author, text: text});
|
||||
this.refs.author.getDOMNode().value = '';
|
||||
this.refs.text.getDOMNode().value = '';
|
||||
return false;
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<form class="commentForm" onSubmit={this.handleSubmit}>
|
||||
@@ -590,7 +585,7 @@ var CommentBox = React.createClass({
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
handleCommentSubmit: React.autoBind(function(comment) {
|
||||
handleCommentSubmit: function(comment) {
|
||||
$.ajax({
|
||||
url: this.props.url,
|
||||
data: comment,
|
||||
@@ -600,7 +595,7 @@ var CommentBox = React.createClass({
|
||||
this.setState({data: data});
|
||||
}.bind(this)
|
||||
});
|
||||
}),
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {data: []};
|
||||
},
|
||||
@@ -625,7 +620,7 @@ var CommentBox = React.createClass({
|
||||
});
|
||||
```
|
||||
|
||||
## Optimization: optimistic updates
|
||||
### Optimization: optimistic updates
|
||||
|
||||
Our application is now feature complete but it feels slow to have to wait for the request to complete before your comment appears in the list. We can optimistically add this comment to the list to make the app feel faster.
|
||||
|
||||
@@ -642,7 +637,7 @@ var CommentBox = React.createClass({
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
handleCommentSubmit: React.autoBind(function(comment) {
|
||||
handleCommentSubmit: function(comment) {
|
||||
var comments = this.state.data;
|
||||
comments.push(comment);
|
||||
this.setState({data: comments});
|
||||
@@ -655,7 +650,7 @@ var CommentBox = React.createClass({
|
||||
this.setState({data: data});
|
||||
}.bind(this)
|
||||
});
|
||||
}),
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {data: []};
|
||||
},
|
||||
@@ -680,6 +675,6 @@ var CommentBox = React.createClass({
|
||||
});
|
||||
```
|
||||
|
||||
## Congrats!
|
||||
### Congrats!
|
||||
|
||||
You have just built a comment box in a few simple steps. Learn more about React in the [reference](syntax.html) or start hacking! Good luck!
|
||||
|
||||
@@ -47,16 +47,3 @@ $ bower install --save react
|
||||
$ npm install -g react-tools
|
||||
```
|
||||
|
||||
## Release Notes
|
||||
|
||||
**0.3.3** Upgrade `react-tools` dependencies, improve in-browser JSX transformer, make `React.renderComponent` more versatile. [Details...](blog/2013/06/20/react-v0-3-3.html)
|
||||
|
||||
**0.3.2** Improve compatibility of JSX Transformer; make `react-tools` compatible with [browserify](https://github.com/substack/node-browserify)
|
||||
|
||||
**0.3.1** Fix `react-tools` module
|
||||
|
||||
**0.3** Initial public release.
|
||||
|
||||
**0.2** Standardize API & refactor component lifecycle. Normalize DOM interactions.
|
||||
|
||||
**0.1** Initial release.
|
||||
|
||||
@@ -14,9 +14,9 @@ var BallmerPeakCalculator = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {bac: 0};
|
||||
},
|
||||
handleChange: React.autoBind(function() {
|
||||
handleChange: function() {
|
||||
this.setState({bac: this.refs.bac.getDOMNode().value});
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
var bac;
|
||||
var pct;
|
||||
@@ -33,7 +33,7 @@ var BallmerPeakCalculator = React.createClass({
|
||||
<h4>Compute your Ballmer Peak:</h4>
|
||||
<p>
|
||||
If your BAC is{' '}
|
||||
<input ref="bac" type="text" onKeyUp={this.handleChange} value={this.state.bac} />
|
||||
<input ref="bac" type="text" onInput={this.handleChange} value={this.state.bac} />
|
||||
{', '}then <b>{pct}</b> of your lines of code will have bugs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
18
examples/jquery-bootstrap/js/app.js
vendored
18
examples/jquery-bootstrap/js/app.js
vendored
@@ -67,18 +67,18 @@ var BootstrapModal = React.createClass({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
onCancel: React.autoBind(function() {
|
||||
onCancel: function() {
|
||||
if (this.props.onCancel) {
|
||||
this.props.onCancel();
|
||||
}
|
||||
this.close();
|
||||
}),
|
||||
onConfirm: React.autoBind(function() {
|
||||
},
|
||||
onConfirm: function() {
|
||||
if (this.props.onConfirm) {
|
||||
this.props.onConfirm();
|
||||
}
|
||||
this.close();
|
||||
}),
|
||||
},
|
||||
close: function() {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
@@ -90,14 +90,14 @@ var Example = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {modalVisible: false};
|
||||
},
|
||||
toggleModal: React.autoBind(function() {
|
||||
toggleModal: function() {
|
||||
this.setState({modalVisible: !this.state.modalVisible});
|
||||
}),
|
||||
handleCancel: React.autoBind(function() {
|
||||
},
|
||||
handleCancel: function() {
|
||||
if (confirm('Are you sure you want to cancel?')) {
|
||||
this.toggleModal();
|
||||
}
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
var modal = null;
|
||||
if (this.state.modalVisible) {
|
||||
@@ -121,4 +121,4 @@ var Example = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(<Example />, document.getElementById('jqueryexample'));
|
||||
React.renderComponent(<Example />, document.getElementById('jqueryexample'));
|
||||
|
||||
@@ -74,17 +74,17 @@ var Utils = {
|
||||
// Begin React stuff
|
||||
|
||||
var TodoItem = React.createClass({
|
||||
handleSubmit: React.autoBind(function(event) {
|
||||
handleSubmit: function(event) {
|
||||
var val = this.refs.editField.getDOMNode().value;
|
||||
if (val) {
|
||||
this.props.onSave(val);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
onEdit: React.autoBind(function() {
|
||||
},
|
||||
onEdit: function() {
|
||||
this.props.onEdit();
|
||||
this.refs.editField.getDOMNode().focus();
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<li class={cx({completed: this.props.todo.get('completed'), editing: this.props.editing})}>
|
||||
@@ -166,7 +166,7 @@ var TodoApp = React.createClass({
|
||||
getBackboneModels: function() {
|
||||
return [this.props.todos];
|
||||
},
|
||||
handleSubmit: React.autoBind(function() {
|
||||
handleSubmit: function() {
|
||||
var val = this.refs.newField.getDOMNode().value.trim();
|
||||
if (val) {
|
||||
this.props.todos.create({
|
||||
@@ -177,13 +177,13 @@ var TodoApp = React.createClass({
|
||||
this.refs.newField.getDOMNode().value = '';
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
toggleAll: React.autoBind(function(event) {
|
||||
},
|
||||
toggleAll: function(event) {
|
||||
var checked = event.nativeEvent.target.checked;
|
||||
this.props.todos.map(function(todo) {
|
||||
todo.set('completed', checked);
|
||||
});
|
||||
}),
|
||||
},
|
||||
destroy: function(todo) {
|
||||
this.props.todos.remove(todo);
|
||||
},
|
||||
@@ -194,11 +194,11 @@ var TodoApp = React.createClass({
|
||||
todo.set('title', text);
|
||||
this.setState({editing: null});
|
||||
},
|
||||
clearCompleted: React.autoBind(function() {
|
||||
clearCompleted: function() {
|
||||
this.props.todos.completed().map(function(todo) {
|
||||
todo.destroy();
|
||||
});
|
||||
}),
|
||||
},
|
||||
render: function() {
|
||||
var footer = null;
|
||||
var main = null;
|
||||
|
||||
@@ -32,24 +32,24 @@ function cx(obj) {
|
||||
}
|
||||
|
||||
var TodoItem = React.createClass({
|
||||
handleSubmit: React.autoBind(function() {
|
||||
handleSubmit: function() {
|
||||
var val = this.state.editText;
|
||||
if (val) {
|
||||
this.props.onSave(val);
|
||||
this.setState({editField: ''});
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
handleEdit: React.autoBind(function() {
|
||||
},
|
||||
handleEdit: function() {
|
||||
this.props.onEdit();
|
||||
this.refs.editField.getDOMNode().focus();
|
||||
}),
|
||||
handleKey: React.autoBind(function(event) {
|
||||
},
|
||||
handleKey: function(event) {
|
||||
if (event.nativeEvent.keyCode === 27) {
|
||||
this.handleSubmit();
|
||||
}
|
||||
this.setState({editText: event.target.value});
|
||||
}),
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {editText: this.props.todo.title};
|
||||
},
|
||||
@@ -114,7 +114,7 @@ var TodoApp = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
handleSubmit: React.autoBind(function() {
|
||||
handleSubmit: function() {
|
||||
var val = this.refs.newField.getDOMNode().value.trim();
|
||||
if (val) {
|
||||
var todos = this.state.todos;
|
||||
@@ -127,7 +127,7 @@ var TodoApp = React.createClass({
|
||||
this.refs.newField.getDOMNode().value = '';
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
},
|
||||
|
||||
toggleAll: function(event) {
|
||||
var checked = event.nativeEvent.target.checked;
|
||||
|
||||
@@ -76,33 +76,8 @@ var transformer = {
|
||||
after: [simpleBannerify]
|
||||
};
|
||||
|
||||
var jasmine = {
|
||||
entries: [
|
||||
"./build/jasmine/all.js"
|
||||
],
|
||||
requires: {
|
||||
"jasmine": "./build/jasmine/all.js"
|
||||
},
|
||||
outfile: "./build/jasmine.js",
|
||||
debug: false
|
||||
};
|
||||
|
||||
var test = {
|
||||
entries: [
|
||||
"./build/modules/test/all.js",
|
||||
],
|
||||
requires: [
|
||||
"**/__tests__/*-test.js"
|
||||
],
|
||||
outfile: './build/react-test.js',
|
||||
debug: false,
|
||||
standalone: false
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
basic: basic,
|
||||
jasmine: jasmine,
|
||||
test: test,
|
||||
min: min,
|
||||
transformer: transformer
|
||||
};
|
||||
|
||||
@@ -12,6 +12,6 @@ module.exports = {
|
||||
return gzip.zip(contents, {}).length;
|
||||
}
|
||||
},
|
||||
cache: "build/.sizecache.json"
|
||||
cache: ".grunt/sizecache.json"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ var test = {
|
||||
"test/all.js",
|
||||
"**/__tests__/*.js"
|
||||
]),
|
||||
configFile: debug.configFile,
|
||||
configFile: "grunt/config/jsx/test.json",
|
||||
sourceDir: "src",
|
||||
outputDir: "build/modules"
|
||||
};
|
||||
|
||||
7
grunt/config/jsx/test.json
Normal file
7
grunt/config/jsx/test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"debug": true,
|
||||
"mocking": true,
|
||||
"constants": {
|
||||
"__DEV__": true
|
||||
}
|
||||
}
|
||||
24
grunt/config/populist.js
Normal file
24
grunt/config/populist.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
var jasmine = {
|
||||
rootDirectory: "build/jasmine",
|
||||
// This syntax means to require and expose the "jasmine" module
|
||||
// (build/jasmine/jasmine.js) as global.jasmine, and to require the
|
||||
// "all" module (build/jasmine/all.js) but not expose it globally.
|
||||
args: ["jasmine:jasmine", "all:"],
|
||||
outfile: "./build/jasmine.js"
|
||||
};
|
||||
|
||||
var test = {
|
||||
rootDirectory: "build/modules",
|
||||
args: ["test/all:"],
|
||||
requires: [
|
||||
"**/__tests__/*-test.js"
|
||||
],
|
||||
outfile: './build/react-test.js'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
jasmine: jasmine,
|
||||
test: test
|
||||
};
|
||||
@@ -1,16 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var exec = require("child_process").exec;
|
||||
var expand = require("grunt").file.expand;
|
||||
var grunt = require("grunt");
|
||||
var expand = grunt.file.expand;
|
||||
var spawn = grunt.util.spawn;
|
||||
|
||||
module.exports = function() {
|
||||
var done = this.async();
|
||||
var config = this.data;
|
||||
|
||||
var args = [
|
||||
"bin/jsx",
|
||||
"--cache-dir", ".module-cache",
|
||||
"--relativize",
|
||||
"--follow-requires",
|
||||
config.sourceDir,
|
||||
config.outputDir
|
||||
];
|
||||
@@ -25,5 +26,18 @@ module.exports = function() {
|
||||
args.push.apply(args, rootIDs);
|
||||
args.push("--config", config.configFile);
|
||||
|
||||
exec(args.join(" "), done);
|
||||
var child = spawn({
|
||||
cmd: "bin/jsx",
|
||||
args: args
|
||||
}, function(error, result, code) {
|
||||
if (error) {
|
||||
grunt.log.error(error);
|
||||
done(false);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stderr);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
var assert = require("assert");
|
||||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var tmp = require("tmp");
|
||||
var grunt = require("grunt");
|
||||
var spawn = grunt.util.spawn;
|
||||
|
||||
module.exports = function() {
|
||||
var config = this.data;
|
||||
var done = this.async();
|
||||
|
||||
function run(cmd, args, opts, callback) {
|
||||
@@ -25,7 +22,8 @@ module.exports = function() {
|
||||
|
||||
grunt.log.writeln("> " + cmd + " " + args.join(" "));
|
||||
|
||||
var proc = spawn({
|
||||
// var proc =
|
||||
spawn({
|
||||
cmd: cmd,
|
||||
args: args,
|
||||
opts: opts
|
||||
@@ -49,7 +47,7 @@ module.exports = function() {
|
||||
grunt.log.writeln("Packing " + tgz + " (this could take a while)...");
|
||||
|
||||
run("npm", ["pack", "--verbose", "."], function() {
|
||||
tmp.dir(function(err, dir) {
|
||||
require("tmp").dir(function(err, dir) {
|
||||
if (err) {
|
||||
grunt.log.error(err);
|
||||
done(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require("assert");
|
||||
var fs = require("fs");
|
||||
var grunt = require("grunt");
|
||||
var spawn = grunt.util.spawn;
|
||||
var semver = require("semver");
|
||||
@@ -38,7 +39,7 @@ function run(config, done) {
|
||||
}
|
||||
|
||||
args.push("--tests");
|
||||
var tests = grunt.file.expand({
|
||||
grunt.file.expand({
|
||||
nonull: true,
|
||||
cwd: "src"
|
||||
}, config.tests || []).forEach(function(file) {
|
||||
@@ -58,11 +59,13 @@ module.exports = function() {
|
||||
var config = this.data;
|
||||
var done = this.async();
|
||||
|
||||
spawn({
|
||||
cmd: phantomjs,
|
||||
args: ["--version"]
|
||||
}, function(error, result, code) {
|
||||
checkVersion(error, result, code);
|
||||
run(config, done);
|
||||
fs.chmod(phantomjs, 755, function(err) {
|
||||
spawn({
|
||||
cmd: phantomjs,
|
||||
args: ["--version"]
|
||||
}, function(error, result, code) {
|
||||
checkVersion(error, result, code);
|
||||
run(config, done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
28
grunt/tasks/populist.js
Normal file
28
grunt/tasks/populist.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var grunt = require('grunt');
|
||||
|
||||
module.exports = function() {
|
||||
var config = this.data;
|
||||
var done = this.async();
|
||||
|
||||
// create the bundle we'll work with
|
||||
var args = config.args;
|
||||
|
||||
// Make sure the things that need to be exposed are.
|
||||
var requires = config.requires || [];
|
||||
grunt.file.expand({
|
||||
nonull: true, // Keep IDs that don't expand to anything.
|
||||
cwd: config.rootDirectory
|
||||
}, requires).forEach(function(name) {
|
||||
args.push(name.replace(/\.js$/i, ""));
|
||||
});
|
||||
|
||||
require("populist").buildP({
|
||||
rootDirectory: config.rootDirectory,
|
||||
args: args
|
||||
}).then(function(output) {
|
||||
grunt.file.write(config.outfile, output);
|
||||
done();
|
||||
});
|
||||
};
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-tools",
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"jsx",
|
||||
@@ -21,7 +21,7 @@
|
||||
"bin/jsx",
|
||||
"build/modules/",
|
||||
"vendor/fbtransform/",
|
||||
"vendor/woodchipper.js"
|
||||
"vendor/constants.js"
|
||||
],
|
||||
"main": "main.js",
|
||||
"bin": {
|
||||
@@ -36,21 +36,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"base62": "~0.1.1",
|
||||
"commoner": "~0.7.0",
|
||||
"esprima": "https://github.com/facebook/esprima/tarball/ca28795124d45968e62a7b4b336d23a053ac3a84",
|
||||
"commoner": "~0.8.4",
|
||||
"esprima": "https://github.com/facebook/esprima/tarball/a3e0ea3979eb8d54d8bfade220c272903f928b1e",
|
||||
"recast": "~0.4.8",
|
||||
"source-map": "~0.1.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "~2.14.2",
|
||||
"browserify": "~2.24.1",
|
||||
"wrapup": "~0.12.0",
|
||||
"populist": "~0.1.3",
|
||||
"grunt-cli": "~0.1.9",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-jshint": "~0.5.4",
|
||||
"grunt-contrib-jshint": "~0.6.0",
|
||||
"optimist": "~0.4.0",
|
||||
"phantomjs": ">= 1.9.0",
|
||||
"semver": ">= 1.1.4",
|
||||
"semver": "~2.0.0",
|
||||
"uglify-js": "~2.3.6",
|
||||
"grunt-contrib-clean": "~0.4.1",
|
||||
"grunt-compare-size": "~0.4.0",
|
||||
@@ -58,5 +59,8 @@
|
||||
"tmp": "~0.0.18",
|
||||
"grunt-contrib-compress": "~0.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"preferGlobal": true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"require": false,
|
||||
"module": false
|
||||
"module": false,
|
||||
"exports": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ var ReactCompositeComponent = require('ReactCompositeComponent');
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactDOM = require('ReactDOM');
|
||||
var ReactMount = require('ReactMount');
|
||||
var ReactPropTypes = require('ReactPropTypes');
|
||||
var ReactServerRendering = require('ReactServerRendering');
|
||||
|
||||
var ReactDefaultInjection = require('ReactDefaultInjection');
|
||||
|
||||
@@ -29,15 +31,16 @@ ReactDefaultInjection.inject();
|
||||
|
||||
var React = {
|
||||
DOM: ReactDOM,
|
||||
PropTypes: ReactPropTypes,
|
||||
initializeTouchEvents: function(shouldUseTouch) {
|
||||
ReactMount.useTouchEvents = shouldUseTouch;
|
||||
},
|
||||
autoBind: ReactCompositeComponent.autoBind,
|
||||
createClass: ReactCompositeComponent.createClass,
|
||||
createComponentRenderer: ReactMount.createComponentRenderer,
|
||||
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
|
||||
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
|
||||
renderComponent: ReactMount.renderComponent,
|
||||
renderComponentToString: ReactServerRendering.renderComponentToString,
|
||||
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode,
|
||||
isValidComponent: ReactComponent.isValidComponent
|
||||
};
|
||||
|
||||
@@ -16,14 +16,17 @@
|
||||
* @providesModule ReactComponent
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
var ReactDOMIDOperations = require('ReactDOMIDOperations');
|
||||
var ReactID = require('ReactID');
|
||||
var ReactMount = require('ReactMount');
|
||||
var ReactOwner = require('ReactOwner');
|
||||
var ReactReconcileTransaction = require('ReactReconcileTransaction');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var keyMirror = require('keyMirror');
|
||||
@@ -35,6 +38,12 @@ var merge = require('merge');
|
||||
*/
|
||||
var OWNER = '{owner}';
|
||||
|
||||
/**
|
||||
* Props key that determines if a component's key was already validated.
|
||||
* @private
|
||||
*/
|
||||
var IS_KEY_VALIDATED = '{is.key.validated}';
|
||||
|
||||
/**
|
||||
* Every React component is in one of these life cycles.
|
||||
*/
|
||||
@@ -50,6 +59,78 @@ var ComponentLifeCycle = keyMirror({
|
||||
UNMOUNTED: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Warn if there's no key explicitly set on dynamic arrays of children.
|
||||
* This allows us to keep track of children between updates.
|
||||
*/
|
||||
|
||||
var ownerHasWarned = {};
|
||||
|
||||
/**
|
||||
* Warn if the component doesn't have an explicit key assigned to it.
|
||||
* This component is in an array. The array could grow and shrink or be
|
||||
* reordered. All children, that hasn't already been validated, are required to
|
||||
* have a "key" property assigned to it.
|
||||
*
|
||||
* @internal
|
||||
* @param {ReactComponent} component Component that requires a key.
|
||||
*/
|
||||
function validateExplicitKey(component) {
|
||||
if (component[IS_KEY_VALIDATED] || component.props.key != null) {
|
||||
return;
|
||||
}
|
||||
component[IS_KEY_VALIDATED] = true;
|
||||
|
||||
// We can't provide friendly warnings for top level components.
|
||||
if (!ReactCurrentOwner.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Name of the component whose render method tried to pass children.
|
||||
var currentName = ReactCurrentOwner.current.constructor.displayName;
|
||||
if (ownerHasWarned.hasOwnProperty(currentName)) {
|
||||
return;
|
||||
}
|
||||
ownerHasWarned[currentName] = true;
|
||||
|
||||
var message = 'Each child in an array should have a unique "key" prop. ' +
|
||||
'Check the render method of ' + currentName + '.';
|
||||
if (!component.isOwnedBy(ReactCurrentOwner.current)) {
|
||||
// Name of the component that originally created this child.
|
||||
var childOwnerName =
|
||||
component.props[OWNER] && component.props[OWNER].constructor.displayName;
|
||||
|
||||
// Usually the current owner is the offender, but if it accepts
|
||||
// children as a property, it may be the creator of the child that's
|
||||
// responsible for assigning it a key.
|
||||
message += ' It was passed a child from ' + childOwnerName + '.';
|
||||
}
|
||||
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that every component either is passed in a static location or, if
|
||||
* if it's passed in an array, has an explicit key property defined.
|
||||
*
|
||||
* @internal
|
||||
* @param {*} component Statically passed child of any type.
|
||||
* @return {boolean}
|
||||
*/
|
||||
function validateChildKeys(component) {
|
||||
if (Array.isArray(component)) {
|
||||
for (var i = 0; i < component.length; i++) {
|
||||
var child = component[i];
|
||||
if (ReactComponent.isValidComponent(child)) {
|
||||
validateExplicitKey(child);
|
||||
}
|
||||
}
|
||||
} else if (ReactComponent.isValidComponent(component)) {
|
||||
// This component was passed in a valid location.
|
||||
component[IS_KEY_VALIDATED] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Components are the basic units of composition in React.
|
||||
*
|
||||
@@ -90,6 +171,23 @@ var ReactComponent = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a key string that identifies a component within a set.
|
||||
*
|
||||
* @param {*} component A component that could contain a manual key.
|
||||
* @param {number} index Index that is used if a manual key is not provided.
|
||||
* @return {string}
|
||||
* @internal
|
||||
*/
|
||||
getKey: function(component, index) {
|
||||
if (component && component.props && component.props.key != null) {
|
||||
// Explicit key
|
||||
return '' + component.props.key;
|
||||
}
|
||||
// Implicit key determined by the index in the set
|
||||
return '' + index;
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -134,53 +232,57 @@ var ReactComponent = {
|
||||
*/
|
||||
Mixin: {
|
||||
|
||||
/**
|
||||
* Checks whether or not this component is mounted.
|
||||
*
|
||||
* @return {boolean} True if mounted, false otherwise.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
isMounted: function() {
|
||||
return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the DOM node rendered by this component.
|
||||
*
|
||||
* @return {?DOMElement} The root node of this component.
|
||||
* @return {DOMElement} The root node of this component.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
getDOMNode: function() {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'getDOMNode(): The DOM is not supported in the current environment.'
|
||||
);
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
this.isMounted(),
|
||||
'getDOMNode(): A component must be mounted to have a DOM node.'
|
||||
);
|
||||
var rootNode = this._rootNode;
|
||||
if (!rootNode) {
|
||||
rootNode = document.getElementById(this._rootNodeID);
|
||||
if (!rootNode) {
|
||||
// TODO: Log the frequency that we reach this path.
|
||||
rootNode = ReactMount.findReactRenderedDOMNodeSlow(this._rootNodeID);
|
||||
}
|
||||
this._rootNode = rootNode;
|
||||
}
|
||||
return rootNode;
|
||||
return ReactID.getNode(this._rootNodeID);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a subset of the props.
|
||||
*
|
||||
* @param {object} partialProps Subset of the next props.
|
||||
* @param {?function} callback Called after props are updated.
|
||||
* @final
|
||||
* @public
|
||||
*/
|
||||
setProps: function(partialProps) {
|
||||
this.replaceProps(merge(this.props, partialProps));
|
||||
setProps: function(partialProps, callback) {
|
||||
// Merge with `_pendingProps` if it exists, otherwise with existing props.
|
||||
this.replaceProps(
|
||||
merge(this._pendingProps || this.props, partialProps),
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces all of the props.
|
||||
*
|
||||
* @param {object} props New props.
|
||||
* @param {?function} callback Called after props are updated.
|
||||
* @final
|
||||
* @public
|
||||
*/
|
||||
replaceProps: function(props) {
|
||||
replaceProps: function(props, callback) {
|
||||
invariant(
|
||||
!this.props[OWNER],
|
||||
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
|
||||
@@ -189,9 +291,8 @@ var ReactComponent = {
|
||||
'`render` method to pass the correct value as props to the component ' +
|
||||
'where it is created.'
|
||||
);
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(this.receiveProps, this, props, transaction);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
this._pendingProps = props;
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -206,13 +307,31 @@ var ReactComponent = {
|
||||
*/
|
||||
construct: function(initialProps, children) {
|
||||
this.props = initialProps || {};
|
||||
if (typeof children !== 'undefined') {
|
||||
this.props.children = children;
|
||||
}
|
||||
// Record the component responsible for creating this component.
|
||||
this.props[OWNER] = ReactCurrentOwner.current;
|
||||
// All components start unmounted.
|
||||
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
||||
|
||||
this._pendingProps = null;
|
||||
this._pendingCallbacks = null;
|
||||
|
||||
// Children can be more than one argument
|
||||
var childrenLength = arguments.length - 1;
|
||||
if (childrenLength === 1) {
|
||||
if (__DEV__) {
|
||||
validateChildKeys(children);
|
||||
}
|
||||
this.props.children = children;
|
||||
} else if (childrenLength > 1) {
|
||||
var childArray = Array(childrenLength);
|
||||
for (var i = 0; i < childrenLength; i++) {
|
||||
if (__DEV__) {
|
||||
validateChildKeys(arguments[i + 1]);
|
||||
}
|
||||
childArray[i] = arguments[i + 1];
|
||||
}
|
||||
this.props.children = childArray;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -230,7 +349,7 @@ var ReactComponent = {
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.UNMOUNTED,
|
||||
!this.isMounted(),
|
||||
'mountComponent(%s, ...): Can only mount an unmounted component.',
|
||||
rootID
|
||||
);
|
||||
@@ -255,14 +374,14 @@ var ReactComponent = {
|
||||
*/
|
||||
unmountComponent: function() {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
this.isMounted(),
|
||||
'unmountComponent(): Can only unmount a mounted component.'
|
||||
);
|
||||
var props = this.props;
|
||||
if (props.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
|
||||
}
|
||||
this._rootNode = null;
|
||||
ReactID.purgeID(this._rootNodeID);
|
||||
this._rootNodeID = null;
|
||||
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
||||
},
|
||||
@@ -279,20 +398,62 @@ var ReactComponent = {
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
this.isMounted(),
|
||||
'receiveProps(...): Can only update a mounted component.'
|
||||
);
|
||||
this._pendingProps = nextProps;
|
||||
this._performUpdateIfNecessary(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call `_performUpdateIfNecessary` within a new transaction.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
performUpdateIfNecessary: function() {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(this._performUpdateIfNecessary, this, transaction);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* If `_pendingProps` is set, update the component.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
_performUpdateIfNecessary: function(transaction) {
|
||||
if (this._pendingProps == null) {
|
||||
return;
|
||||
}
|
||||
var prevProps = this.props;
|
||||
this.props = this._pendingProps;
|
||||
this._pendingProps = null;
|
||||
this.updateComponent(transaction, prevProps);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the component's currently mounted representation.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @internal
|
||||
*/
|
||||
updateComponent: function(transaction, prevProps) {
|
||||
var props = this.props;
|
||||
// If either the owner or a `ref` has changed, make sure the newest owner
|
||||
// has stored a reference to `this`, and the previous owner (if different)
|
||||
// has forgotten the reference to `this`.
|
||||
if (nextProps[OWNER] !== props[OWNER] || nextProps.ref !== props.ref) {
|
||||
if (props.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
|
||||
if (props[OWNER] !== prevProps[OWNER] || props.ref !== prevProps.ref) {
|
||||
if (prevProps.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(
|
||||
this, prevProps.ref, prevProps[OWNER]
|
||||
);
|
||||
}
|
||||
// Correct, even if the owner is the same, and only the ref has changed.
|
||||
if (nextProps.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, nextProps.ref, nextProps[OWNER]);
|
||||
if (props.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, props.ref, props[OWNER]);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -302,18 +463,20 @@ var ReactComponent = {
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
||||
* @final
|
||||
* @internal
|
||||
* @see {ReactMount.renderComponent}
|
||||
*/
|
||||
mountComponentIntoNode: function(rootID, container) {
|
||||
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._mountComponentIntoNode,
|
||||
this,
|
||||
rootID,
|
||||
container,
|
||||
transaction
|
||||
transaction,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
@@ -322,14 +485,27 @@ var ReactComponent = {
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
||||
* @final
|
||||
* @private
|
||||
*/
|
||||
_mountComponentIntoNode: function(rootID, container, transaction) {
|
||||
_mountComponentIntoNode: function(
|
||||
rootID,
|
||||
container,
|
||||
transaction,
|
||||
shouldReuseMarkup) {
|
||||
invariant(
|
||||
container && container.nodeType === 1,
|
||||
'mountComponentIntoNode(...): Target container is not a DOM element.'
|
||||
);
|
||||
var renderStart = Date.now();
|
||||
var markup = this.mountComponent(rootID, transaction);
|
||||
ReactMount.totalInstantiationTime += (Date.now() - renderStart);
|
||||
|
||||
if (shouldReuseMarkup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var injectionStart = Date.now();
|
||||
// Asynchronously inject markup by ensuring that the container is not in
|
||||
// the document when settings its `innerHTML`.
|
||||
@@ -375,30 +551,26 @@ var ReactComponent = {
|
||||
*/
|
||||
isOwnedBy: function(owner) {
|
||||
return this.props[OWNER] === owner;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets another component, that shares the same owner as this one, by ref.
|
||||
*
|
||||
* @param {string} ref of a sibling Component.
|
||||
* @return {?ReactComponent} the actual sibling Component.
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
getSiblingByRef: function(ref) {
|
||||
var owner = this.props[OWNER];
|
||||
if (!owner || !owner.refs) {
|
||||
return null;
|
||||
}
|
||||
return owner.refs[ref];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function logDeprecated(msg) {
|
||||
if (__DEV__) {
|
||||
throw new Error(msg);
|
||||
} else {
|
||||
console && console.warn && console.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
ReactComponent.Mixin.update = function(props) {
|
||||
logDeprecated('this.update() is deprecated. Use this.setProps()');
|
||||
this.setProps(props);
|
||||
};
|
||||
ReactComponent.Mixin.updateAll = function(props) {
|
||||
logDeprecated('this.updateAll() is deprecated. Use this.replaceProps()');
|
||||
this.replaceProps(props);
|
||||
};
|
||||
|
||||
module.exports = ReactComponent;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactComponentWithPureRender
|
||||
* @providesModule ReactComponentWithPureRenderMixin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -27,9 +27,10 @@
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var ReactComponentWithPureRender = require('ReactComponentWithPureRender');
|
||||
* var ReactComponentWithPureRender =
|
||||
* require('ReactComponentWithPureRenderMixin');
|
||||
* React.createClass({
|
||||
* mixins: [ReactComponentWithPureRender],
|
||||
* mixins: [ReactComponentWithPureRenderMixin],
|
||||
*
|
||||
* render: function() {
|
||||
* return <div className={this.props.className}>foo</div>;
|
||||
@@ -41,7 +42,7 @@
|
||||
* differences. Only mixin to components which have simple props and state, or
|
||||
* use `forceUpdate()` when you know deep data structures have changed.
|
||||
*/
|
||||
var ReactComponentWithPureRender = {
|
||||
var ReactComponentWithPureRenderMixin = {
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
return !shallowEqual(this.props, nextProps) ||
|
||||
!shallowEqual(this.state, nextState);
|
||||
@@ -74,4 +75,4 @@ function shallowEqual(objA, objB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = ReactComponentWithPureRender;
|
||||
module.exports = ReactComponentWithPureRenderMixin;
|
||||
|
||||
@@ -22,6 +22,7 @@ var ReactComponent = require('ReactComponent');
|
||||
var ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
var ReactOwner = require('ReactOwner');
|
||||
var ReactPropTransferer = require('ReactPropTransferer');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var keyMirror = require('keyMirror');
|
||||
@@ -80,17 +81,29 @@ var ReactCompositeComponentInterface = {
|
||||
mixins: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Definition of props for this component.
|
||||
* Definition of prop types for this component.
|
||||
*
|
||||
* @type {array}
|
||||
* @type {object}
|
||||
* @optional
|
||||
*/
|
||||
props: SpecPolicy.DEFINE_ONCE,
|
||||
propTypes: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
|
||||
|
||||
// ==== Definition methods ====
|
||||
|
||||
/**
|
||||
* Invoked when the component is mounted. Values in the mapping will be set on
|
||||
* `this.props` if that prop is not specified (i.e. using an `in` check).
|
||||
*
|
||||
* This method is invoked before `getInitialState` and therefore cannot rely
|
||||
* on `this.state` or use `this.setState`.
|
||||
*
|
||||
* @return {object}
|
||||
* @optional
|
||||
*/
|
||||
getDefaultProps: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
/**
|
||||
* Invoked once before the component is mounted. The return value will be used
|
||||
* as the initial value of `this.state`.
|
||||
@@ -265,11 +278,53 @@ var RESERVED_SPEC_KEYS = {
|
||||
}
|
||||
}
|
||||
},
|
||||
props: function(Constructor, props) {
|
||||
Constructor.propDeclarations = props;
|
||||
propTypes: function(Constructor, propTypes) {
|
||||
Constructor.propTypes = propTypes;
|
||||
}
|
||||
};
|
||||
|
||||
function validateMethodOverride(proto, name) {
|
||||
var specPolicy = ReactCompositeComponentInterface[name];
|
||||
|
||||
// Disallow overriding of base class methods unless explicitly allowed.
|
||||
if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.OVERRIDE_BASE,
|
||||
'ReactCompositeComponentInterface: You are attempting to override ' +
|
||||
'`%s` from your class specification. Ensure that your method names ' +
|
||||
'do not overlap with React methods.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow defining methods more than once unless explicitly allowed.
|
||||
if (proto.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.DEFINE_MANY,
|
||||
'ReactCompositeComponentInterface: You are attempting to define ' +
|
||||
'`%s` on your component more than once. This conflict may be due ' +
|
||||
'to a mixin.',
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function validateLifeCycleOnReplaceState(instance) {
|
||||
var compositeLifeCycleState = instance._compositeLifeCycleState;
|
||||
invariant(
|
||||
instance.isMounted() ||
|
||||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
|
||||
'replaceState(...): Can only update a mounted or mounting component.'
|
||||
);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'replaceState(...): Cannot update while unmounting component or during ' +
|
||||
'an existing state transition (such as within `render`).'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom version of `mixInto` which handles policy validation and reserved
|
||||
* specification keys when building `ReactCompositeComponent` classses.
|
||||
@@ -277,58 +332,44 @@ var RESERVED_SPEC_KEYS = {
|
||||
function mixSpecIntoComponent(Constructor, spec) {
|
||||
var proto = Constructor.prototype;
|
||||
for (var name in spec) {
|
||||
if (!spec.hasOwnProperty(name)) {
|
||||
var property = spec[name];
|
||||
if (!spec.hasOwnProperty(name) || !property) {
|
||||
continue;
|
||||
}
|
||||
var property = spec[name];
|
||||
var specPolicy = ReactCompositeComponentInterface[name];
|
||||
|
||||
// Disallow overriding of base class methods unless explicitly allowed.
|
||||
if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.OVERRIDE_BASE,
|
||||
'ReactCompositeComponentInterface: You are attempting to override ' +
|
||||
'`%s` from your class specification. Ensure that your method names ' +
|
||||
'do not overlap with React methods.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow using `React.autoBind` on internal methods.
|
||||
if (specPolicy != null) {
|
||||
invariant(
|
||||
!property || !property.__reactAutoBind,
|
||||
'ReactCompositeComponentInterface: You are attempting to use ' +
|
||||
'`React.autoBind` on `%s`, a method that is internal to React.' +
|
||||
'Internal methods are called with the component as the context.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow defining methods more than once unless explicitly allowed.
|
||||
if (proto.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.DEFINE_MANY,
|
||||
'ReactCompositeComponentInterface: You are attempting to define ' +
|
||||
'`%s` on your component more than once. This conflict may be due ' +
|
||||
'to a mixin.',
|
||||
name
|
||||
);
|
||||
}
|
||||
validateMethodOverride(proto, name);
|
||||
|
||||
if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
|
||||
RESERVED_SPEC_KEYS[name](Constructor, property);
|
||||
} else if (property && property.__reactAutoBind) {
|
||||
if (!proto.__reactAutoBindMap) {
|
||||
proto.__reactAutoBindMap = {};
|
||||
}
|
||||
proto.__reactAutoBindMap[name] = property.__reactAutoBind;
|
||||
} else if (proto.hasOwnProperty(name)) {
|
||||
// For methods which are defined more than once, call the existing methods
|
||||
// before calling the new property.
|
||||
proto[name] = createChainedFunction(proto[name], property);
|
||||
} else {
|
||||
proto[name] = property;
|
||||
// Setup methods on prototype:
|
||||
// The following member methods should not be automatically bound:
|
||||
// 1. Expected ReactCompositeComponent methods (in the "interface").
|
||||
// 2. Overridden methods (that were mixed in).
|
||||
var isCompositeComponentMethod = name in ReactCompositeComponentInterface;
|
||||
var isInherited = name in proto;
|
||||
var markedDontBind = property.__reactDontBind;
|
||||
var isFunction = typeof property === 'function';
|
||||
var shouldAutoBind =
|
||||
isFunction &&
|
||||
!isCompositeComponentMethod &&
|
||||
!isInherited &&
|
||||
!markedDontBind;
|
||||
|
||||
if (shouldAutoBind) {
|
||||
if (!proto.__reactAutoBindMap) {
|
||||
proto.__reactAutoBindMap = {};
|
||||
}
|
||||
proto.__reactAutoBindMap[name] = property;
|
||||
proto[name] = property;
|
||||
} else {
|
||||
if (isInherited) {
|
||||
// For methods which are defined more than once, call the existing
|
||||
// methods before calling the new property.
|
||||
proto[name] = createChainedFunction(proto[name], property);
|
||||
} else {
|
||||
proto[name] = property;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,13 +383,9 @@ function mixSpecIntoComponent(Constructor, spec) {
|
||||
* @private
|
||||
*/
|
||||
function createChainedFunction(one, two) {
|
||||
return function chainedFunction(a, b, c, d, e, tooMany) {
|
||||
invariant(
|
||||
typeof tooMany === 'undefined',
|
||||
'Chained function can only take a maximum of 5 arguments.'
|
||||
);
|
||||
one.call(this, a, b, c, d, e);
|
||||
two.call(this, a, b, c, d, e);
|
||||
return function chainedFunction() {
|
||||
one.apply(this, arguments);
|
||||
two.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -357,7 +394,26 @@ function createChainedFunction(one, two) {
|
||||
* `this._compositeLifeCycleState` (which can be null).
|
||||
*
|
||||
* This is different from the life cycle state maintained by `ReactComponent` in
|
||||
* `this._lifeCycleState`.
|
||||
* `this._lifeCycleState`. The following diagram shows how the states overlap in
|
||||
* time. There are times when the CompositeLifeCycle is null - at those times it
|
||||
* is only meaningful to look at ComponentLifeCycle alone.
|
||||
*
|
||||
* Top Row: ReactComponent.ComponentLifeCycle
|
||||
* Low Row: ReactComponent.CompositeLifeCycle
|
||||
*
|
||||
* +-------+------------------------------------------------------+--------+
|
||||
* | UN | MOUNTED | UN |
|
||||
* |MOUNTED| | MOUNTED|
|
||||
* +-------+------------------------------------------------------+--------+
|
||||
* | ^--------+ +------+ +------+ +------+ +--------^ |
|
||||
* | | | | | | | | | | | |
|
||||
* | 0--|MOUNTING|-0-|RECEIV|-0-|RECEIV|-0-|RECEIV|-0-| UN |--->0 |
|
||||
* | | | |PROPS | | PROPS| | STATE| |MOUNTING| |
|
||||
* | | | | | | | | | | | |
|
||||
* | | | | | | | | | | | |
|
||||
* | +--------+ +------+ +------+ +------+ +--------+ |
|
||||
* | | | |
|
||||
* +-------+------------------------------------------------------+--------+
|
||||
*/
|
||||
var CompositeLifeCycle = keyMirror({
|
||||
/**
|
||||
@@ -396,12 +452,24 @@ var ReactCompositeComponentMixin = {
|
||||
* @internal
|
||||
*/
|
||||
construct: function(initialProps, children) {
|
||||
ReactComponent.Mixin.construct.call(this, initialProps, children);
|
||||
// Children can be either an array or more than one argument
|
||||
ReactComponent.Mixin.construct.apply(this, arguments);
|
||||
this.state = null;
|
||||
this._pendingState = null;
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether or not this composite component is mounted.
|
||||
* @return {boolean} True if mounted, false otherwise.
|
||||
* @protected
|
||||
* @final
|
||||
*/
|
||||
isMounted: function() {
|
||||
return ReactComponent.Mixin.isMounted.call(this) &&
|
||||
this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the component, renders markup, and registers event listeners.
|
||||
*
|
||||
@@ -413,14 +481,10 @@ var ReactCompositeComponentMixin = {
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
|
||||
|
||||
// Unset `this._lifeCycleState` until after this method is finished.
|
||||
this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED;
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
|
||||
|
||||
if (this.constructor.propDeclarations) {
|
||||
this._assertValidProps(this.props);
|
||||
}
|
||||
this._defaultProps = this.getDefaultProps ? this.getDefaultProps() : null;
|
||||
this._processProps(this.props);
|
||||
|
||||
if (this.__reactAutoBindMap) {
|
||||
this._bindAutoBindMethods();
|
||||
@@ -428,6 +492,7 @@ var ReactCompositeComponentMixin = {
|
||||
|
||||
this.state = this.getInitialState ? this.getInitialState() : null;
|
||||
this._pendingState = null;
|
||||
this._pendingForceUpdate = false;
|
||||
|
||||
if (this.componentWillMount) {
|
||||
this.componentWillMount();
|
||||
@@ -439,17 +504,15 @@ var ReactCompositeComponentMixin = {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.componentDidMount) {
|
||||
transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
|
||||
}
|
||||
|
||||
this._renderedComponent = this._renderValidatedComponent();
|
||||
|
||||
// Done with mounting, `setState` will now trigger UI changes.
|
||||
this._compositeLifeCycleState = null;
|
||||
this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED;
|
||||
|
||||
return this._renderedComponent.mountComponent(rootID, transaction);
|
||||
var markup = this._renderedComponent.mountComponent(rootID, transaction);
|
||||
if (this.componentDidMount) {
|
||||
transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
|
||||
}
|
||||
return markup;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -465,6 +528,8 @@ var ReactCompositeComponentMixin = {
|
||||
}
|
||||
this._compositeLifeCycleState = null;
|
||||
|
||||
this._defaultProps = null;
|
||||
|
||||
ReactComponent.Mixin.unmountComponent.call(this);
|
||||
this._renderedComponent.unmountComponent();
|
||||
this._renderedComponent = null;
|
||||
@@ -479,33 +544,6 @@ var ReactCompositeComponentMixin = {
|
||||
// TODO: this.state = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the rendered DOM nodes given a new set of props.
|
||||
*
|
||||
* @param {object} nextProps Next set of properties.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
if (this.constructor.propDeclarations) {
|
||||
this._assertValidProps(nextProps);
|
||||
}
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
|
||||
if (this.componentWillReceiveProps) {
|
||||
this.componentWillReceiveProps(nextProps, transaction);
|
||||
}
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
// When receiving props, calls to `setState` by `componentWillReceiveProps`
|
||||
// will set `this._pendingState` without triggering a re-render.
|
||||
var nextState = this._pendingState || this.state;
|
||||
this._pendingState = null;
|
||||
this._receivePropsAndState(nextProps, nextState, transaction);
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a subset of the state. Always use this or `replaceState` to mutate
|
||||
* state. You should treat `this.state` as immutable.
|
||||
@@ -513,13 +551,22 @@ var ReactCompositeComponentMixin = {
|
||||
* There is no guarantee that `this.state` will be immediately updated, so
|
||||
* accessing `this.state` after calling this method may return the old value.
|
||||
*
|
||||
* There is no guarantee that calls to `setState` will run synchronously,
|
||||
* as they may eventually be batched together. You can provide an optional
|
||||
* callback that will be executed when the call to setState is actually
|
||||
* completed.
|
||||
*
|
||||
* @param {object} partialState Next partial state to be merged with state.
|
||||
* @param {?function} callback Called after state is updated.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
setState: function(partialState) {
|
||||
setState: function(partialState, callback) {
|
||||
// Merge with `_pendingState` if it exists, otherwise with existing state.
|
||||
this.replaceState(merge(this._pendingState || this.state, partialState));
|
||||
this.replaceState(
|
||||
merge(this._pendingState || this.state, partialState),
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -530,60 +577,89 @@ var ReactCompositeComponentMixin = {
|
||||
* accessing `this.state` after calling this method may return the old value.
|
||||
*
|
||||
* @param {object} completeState Next state.
|
||||
* @param {?function} callback Called after state is updated.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
replaceState: function(completeState) {
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
invariant(
|
||||
this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
|
||||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
|
||||
'replaceState(...): Can only update a mounted (or mounting) component.'
|
||||
);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'replaceState(...): Cannot update while unmounting component or during ' +
|
||||
'an existing state transition (such as within `render`).'
|
||||
);
|
||||
|
||||
replaceState: function(completeState, callback) {
|
||||
validateLifeCycleOnReplaceState(this);
|
||||
this._pendingState = completeState;
|
||||
|
||||
// Do not trigger a state transition if we are in the middle of mounting or
|
||||
// receiving props because both of those will already be doing this.
|
||||
if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
|
||||
var nextState = this._pendingState;
|
||||
this._pendingState = null;
|
||||
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._receivePropsAndState,
|
||||
this,
|
||||
this.props,
|
||||
nextState,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
this._compositeLifeCycleState = null;
|
||||
}
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Receives next props and next state, and negotiates whether or not the
|
||||
* component should update as a result.
|
||||
* Processes props by setting default values for unspecified props and
|
||||
* asserting that the props are valid.
|
||||
*
|
||||
* @param {object} nextProps Next object to set as props.
|
||||
* @param {?object} nextState Next object to set as state.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} props
|
||||
* @private
|
||||
*/
|
||||
_receivePropsAndState: function(nextProps, nextState, transaction) {
|
||||
if (!this.shouldComponentUpdate ||
|
||||
_processProps: function(props) {
|
||||
var propName;
|
||||
var defaultProps = this._defaultProps;
|
||||
for (propName in defaultProps) {
|
||||
if (!(propName in props)) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
var propTypes = this.constructor.propTypes;
|
||||
if (propTypes) {
|
||||
var componentName = this.constructor.displayName;
|
||||
for (propName in propTypes) {
|
||||
var checkProp = propTypes[propName];
|
||||
if (checkProp) {
|
||||
checkProp(props, propName, componentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
performUpdateIfNecessary: function() {
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
// Do not trigger a state transition if we are in the middle of mounting or
|
||||
// receiving props because both of those will already be doing this.
|
||||
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
|
||||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
|
||||
return;
|
||||
}
|
||||
ReactComponent.Mixin.performUpdateIfNecessary.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* If any of `_pendingProps`, `_pendingState`, or `_pendingForceUpdate` is
|
||||
* set, update the component.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
_performUpdateIfNecessary: function(transaction) {
|
||||
if (this._pendingProps == null &&
|
||||
this._pendingState == null &&
|
||||
!this._pendingForceUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextProps = this.props;
|
||||
if (this._pendingProps != null) {
|
||||
nextProps = this._pendingProps;
|
||||
this._processProps(nextProps);
|
||||
this._pendingProps = null;
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
|
||||
if (this.componentWillReceiveProps) {
|
||||
this.componentWillReceiveProps(nextProps, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
|
||||
var nextState = this._pendingState || this.state;
|
||||
this._pendingState = null;
|
||||
|
||||
if (this._pendingForceUpdate ||
|
||||
!this.shouldComponentUpdate ||
|
||||
this.shouldComponentUpdate(nextProps, nextState)) {
|
||||
this._pendingForceUpdate = false;
|
||||
// Will set `this.props` and `this.state`.
|
||||
this._performComponentUpdate(nextProps, nextState, transaction);
|
||||
} else {
|
||||
@@ -592,6 +668,8 @@ var ReactCompositeComponentMixin = {
|
||||
this.props = nextProps;
|
||||
this.state = nextState;
|
||||
}
|
||||
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -614,7 +692,7 @@ var ReactCompositeComponentMixin = {
|
||||
this.props = nextProps;
|
||||
this.state = nextState;
|
||||
|
||||
this.updateComponent(transaction);
|
||||
this.updateComponent(transaction, prevProps, prevState);
|
||||
|
||||
if (this.componentDidUpdate) {
|
||||
transaction.getReactOnDOMReady().enqueue(
|
||||
@@ -631,16 +709,17 @@ var ReactCompositeComponentMixin = {
|
||||
* Sophisticated clients may wish to override this.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @param {?object} prevState
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
updateComponent: function(transaction) {
|
||||
updateComponent: function(transaction, prevProps, prevState) {
|
||||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
|
||||
var currentComponent = this._renderedComponent;
|
||||
var nextComponent = this._renderValidatedComponent();
|
||||
if (currentComponent.constructor === nextComponent.constructor) {
|
||||
if (!nextComponent.props.isStatic) {
|
||||
currentComponent.receiveProps(nextComponent.props, transaction);
|
||||
}
|
||||
currentComponent.receiveProps(nextComponent.props, transaction);
|
||||
} else {
|
||||
// These two IDs are actually the same! But nothing should rely on that.
|
||||
var thisID = this._rootNodeID;
|
||||
@@ -665,28 +744,42 @@ var ReactCompositeComponentMixin = {
|
||||
* This will not invoke `shouldUpdateComponent`, but it will invoke
|
||||
* `componentWillUpdate` and `componentDidUpdate`.
|
||||
*
|
||||
* @param {?function} callback Called after update is complete.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
forceUpdate: function() {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._performComponentUpdate,
|
||||
this,
|
||||
this.props,
|
||||
this.state,
|
||||
transaction
|
||||
forceUpdate: function(callback) {
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
invariant(
|
||||
this.isMounted() ||
|
||||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
|
||||
'forceUpdate(...): Can only force an update on mounted or mounting ' +
|
||||
'components.'
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'forceUpdate(...): Cannot force an update while unmounting component ' +
|
||||
'or during an existing state transition (such as within `render`).'
|
||||
);
|
||||
this._pendingForceUpdate = true;
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderValidatedComponent: function() {
|
||||
var renderedComponent;
|
||||
ReactCurrentOwner.current = this;
|
||||
var renderedComponent = this.render();
|
||||
ReactCurrentOwner.current = null;
|
||||
try {
|
||||
renderedComponent = this.render();
|
||||
} catch (error) {
|
||||
// IE8 requires `catch` in order to use `finally`.
|
||||
throw error;
|
||||
} finally {
|
||||
ReactCurrentOwner.current = null;
|
||||
}
|
||||
invariant(
|
||||
ReactComponent.isValidComponent(renderedComponent),
|
||||
'%s.render(): A valid ReactComponent must be returned.',
|
||||
@@ -695,21 +788,6 @@ var ReactCompositeComponentMixin = {
|
||||
return renderedComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
* @private
|
||||
*/
|
||||
_assertValidProps: function(props) {
|
||||
var propDeclarations = this.constructor.propDeclarations;
|
||||
var componentName = this.constructor.displayName;
|
||||
for (var propName in propDeclarations) {
|
||||
var checkProp = propDeclarations[propName];
|
||||
if (checkProp) {
|
||||
checkProp(props, propName, componentName);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -731,29 +809,34 @@ var ReactCompositeComponentMixin = {
|
||||
*/
|
||||
_bindAutoBindMethod: function(method) {
|
||||
var component = this;
|
||||
var hasWarned = false;
|
||||
function autoBound(a, b, c, d, e, tooMany) {
|
||||
invariant(
|
||||
typeof tooMany === 'undefined',
|
||||
'React.autoBind(...): Methods can only take a maximum of 5 arguments.'
|
||||
);
|
||||
if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) {
|
||||
return method.call(component, a, b, c, d, e);
|
||||
} else if (!hasWarned) {
|
||||
hasWarned = true;
|
||||
if (__DEV__) {
|
||||
var boundMethod = function() {
|
||||
return method.apply(component, arguments);
|
||||
};
|
||||
if (__DEV__) {
|
||||
var componentName = component.constructor.displayName;
|
||||
var _bind = boundMethod.bind;
|
||||
boundMethod.bind = function(newThis) {
|
||||
// User is trying to bind() an autobound method; we effectively will
|
||||
// ignore the value of "this" that the user is trying to use, so
|
||||
// let's warn.
|
||||
if (newThis !== component) {
|
||||
console.warn(
|
||||
'React.autoBind(...): Attempted to invoke an auto-bound method ' +
|
||||
'on an unmounted instance of `%s`. You either have a memory leak ' +
|
||||
'or an event handler that is being run after unmounting.',
|
||||
component.constructor.displayName || 'ReactCompositeComponent'
|
||||
'bind(): React component methods may only be bound to the ' +
|
||||
'component instance. See ' + componentName
|
||||
);
|
||||
} else if (arguments.length === 1) {
|
||||
console.warn(
|
||||
'bind(): You are binding a component method to the component. ' +
|
||||
'React does this for you automatically in a high-performance ' +
|
||||
'way, so you can safely remove this call. See ' + componentName
|
||||
);
|
||||
return boundMethod;
|
||||
}
|
||||
}
|
||||
return _bind.apply(boundMethod, arguments);
|
||||
};
|
||||
}
|
||||
return autoBound;
|
||||
return boundMethod;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var ReactCompositeComponentBase = function() {};
|
||||
@@ -784,9 +867,7 @@ var ReactCompositeComponent = {
|
||||
* @public
|
||||
*/
|
||||
createClass: function(spec) {
|
||||
var Constructor = function(initialProps, children) {
|
||||
this.construct(initialProps, children);
|
||||
};
|
||||
var Constructor = function() {};
|
||||
Constructor.prototype = new ReactCompositeComponentBase();
|
||||
Constructor.prototype.constructor = Constructor;
|
||||
mixSpecIntoComponent(Constructor, spec);
|
||||
@@ -794,9 +875,17 @@ var ReactCompositeComponent = {
|
||||
Constructor.prototype.render,
|
||||
'createClass(...): Class specification must implement a `render` method.'
|
||||
);
|
||||
// Reduce time spent doing lookups by setting these on the prototype.
|
||||
for (var methodName in ReactCompositeComponentInterface) {
|
||||
if (!Constructor.prototype[methodName]) {
|
||||
Constructor.prototype[methodName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
var ConvenienceConstructor = function(props, children) {
|
||||
return new Constructor(props, children);
|
||||
var instance = new Constructor();
|
||||
instance.construct.apply(instance, arguments);
|
||||
return instance;
|
||||
};
|
||||
ConvenienceConstructor.componentConstructor = Constructor;
|
||||
ConvenienceConstructor.originalSpec = spec;
|
||||
@@ -804,33 +893,23 @@ var ReactCompositeComponent = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks the provided method to be automatically bound to the component.
|
||||
* This means the method's context will always be the component.
|
||||
*
|
||||
* React.createClass({
|
||||
* handleClick: React.autoBind(function() {
|
||||
* this.setState({jumping: true});
|
||||
* }),
|
||||
* render: function() {
|
||||
* return <a onClick={this.handleClick}>Jump</a>;
|
||||
* }
|
||||
* });
|
||||
* TODO: Delete this when all callers have been updated to rely on this
|
||||
* behavior being the default.
|
||||
*
|
||||
* Backwards compatible stub for what is now the default behavior.
|
||||
* @param {function} method Method to be bound.
|
||||
* @public
|
||||
*/
|
||||
autoBind: function(method) {
|
||||
function unbound() {
|
||||
invariant(
|
||||
false,
|
||||
'React.autoBind(...): Attempted to invoke an auto-bound method that ' +
|
||||
'was not correctly defined on the class specification.'
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'React.autoBind() is now deprecated. All React component methods ' +
|
||||
'are auto bound by default, so React.autoBind() is a no-op. It ' +
|
||||
'will be removed in the next version of React'
|
||||
);
|
||||
}
|
||||
unbound.__reactAutoBind = method;
|
||||
return unbound;
|
||||
return method;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactCompositeComponent;
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
*
|
||||
* The current owner is the component who should own any components that are
|
||||
* currently being constructed.
|
||||
*
|
||||
* The depth indicate how many composite components are above this render level.
|
||||
*/
|
||||
var ReactCurrentOwner = {
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactDOM
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -40,16 +40,17 @@ var objMapKeyVal = require('objMapKeyVal');
|
||||
* @private
|
||||
*/
|
||||
function createDOMComponentClass(tag, omitClose) {
|
||||
var Constructor = function(initialProps, children) {
|
||||
this.construct(initialProps, children);
|
||||
};
|
||||
|
||||
var Constructor = function() {};
|
||||
Constructor.prototype = new ReactNativeComponent(tag, omitClose);
|
||||
Constructor.prototype.constructor = Constructor;
|
||||
|
||||
return function(props, children) {
|
||||
return new Constructor(props, children);
|
||||
var ConvenienceConstructor = function(props, children) {
|
||||
var instance = new Constructor();
|
||||
instance.construct.apply(instance, arguments);
|
||||
return instance;
|
||||
};
|
||||
ConvenienceConstructor.componentConstructor = Constructor;
|
||||
return ConvenienceConstructor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,74 +63,118 @@ var ReactDOM = objMapKeyVal({
|
||||
a: false,
|
||||
abbr: false,
|
||||
address: false,
|
||||
area: false,
|
||||
article: false,
|
||||
aside: false,
|
||||
audio: false,
|
||||
b: false,
|
||||
base: false,
|
||||
bdi: false,
|
||||
bdo: false,
|
||||
big: false,
|
||||
blockquote: false,
|
||||
body: false,
|
||||
br: true,
|
||||
button: false,
|
||||
canvas: false,
|
||||
caption: false,
|
||||
cite: false,
|
||||
code: false,
|
||||
col: true,
|
||||
colgroup: false,
|
||||
data: false,
|
||||
datalist: false,
|
||||
dd: false,
|
||||
del: false,
|
||||
details: false,
|
||||
dfn: false,
|
||||
div: false,
|
||||
section: false,
|
||||
dl: false,
|
||||
dt: false,
|
||||
em: false,
|
||||
embed: true,
|
||||
fieldset: false,
|
||||
figcaption: false,
|
||||
figure: false,
|
||||
footer: false,
|
||||
// Danger: this gets monkeypatched! See ReactDOMForm for more info.
|
||||
form: false,
|
||||
form: false, // NOTE: Injected, see `ReactDOMForm`.
|
||||
h1: false,
|
||||
h2: false,
|
||||
h3: false,
|
||||
h4: false,
|
||||
h5: false,
|
||||
h6: false,
|
||||
head: false,
|
||||
header: false,
|
||||
hr: true,
|
||||
html: false,
|
||||
i: false,
|
||||
iframe: false,
|
||||
img: true,
|
||||
input: true,
|
||||
ins: false,
|
||||
kbd: false,
|
||||
keygen: true,
|
||||
label: false,
|
||||
legend: false,
|
||||
li: false,
|
||||
line: false,
|
||||
link: false,
|
||||
main: false,
|
||||
map: false,
|
||||
mark: false,
|
||||
menu: false,
|
||||
menuitem: false, // NOTE: Close tag should be omitted, but causes problems.
|
||||
meta: true,
|
||||
meter: false,
|
||||
nav: false,
|
||||
noscript: false,
|
||||
object: false,
|
||||
ol: false,
|
||||
optgroup: false,
|
||||
option: false,
|
||||
output: false,
|
||||
p: false,
|
||||
param: true,
|
||||
pre: false,
|
||||
progress: false,
|
||||
q: false,
|
||||
rp: false,
|
||||
rt: false,
|
||||
ruby: false,
|
||||
s: false,
|
||||
samp: false,
|
||||
script: false,
|
||||
section: false,
|
||||
select: false,
|
||||
small: false,
|
||||
source: false,
|
||||
span: false,
|
||||
sub: false,
|
||||
sup: false,
|
||||
strong: false,
|
||||
style: false,
|
||||
sub: false,
|
||||
summary: false,
|
||||
sup: false,
|
||||
table: false,
|
||||
tbody: false,
|
||||
td: false,
|
||||
textarea: false,
|
||||
textarea: false, // NOTE: Injected, see `ReactDOMTextarea`.
|
||||
tfoot: false,
|
||||
th: false,
|
||||
thead: false,
|
||||
time: false,
|
||||
title: false,
|
||||
tr: false,
|
||||
track: true,
|
||||
u: false,
|
||||
ul: false,
|
||||
'var': false,
|
||||
video: false,
|
||||
wbr: false,
|
||||
|
||||
// SVG
|
||||
circle: false,
|
||||
g: false,
|
||||
line: false,
|
||||
path: false,
|
||||
polyline: false,
|
||||
rect: false,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactDOMIDOperations
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
@@ -24,7 +24,7 @@
|
||||
var CSSPropertyOperations = require('CSSPropertyOperations');
|
||||
var DOMChildrenOperations = require('DOMChildrenOperations');
|
||||
var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var ReactDOMNodeCache = require('ReactDOMNodeCache');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var getTextContentAccessor = require('getTextContentAccessor');
|
||||
var invariant = require('invariant');
|
||||
@@ -36,7 +36,6 @@ var invariant = require('invariant');
|
||||
* @private
|
||||
*/
|
||||
var INVALID_PROPERTY_ERRORS = {
|
||||
content: '`content` must be set using `updateTextContentByID()`.',
|
||||
dangerouslySetInnerHTML:
|
||||
'`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
|
||||
style: '`style` must be set using `updateStylesByID()`.'
|
||||
@@ -66,7 +65,7 @@ var ReactDOMIDOperations = {
|
||||
* @internal
|
||||
*/
|
||||
updatePropertyByID: function(id, name, value) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
var node = ReactID.getNode(id);
|
||||
invariant(
|
||||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
|
||||
'updatePropertyByID(...): %s',
|
||||
@@ -75,6 +74,24 @@ var ReactDOMIDOperations = {
|
||||
DOMPropertyOperations.setValueForProperty(node, name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node to remove a property. This should only be used to remove
|
||||
* DOM properties in `DOMProperty`.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {string} name A property name to remove, see `DOMProperty`.
|
||||
* @internal
|
||||
*/
|
||||
deletePropertyByID: function(id, name, value) {
|
||||
var node = ReactID.getNode(id);
|
||||
invariant(
|
||||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
|
||||
'updatePropertyByID(...): %s',
|
||||
INVALID_PROPERTY_ERRORS[name]
|
||||
);
|
||||
DOMPropertyOperations.deleteValueForProperty(node, name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* This should almost never be used instead of `updatePropertyByID()` due to
|
||||
* the extra object allocation required by the API. That said, this is useful
|
||||
@@ -95,14 +112,15 @@ var ReactDOMIDOperations = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node with new style values.
|
||||
* Updates a DOM node with new style values. If a value is specified as '',
|
||||
* the corresponding style property will be unset.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {object} styles Mapping from styles to values.
|
||||
* @internal
|
||||
*/
|
||||
updateStylesByID: function(id, styles) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
var node = ReactID.getNode(id);
|
||||
CSSPropertyOperations.setValueForStyles(node, styles);
|
||||
},
|
||||
|
||||
@@ -114,7 +132,7 @@ var ReactDOMIDOperations = {
|
||||
* @internal
|
||||
*/
|
||||
updateInnerHTMLByID: function(id, html) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
var node = ReactID.getNode(id);
|
||||
// HACK: IE8- normalize whitespace in innerHTML, removing leading spaces.
|
||||
// @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
|
||||
node.innerHTML = (html && html.__html || '').replace(/^ /g, ' ');
|
||||
@@ -128,7 +146,7 @@ var ReactDOMIDOperations = {
|
||||
* @internal
|
||||
*/
|
||||
updateTextContentByID: function(id, content) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
var node = ReactID.getNode(id);
|
||||
node[textContentAccessor] = content;
|
||||
},
|
||||
|
||||
@@ -141,9 +159,8 @@ var ReactDOMIDOperations = {
|
||||
* @see {Danger.dangerouslyReplaceNodeWithMarkup}
|
||||
*/
|
||||
dangerouslyReplaceNodeWithMarkupByID: function(id, markup) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
var node = ReactID.getNode(id);
|
||||
DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
|
||||
ReactDOMNodeCache.purgeEntireCache();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -151,14 +168,8 @@ var ReactDOMIDOperations = {
|
||||
* Detect if any elements were removed instead of blindly purging.
|
||||
*/
|
||||
manageChildrenByParentID: function(parentID, domOperations) {
|
||||
var parent = ReactDOMNodeCache.getCachedNodeByID(parentID);
|
||||
var parent = ReactID.getNode(parentID);
|
||||
DOMChildrenOperations.manageChildren(parent, domOperations);
|
||||
ReactDOMNodeCache.purgeEntireCache();
|
||||
},
|
||||
|
||||
setTextNodeValueAtIndexByParentID: function(parentID, index, value) {
|
||||
var parent = ReactDOMNodeCache.getCachedNodeByID(parentID);
|
||||
DOMChildrenOperations.setTextNodeValueAtIndex(parent, index, value);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactDOMNodeCache
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactMount = require('ReactMount');
|
||||
|
||||
var nodeCache = {};
|
||||
|
||||
/**
|
||||
* DOM node cache only intended for use by React. Placed into a shared module so
|
||||
* that both read and write utilities may benefit from a shared cache.
|
||||
*/
|
||||
var ReactDOMNodeCache = {
|
||||
/**
|
||||
* Releases fast id lookups (node/style cache). This implementation is
|
||||
* aggressive with purging because the bookkeeping associated with doing fine
|
||||
* grained deleted from the cache may outweight the benefits of the cache. The
|
||||
* heuristic that should be used to purge is 'any time anything is deleted'.
|
||||
* Typically this means that a large amount of content is being replaced and
|
||||
* several elements would need purging regardless. It's also a time when an
|
||||
* application is likely not in the middle of a "smooth operation" (such as
|
||||
* animating/scrolling).
|
||||
*/
|
||||
purgeEntireCache: function() {
|
||||
nodeCache = {};
|
||||
return nodeCache;
|
||||
},
|
||||
getCachedNodeByID: function(id) {
|
||||
return nodeCache[id] ||
|
||||
(nodeCache[id] =
|
||||
document.getElementById(id) ||
|
||||
ReactMount.findReactRenderedDOMNodeSlow(id));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactDOMNodeCache;
|
||||
@@ -20,9 +20,17 @@
|
||||
|
||||
var ReactDOM = require('ReactDOM');
|
||||
var ReactDOMForm = require('ReactDOMForm');
|
||||
var ReactDOMInput = require('ReactDOMInput');
|
||||
var ReactDOMOption = require('ReactDOMOption');
|
||||
var ReactDOMSelect = require('ReactDOMSelect');
|
||||
var ReactDOMTextarea = require('ReactDOMTextarea');
|
||||
|
||||
var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig');
|
||||
var DOMProperty = require('DOMProperty');
|
||||
|
||||
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
|
||||
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
|
||||
var ChangeEventPlugin = require('ChangeEventPlugin');
|
||||
var EventPluginHub = require('EventPluginHub');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
var SimpleEventPlugin = require('SimpleEventPlugin');
|
||||
@@ -35,23 +43,24 @@ function inject() {
|
||||
EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
|
||||
|
||||
/**
|
||||
* Two important event plugins included by default (without having to require
|
||||
* Some important event plugins included by default (without having to require
|
||||
* them).
|
||||
*/
|
||||
EventPluginHub.injection.injectEventPluginsByName({
|
||||
'SimpleEventPlugin': SimpleEventPlugin,
|
||||
'EnterLeaveEventPlugin': EnterLeaveEventPlugin
|
||||
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
|
||||
'ChangeEventPlugin': ChangeEventPlugin
|
||||
});
|
||||
|
||||
/*
|
||||
* This is a bit of a hack. We need to override the <form> element
|
||||
* to be a composite component because IE8 does not bubble or capture
|
||||
* submit to the top level. In order to make this work with our
|
||||
* dependency graph we need to inject it here.
|
||||
*/
|
||||
ReactDOM.injection.injectComponentClasses({
|
||||
form: ReactDOMForm
|
||||
form: ReactDOMForm,
|
||||
input: ReactDOMInput,
|
||||
option: ReactDOMOption,
|
||||
select: ReactDOMSelect,
|
||||
textarea: ReactDOMTextarea
|
||||
});
|
||||
|
||||
DOMProperty.injection.injectDOMPropertyConfig(DefaultDOMPropertyConfig);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
50
src/core/ReactDoNotBindDeprecated.js
Normal file
50
src/core/ReactDoNotBindDeprecated.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactDoNotBindDeprecated
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactDoNotBindDeprecated = {
|
||||
/**
|
||||
* Marks the method for not being automatically bound on component mounting. A
|
||||
* couple of reasons you might want to use this:
|
||||
*
|
||||
* - Automatically supporting the previous behavior in components that were
|
||||
* built with previous versions of React.
|
||||
* - Tuning performance, by avoiding binding on initial render for methods
|
||||
* that are always invoked while being preceded by `this.`. Such binds are
|
||||
* unnecessary.
|
||||
*
|
||||
* React.createClass({
|
||||
* handleClick: ReactDoNotBindDeprecated.doNotBind(function() {
|
||||
* alert(this.setState); // undefined!
|
||||
* }),
|
||||
* render: function() {
|
||||
* return <a onClick={this.handleClick}>Jump</a>;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param {function} method Method to avoid automatically binding.
|
||||
* @public
|
||||
*/
|
||||
doNotBind: function(method) {
|
||||
method.__reactDontBind = true; // Mutating
|
||||
return method;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactDoNotBindDeprecated;
|
||||
@@ -1,321 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactEvent
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var BrowserEnv = require('BrowserEnv');
|
||||
var EventConstants = require('EventConstants');
|
||||
var EventPluginHub = require('EventPluginHub');
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var NormalizedEventListener = require('NormalizedEventListener');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var isEventSupported = require('isEventSupported');
|
||||
|
||||
var registrationNames = EventPluginHub.registrationNames;
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
var listen = NormalizedEventListener.listen;
|
||||
var capture = NormalizedEventListener.capture;
|
||||
|
||||
/**
|
||||
* `ReactEvent` is used to attach top-level event listeners. For example:
|
||||
*
|
||||
* ReactEvent.putListener('myID', 'onClick', myFunction);
|
||||
*
|
||||
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Overview of React and the event system:
|
||||
*
|
||||
* .
|
||||
* +-------------+ .
|
||||
* | DOM | .
|
||||
* +-------------+ . +-----------+
|
||||
* + . +--------+|SimpleEvent|
|
||||
* | . | |Plugin |
|
||||
* +-----|-------+ . v +-----------+
|
||||
* | | | . +--------------+ +------------+
|
||||
* | +------------.---->|EventPluginHub| | Event |
|
||||
* | | . | | +-----------+ | Propagators|
|
||||
* | ReactEvent | . | | |TapEvent | |------------|
|
||||
* | | . | |<---+|Plugin | |other plugin|
|
||||
* | +------------.---------+ | +-----------+ | utilities |
|
||||
* | | | . | | | +------------+
|
||||
* | | | . +---|----------+
|
||||
* | | | . | ^ +-----------+
|
||||
* | | | . | | |Enter/Leave|
|
||||
* +-----| ------+ . | +-------+|Plugin |
|
||||
* | . v +-----------+
|
||||
* + . +--------+
|
||||
* +-------------+ . |callback|
|
||||
* | application | . |registry|
|
||||
* |-------------| . +--------+
|
||||
* | | .
|
||||
* | | .
|
||||
* | | .
|
||||
* | | .
|
||||
* +-------------+ .
|
||||
* .
|
||||
* React Core . General Purpose Event Plugin System
|
||||
*/
|
||||
|
||||
/**
|
||||
* We listen for bubbled touch events on the document object.
|
||||
*
|
||||
* Firefox v8.01 (and possibly others) exhibited strange behavior when mounting
|
||||
* `onmousemove` events at some node that was not the document element. The
|
||||
* symptoms were that if your mouse is not moving over something contained
|
||||
* within that mount point (for example on the background) the top-level
|
||||
* listeners for `onmousemove` won't be called. However, if you register the
|
||||
* `mousemove` on the document object, then it will of course catch all
|
||||
* `mousemove`s. This along with iOS quirks, justifies restricting top-level
|
||||
* listeners to the document object only, at least for these movement types of
|
||||
* events and possibly all events.
|
||||
*
|
||||
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
*
|
||||
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
|
||||
* they bubble to document.
|
||||
*
|
||||
* @see http://www.quirksmode.org/dom/events/keys.html.
|
||||
*/
|
||||
|
||||
var _isListening = false;
|
||||
|
||||
var EVENT_LISTEN_MISUSE;
|
||||
var WORKER_DISABLE;
|
||||
|
||||
if (__DEV__) {
|
||||
EVENT_LISTEN_MISUSE =
|
||||
'You must register listeners at the top of the document, only once - ' +
|
||||
'and only in the main UI thread of a browser - if you are attempting ' +
|
||||
'listen in a worker, the framework is probably doing something wrong ' +
|
||||
'and you should report this immediately.';
|
||||
WORKER_DISABLE =
|
||||
'Cannot disable event listening in Worker thread. This is likely a ' +
|
||||
'bug in the framework. Please report immediately.';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Traps top-level events that bubble. Delegates to the main dispatcher
|
||||
* `handleTopLevel` after performing some basic normalization via
|
||||
* `TopLevelCallbackCreator.createTopLevelCallback`.
|
||||
*/
|
||||
function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) {
|
||||
listen(
|
||||
onWhat,
|
||||
handlerBaseName,
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traps a top-level event by using event capturing.
|
||||
*/
|
||||
function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) {
|
||||
capture(
|
||||
onWhat,
|
||||
handlerBaseName,
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to document scroll and window resize events that may change the
|
||||
* document scroll values. We store those results so as to discourage
|
||||
* application code from asking the DOM itself which could trigger additional
|
||||
* reflows.
|
||||
*/
|
||||
function registerDocumentScrollListener() {
|
||||
listen(window, 'scroll', function(nativeEvent) {
|
||||
if (nativeEvent.target === window) {
|
||||
BrowserEnv.refreshAuthoritativeScrollValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerDocumentResizeListener() {
|
||||
listen(window, 'resize', function(nativeEvent) {
|
||||
if (nativeEvent.target === window) {
|
||||
BrowserEnv.refreshAuthoritativeScrollValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of `ReactEvent` event handling:
|
||||
*
|
||||
* - We trap low level 'top-level' events.
|
||||
*
|
||||
* - We dedupe cross-browser event names into these 'top-level types' so that
|
||||
* `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`.
|
||||
*
|
||||
* - At this point we have native browser events with the top-level type that
|
||||
* was used to catch it at the top-level.
|
||||
*
|
||||
* - We continuously stream these native events (and their respective top-level
|
||||
* types) to the event plugin system `EventPluginHub` and ask the plugin
|
||||
* system if it was able to extract `AbstractEvent` objects. `AbstractEvent`
|
||||
* objects are the events that applications actually deal with - they are not
|
||||
* native browser events but cross-browser wrappers.
|
||||
*
|
||||
* - When returning the `AbstractEvent` objects, `EventPluginHub` will make
|
||||
* sure each abstract event is annotated with "dispatches", which are the
|
||||
* sequence of listeners (and IDs) that care about the event.
|
||||
*
|
||||
* - These `AbstractEvent` objects are fed back into the event plugin system,
|
||||
* which in turn executes these dispatches.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function listenAtTopLevel(touchNotMouse) {
|
||||
invariant(
|
||||
!_isListening,
|
||||
'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
|
||||
);
|
||||
var mountAt = document;
|
||||
|
||||
registerDocumentScrollListener();
|
||||
registerDocumentResizeListener();
|
||||
trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseWheel, 'mousewheel', mountAt);
|
||||
if (touchNotMouse) {
|
||||
trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt);
|
||||
}
|
||||
trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
|
||||
trapBubbledEvent(
|
||||
topLevelTypes.topDOMCharacterDataModified,
|
||||
'DOMCharacterDataModified',
|
||||
mountAt
|
||||
);
|
||||
|
||||
// Firefox needs to capture a different mouse scroll event.
|
||||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
|
||||
trapBubbledEvent(topLevelTypes.topMouseWheel, 'DOMMouseScroll', mountAt);
|
||||
// IE < 9 doesn't support capturing so just trap the bubbled event there.
|
||||
if (isEventSupported('scroll', true)) {
|
||||
trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
|
||||
} else {
|
||||
trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
|
||||
}
|
||||
|
||||
if (isEventSupported('focus', true)) {
|
||||
trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
|
||||
trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
|
||||
} else if (isEventSupported('focusin')) {
|
||||
// IE has `focusin` and `focusout` events which bubble.
|
||||
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
|
||||
trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the heart of `ReactEvent`. It simply streams the top-level native
|
||||
* events to `EventPluginHub`.
|
||||
*
|
||||
* @param {object} topLevelType Record from `EventConstants`.
|
||||
* @param {Event} nativeEvent A Standard Event with fixed `target` property.
|
||||
* @param {DOMElement} renderedTarget Element of interest to the framework.
|
||||
* @param {string} renderedTargetID string ID of `renderedTarget`.
|
||||
* @internal
|
||||
*/
|
||||
function handleTopLevel(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget) {
|
||||
var abstractEvents = EventPluginHub.extractAbstractEvents(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget
|
||||
);
|
||||
|
||||
// The event queue being processed in the same cycle allows preventDefault.
|
||||
EventPluginHub.enqueueAbstractEvents(abstractEvents);
|
||||
EventPluginHub.processAbstractEventQueue();
|
||||
}
|
||||
|
||||
function setEnabled(enabled) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' +
|
||||
'is likely a bug in the framework. Please report immediately.'
|
||||
);
|
||||
ReactEvent.TopLevelCallbackCreator.setEnabled(enabled);
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
return ReactEvent.TopLevelCallbackCreator.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that top-level event delegation listeners are listening at `mountAt`.
|
||||
* There are issues with listening to both touch events and mouse events on the
|
||||
* top-level, so we make the caller choose which one to listen to. (If there's a
|
||||
* touch top-level listeners, anchors don't receive clicks for some reason, and
|
||||
* only in some cases).
|
||||
*
|
||||
* @param {boolean} touchNotMouse Listen to touch events instead of mouse.
|
||||
* @param {object} TopLevelCallbackCreator Module that can create top-level
|
||||
* callback handlers.
|
||||
* @internal
|
||||
*/
|
||||
function ensureListening(touchNotMouse, TopLevelCallbackCreator) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'ensureListening(...): Cannot toggle event listening in a Worker thread. ' +
|
||||
'This is likely a bug in the framework. Please report immediately.'
|
||||
);
|
||||
if (!_isListening) {
|
||||
ReactEvent.TopLevelCallbackCreator = TopLevelCallbackCreator;
|
||||
listenAtTopLevel(touchNotMouse);
|
||||
_isListening = true;
|
||||
}
|
||||
}
|
||||
|
||||
var ReactEvent = {
|
||||
TopLevelCallbackCreator: null, // Injectable callback creator.
|
||||
handleTopLevel: handleTopLevel,
|
||||
setEnabled: setEnabled,
|
||||
isEnabled: isEnabled,
|
||||
ensureListening: ensureListening,
|
||||
registrationNames: registrationNames,
|
||||
putListener: EventPluginHub.putListener,
|
||||
getListener: EventPluginHub.getListener,
|
||||
deleteAllListeners: EventPluginHub.deleteAllListeners,
|
||||
trapBubbledEvent: trapBubbledEvent,
|
||||
trapCapturedEvent: trapCapturedEvent
|
||||
};
|
||||
|
||||
module.exports = ReactEvent;
|
||||
346
src/core/ReactEventEmitter.js
Normal file
346
src/core/ReactEventEmitter.js
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactEventEmitter
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EventConstants = require('EventConstants');
|
||||
var EventListener = require('EventListener');
|
||||
var EventPluginHub = require('EventPluginHub');
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
var ViewportMetrics = require('ViewportMetrics');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var isEventSupported = require('isEventSupported');
|
||||
|
||||
/**
|
||||
* Summary of `ReactEventEmitter` event handling:
|
||||
*
|
||||
* - Top-level delegation is used to trap native browser events. We normalize
|
||||
* and de-duplicate events to account for browser quirks.
|
||||
*
|
||||
* - Forward these native events (with the associated top-level type used to
|
||||
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
|
||||
* to extract any synthetic events.
|
||||
*
|
||||
* - The `EventPluginHub` will then process each event by annotating them with
|
||||
* "dispatches", a sequence of listeners and IDs that care about that event.
|
||||
*
|
||||
* - The `EventPluginHub` then dispatches the events.
|
||||
*
|
||||
* Overview of React and the event system:
|
||||
*
|
||||
* .
|
||||
* +------------+ .
|
||||
* | DOM | .
|
||||
* +------------+ . +-----------+
|
||||
* + . +--------+|SimpleEvent|
|
||||
* | . | |Plugin |
|
||||
* +-----|------+ . v +-----------+
|
||||
* | | | . +--------------+ +------------+
|
||||
* | +-----------.--->|EventPluginHub| | Event |
|
||||
* | | . | | +-----------+ | Propagators|
|
||||
* | ReactEvent | . | | |TapEvent | |------------|
|
||||
* | Emitter | . | |<---+|Plugin | |other plugin|
|
||||
* | | . | | +-----------+ | utilities |
|
||||
* | +-----------.---------+ | +------------+
|
||||
* | | | . +----|---------+
|
||||
* +-----|------+ . | ^ +-----------+
|
||||
* | . | | |Enter/Leave|
|
||||
* + . | +-------+|Plugin |
|
||||
* +-------------+ . v +-----------+
|
||||
* | application | . +----------+
|
||||
* |-------------| . | callback |
|
||||
* | | . | registry |
|
||||
* | | . +----------+
|
||||
* +-------------+ .
|
||||
* .
|
||||
* React Core . General Purpose Event Plugin System
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether or not `ensureListening` has been invoked.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
var _isListening = false;
|
||||
|
||||
/**
|
||||
* Traps top-level events by using event bubbling.
|
||||
*
|
||||
* @param {string} topLevelType Record from `EventConstants`.
|
||||
* @param {string} handlerBaseName Event name (e.g. "click").
|
||||
* @param {DOMEventTarget} element Element on which to attach listener.
|
||||
* @internal
|
||||
*/
|
||||
function trapBubbledEvent(topLevelType, handlerBaseName, element) {
|
||||
EventListener.listen(
|
||||
element,
|
||||
handlerBaseName,
|
||||
ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
|
||||
topLevelType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traps a top-level event by using event capturing.
|
||||
*
|
||||
* @param {string} topLevelType Record from `EventConstants`.
|
||||
* @param {string} handlerBaseName Event name (e.g. "click").
|
||||
* @param {DOMEventTarget} element Element on which to attach listener.
|
||||
* @internal
|
||||
*/
|
||||
function trapCapturedEvent(topLevelType, handlerBaseName, element) {
|
||||
EventListener.capture(
|
||||
element,
|
||||
handlerBaseName,
|
||||
ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
|
||||
topLevelType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to window scroll and resize events. We cache scroll values so that
|
||||
* application code can access them without triggering reflows.
|
||||
*
|
||||
* NOTE: Scroll events do not bubble.
|
||||
*
|
||||
* @private
|
||||
* @see http://www.quirksmode.org/dom/events/scroll.html
|
||||
*/
|
||||
function registerScrollValueMonitoring() {
|
||||
var refresh = ViewportMetrics.refreshScrollValues;
|
||||
EventListener.listen(window, 'scroll', refresh);
|
||||
EventListener.listen(window, 'resize', refresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* We listen for bubbled touch events on the document object.
|
||||
*
|
||||
* Firefox v8.01 (and possibly others) exhibited strange behavior when mounting
|
||||
* `onmousemove` events at some node that was not the document element. The
|
||||
* symptoms were that if your mouse is not moving over something contained
|
||||
* within that mount point (for example on the background) the top-level
|
||||
* listeners for `onmousemove` won't be called. However, if you register the
|
||||
* `mousemove` on the document object, then it will of course catch all
|
||||
* `mousemove`s. This along with iOS quirks, justifies restricting top-level
|
||||
* listeners to the document object only, at least for these movement types of
|
||||
* events and possibly all events.
|
||||
*
|
||||
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
*
|
||||
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
|
||||
* they bubble to document.
|
||||
*
|
||||
* @param {boolean} touchNotMouse Listen to touch events instead of mouse.
|
||||
* @private
|
||||
* @see http://www.quirksmode.org/dom/events/keys.html.
|
||||
*/
|
||||
function listenAtTopLevel(touchNotMouse) {
|
||||
invariant(
|
||||
!_isListening,
|
||||
'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
|
||||
);
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
var mountAt = document;
|
||||
|
||||
registerScrollValueMonitoring();
|
||||
trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt);
|
||||
if (touchNotMouse) {
|
||||
trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt);
|
||||
}
|
||||
trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
|
||||
trapBubbledEvent(
|
||||
topLevelTypes.topSelectionChange,
|
||||
'selectionchange',
|
||||
mountAt
|
||||
);
|
||||
trapBubbledEvent(
|
||||
topLevelTypes.topDOMCharacterDataModified,
|
||||
'DOMCharacterDataModified',
|
||||
mountAt
|
||||
);
|
||||
|
||||
if (isEventSupported('drag')) {
|
||||
trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt);
|
||||
}
|
||||
|
||||
if (isEventSupported('wheel')) {
|
||||
trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
|
||||
} else if (isEventSupported('mousewheel')) {
|
||||
trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
|
||||
} else {
|
||||
// Firefox needs to capture a different mouse scroll event.
|
||||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
|
||||
trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
|
||||
}
|
||||
|
||||
// IE<9 does not support capturing so just trap the bubbled event there.
|
||||
if (isEventSupported('scroll', true)) {
|
||||
trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
|
||||
} else {
|
||||
trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
|
||||
}
|
||||
|
||||
if (isEventSupported('focus', true)) {
|
||||
trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
|
||||
trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
|
||||
} else if (isEventSupported('focusin')) {
|
||||
// IE has `focusin` and `focusout` events which bubble.
|
||||
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
|
||||
trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
|
||||
trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `ReactEventEmitter` is used to attach top-level event listeners. For example:
|
||||
*
|
||||
* ReactEventEmitter.putListener('myID', 'onClick', myFunction);
|
||||
*
|
||||
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
var ReactEventEmitter = {
|
||||
|
||||
/**
|
||||
* React references `ReactEventTopLevelCallback` using this property in order
|
||||
* to allow dependency injection via `ensureListening`.
|
||||
*/
|
||||
TopLevelCallbackCreator: null,
|
||||
|
||||
/**
|
||||
* Ensures that top-level event delegation listeners are installed.
|
||||
*
|
||||
* There are issues with listening to both touch events and mouse events on
|
||||
* the top-level, so we make the caller choose which one to listen to. (If
|
||||
* there's a touch top-level listeners, anchors don't receive clicks for some
|
||||
* reason, and only in some cases).
|
||||
*
|
||||
* @param {boolean} touchNotMouse Listen to touch events instead of mouse.
|
||||
* @param {object} TopLevelCallbackCreator
|
||||
*/
|
||||
ensureListening: function(touchNotMouse, TopLevelCallbackCreator) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'ensureListening(...): Cannot toggle event listening in a Worker ' +
|
||||
'thread. This is likely a bug in the framework. Please report ' +
|
||||
'immediately.'
|
||||
);
|
||||
if (!_isListening) {
|
||||
ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator;
|
||||
listenAtTopLevel(touchNotMouse);
|
||||
_isListening = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets whether or not any created callbacks should be enabled.
|
||||
*
|
||||
* @param {boolean} enabled True if callbacks should be enabled.
|
||||
*/
|
||||
setEnabled: function(enabled) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'setEnabled(...): Cannot toggle event listening in a Worker thread. ' +
|
||||
'This is likely a bug in the framework. Please report immediately.'
|
||||
);
|
||||
if (ReactEventEmitter.TopLevelCallbackCreator) {
|
||||
ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {boolean} True if callbacks are enabled.
|
||||
*/
|
||||
isEnabled: function() {
|
||||
return !!(
|
||||
ReactEventEmitter.TopLevelCallbackCreator &&
|
||||
ReactEventEmitter.TopLevelCallbackCreator.isEnabled()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Streams a fired top-level event to `EventPluginHub` where plugins have the
|
||||
* opportunity to create `ReactEvent`s to be dispatched.
|
||||
*
|
||||
* @param {string} topLevelType Record from `EventConstants`.
|
||||
* @param {DOMEventTarget} topLevelTarget The listening component root node.
|
||||
* @param {string} topLevelTargetID ID of `topLevelTarget`.
|
||||
* @param {object} nativeEvent Native browser event.
|
||||
*/
|
||||
handleTopLevel: function(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent) {
|
||||
var events = EventPluginHub.extractEvents(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
);
|
||||
|
||||
// Event queue being processed in the same cycle allows `preventDefault`.
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
EventPluginHub.enqueueEvents(events);
|
||||
EventPluginHub.processEventQueue();
|
||||
});
|
||||
},
|
||||
|
||||
registrationNames: EventPluginHub.registrationNames,
|
||||
|
||||
putListener: EventPluginHub.putListener,
|
||||
|
||||
getListener: EventPluginHub.getListener,
|
||||
|
||||
deleteListener: EventPluginHub.deleteListener,
|
||||
|
||||
deleteAllListeners: EventPluginHub.deleteAllListeners,
|
||||
|
||||
trapBubbledEvent: trapBubbledEvent,
|
||||
|
||||
trapCapturedEvent: trapCapturedEvent
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactEventEmitter;
|
||||
@@ -14,58 +14,75 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactEventTopLevelCallback
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var ReactEvent = require('ReactEvent');
|
||||
var ReactEventEmitter = require('ReactEventEmitter');
|
||||
var ReactID = require('ReactID');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
|
||||
var getDOMNodeID = require('getDOMNodeID');
|
||||
var getEventTarget = require('getEventTarget');
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
var _topLevelListenersEnabled = true;
|
||||
|
||||
/**
|
||||
* Top-level callback creator used to implement event handling using delegation.
|
||||
* This is used via dependency injection in `ReactEventEmitter.ensureListening`.
|
||||
*/
|
||||
var ReactEventTopLevelCallback = {
|
||||
|
||||
/**
|
||||
* @param {boolean} enabled Whether or not all callbacks that have ever been
|
||||
* created with this module should be enabled.
|
||||
* Sets whether or not any created callbacks should be enabled.
|
||||
*
|
||||
* @param {boolean} enabled True if callbacks should be enabled.
|
||||
*/
|
||||
setEnabled: function(enabled) {
|
||||
_topLevelListenersEnabled = !!enabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {boolean} True if callbacks are enabled.
|
||||
*/
|
||||
isEnabled: function() {
|
||||
return _topLevelListenersEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* For a given `topLevelType`, creates a callback that could be added as a
|
||||
* listener to the document. That top level callback will simply fix the
|
||||
* native events before invoking `handleTopLevel`.
|
||||
* Creates a callback for the supplied `topLevelType` that could be added as
|
||||
* a listener to the document. The callback computes a `topLevelTarget` which
|
||||
* should be the root node of a mounted React component where the listener
|
||||
* is attached.
|
||||
*
|
||||
* - Raw native events cannot be trusted to describe their targets correctly
|
||||
* so we expect that the argument to the nested function has already been
|
||||
* fixed. But the `target` property may not be something of interest to
|
||||
* React, so we find the most suitable target. But even at that point, DOM
|
||||
* Elements (the target ) can't be trusted to describe their IDs correctly
|
||||
* so we obtain the ID in a reliable manner and pass it to
|
||||
* `handleTopLevel`. The target/id that we found to be relevant to our
|
||||
* framework are called `renderedTarget`/`renderedTargetID` respectively.
|
||||
* @param {string} topLevelType Record from `EventConstants`.
|
||||
* @return {function} Callback for handling top-level events.
|
||||
*/
|
||||
createTopLevelCallback: function(topLevelType) {
|
||||
return function(fixedNativeEvent) {
|
||||
return function(nativeEvent) {
|
||||
if (!_topLevelListenersEnabled) {
|
||||
return;
|
||||
}
|
||||
var renderedTarget = ReactInstanceHandles.getFirstReactDOM(
|
||||
fixedNativeEvent.target
|
||||
// TODO: Remove when synthetic events are ready, this is for IE<9.
|
||||
if (nativeEvent.srcElement &&
|
||||
nativeEvent.srcElement !== nativeEvent.target) {
|
||||
nativeEvent.target = nativeEvent.srcElement;
|
||||
}
|
||||
var topLevelTarget = ReactInstanceHandles.getFirstReactDOM(
|
||||
getEventTarget(nativeEvent)
|
||||
) || ExecutionEnvironment.global;
|
||||
var renderedTargetID = getDOMNodeID(renderedTarget);
|
||||
var event = fixedNativeEvent;
|
||||
var target = renderedTarget;
|
||||
ReactEvent.handleTopLevel(topLevelType, event, renderedTargetID, target);
|
||||
var topLevelTargetID = ReactID.getID(topLevelTarget) || '';
|
||||
ReactEventEmitter.handleTopLevel(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
166
src/core/ReactID.js
Normal file
166
src/core/ReactID.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactID
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require('invariant');
|
||||
var ReactMount = require('ReactMount');
|
||||
var ATTR_NAME = 'data-reactid';
|
||||
var nodeCache = {};
|
||||
|
||||
/**
|
||||
* Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
|
||||
* element can return its control whose name or ID equals ATTR_NAME. All
|
||||
* DOM nodes support `getAttributeNode` but this can also get called on
|
||||
* other objects so just return '' if we're given something other than a
|
||||
* DOM node (such as window).
|
||||
*
|
||||
* @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
|
||||
* @return {string} ID of the supplied `domNode`.
|
||||
*/
|
||||
function getID(node) {
|
||||
var id = internalGetID(node);
|
||||
if (id) {
|
||||
if (nodeCache.hasOwnProperty(id)) {
|
||||
var cached = nodeCache[id];
|
||||
if (cached !== node) {
|
||||
invariant(
|
||||
!isValid(cached, id),
|
||||
'ReactID: Two valid but unequal nodes with the same `%s`: %s',
|
||||
ATTR_NAME, id
|
||||
);
|
||||
|
||||
nodeCache[id] = node;
|
||||
}
|
||||
} else {
|
||||
nodeCache[id] = node;
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function internalGetID(node) {
|
||||
if (node && node.getAttributeNode) {
|
||||
var attributeNode = node.getAttributeNode(ATTR_NAME);
|
||||
if (attributeNode) {
|
||||
return attributeNode.value || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the React-specific ID of the given node.
|
||||
*
|
||||
* @param {DOMElement} node The DOM node whose ID will be set.
|
||||
* @param {string} id The value of the ID attribute.
|
||||
*/
|
||||
function setID(node, id) {
|
||||
var oldID = internalGetID(node);
|
||||
if (oldID !== id) {
|
||||
delete nodeCache[oldID];
|
||||
}
|
||||
node.setAttribute(ATTR_NAME, id);
|
||||
nodeCache[id] = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the node with the supplied React-generated DOM ID.
|
||||
*
|
||||
* @param {string} id A React-generated DOM ID.
|
||||
* @return {DOMElement} DOM node with the suppled `id`.
|
||||
* @internal
|
||||
*/
|
||||
function getNode(id) {
|
||||
if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
|
||||
nodeCache[id] = ReactMount.findReactNodeByID(id);
|
||||
}
|
||||
return nodeCache[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* A node is "valid" if it is contained by a currently mounted container.
|
||||
*
|
||||
* This means that the node does not have to be contained by a document in
|
||||
* order to be considered valid.
|
||||
*
|
||||
* @param {?DOMElement} node The candidate DOM node.
|
||||
* @param {string} id The expected ID of the node.
|
||||
* @return {boolean} Whether the node is contained by a mounted container.
|
||||
*/
|
||||
function isValid(node, id) {
|
||||
if (node) {
|
||||
invariant(
|
||||
internalGetID(node) === id,
|
||||
'ReactID: Unexpected modification of `%s`',
|
||||
ATTR_NAME
|
||||
);
|
||||
|
||||
var container = ReactMount.findReactContainerForID(id);
|
||||
if (container && contains(container, node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function contains(ancestor, descendant) {
|
||||
if (ancestor.contains) {
|
||||
// Supported natively in virtually all browsers, but not in jsdom.
|
||||
return ancestor.contains(descendant);
|
||||
}
|
||||
|
||||
if (descendant === ancestor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (descendant.nodeType === 3) {
|
||||
// If descendant is a text node, start from descendant.parentNode
|
||||
// instead, so that we can assume all ancestors worth considering are
|
||||
// element nodes with nodeType === 1.
|
||||
descendant = descendant.parentNode;
|
||||
}
|
||||
|
||||
while (descendant && descendant.nodeType === 1) {
|
||||
if (descendant === ancestor) {
|
||||
return true;
|
||||
}
|
||||
descendant = descendant.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the cache to forget about one React-specific ID.
|
||||
*
|
||||
* @param {string} id The ID to forget.
|
||||
*/
|
||||
function purgeID(id) {
|
||||
delete nodeCache[id];
|
||||
}
|
||||
|
||||
exports.ATTR_NAME = ATTR_NAME;
|
||||
exports.getID = getID;
|
||||
exports.rawGetID = internalGetID;
|
||||
exports.setID = setID;
|
||||
exports.getNode = getNode;
|
||||
exports.purgeID = purgeID;
|
||||
@@ -27,6 +27,10 @@ function getActiveElement() {
|
||||
}
|
||||
}
|
||||
|
||||
function isInDocument(node) {
|
||||
return document.documentElement.contains(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ReactInputSelection: React input selection module. Based on Selection.js,
|
||||
* but modified to be suitable for react and has a couple of bug fixes (doesn't
|
||||
@@ -64,7 +68,7 @@ var ReactInputSelection = {
|
||||
var priorFocusedElem = priorSelectionInformation.focusedElem;
|
||||
var priorSelectionRange = priorSelectionInformation.selectionRange;
|
||||
if (curFocusedElem !== priorFocusedElem &&
|
||||
document.getElementById(priorFocusedElem.id)) {
|
||||
isInDocument(priorFocusedElem)) {
|
||||
if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
|
||||
ReactInputSelection.setSelection(
|
||||
priorFocusedElem,
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactInstanceHandles
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var getDOMNodeID = require('getDOMNodeID');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var SEPARATOR = '.';
|
||||
@@ -29,6 +31,26 @@ var SEPARATOR_LENGTH = SEPARATOR.length;
|
||||
*/
|
||||
var MAX_TREE_DEPTH = 100;
|
||||
|
||||
/**
|
||||
* Size of the reactRoot ID space. We generate random numbers for React root
|
||||
* IDs and if there's a collision the events and DOM update system will
|
||||
* get confused. If we assume 100 React components per page, and a user
|
||||
* loads 1 page per minute 24/7 for 50 years, with a mount point space of
|
||||
* 9,999,999 the likelihood of never having a collision is 99.997%.
|
||||
*/
|
||||
var GLOBAL_MOUNT_POINT_MAX = 9999999;
|
||||
|
||||
/**
|
||||
* Creates a DOM ID prefix to use when mounting React components.
|
||||
*
|
||||
* @param {number} index A unique integer
|
||||
* @return {string} React root ID.
|
||||
* @internal
|
||||
*/
|
||||
function getReactRootIDString(index) {
|
||||
return SEPARATOR + 'r[' + index.toString(36) + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a character in the supplied ID is a separator or the end.
|
||||
*
|
||||
@@ -37,7 +59,7 @@ var MAX_TREE_DEPTH = 100;
|
||||
* @return {boolean} True if the character is a separator or end of the ID.
|
||||
* @private
|
||||
*/
|
||||
function isMarker(id, index) {
|
||||
function isBoundary(id, index) {
|
||||
return id.charAt(index) === SEPARATOR || index === id.length;
|
||||
}
|
||||
|
||||
@@ -55,15 +77,18 @@ function isValidID(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the supplied `node` is rendered by React.
|
||||
* Checks if the first ID is an ancestor of or equal to the second ID.
|
||||
*
|
||||
* @param {DOMElement} node DOM Element to check.
|
||||
* @return {boolean} True if the DOM Element appears to be rendered by React.
|
||||
* @private
|
||||
* @param {string} ancestorID
|
||||
* @param {string} descendantID
|
||||
* @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
|
||||
* @internal
|
||||
*/
|
||||
function isRenderedByReact(node) {
|
||||
var id = getDOMNodeID(node);
|
||||
return id && id.charAt(0) === SEPARATOR;
|
||||
function isAncestorIDOf(ancestorID, descendantID) {
|
||||
return (
|
||||
descendantID.indexOf(ancestorID) === 0 &&
|
||||
isBoundary(descendantID, ancestorID.length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,10 +98,83 @@ function isRenderedByReact(node) {
|
||||
* @return {string} ID of the parent, or an empty string.
|
||||
* @private
|
||||
*/
|
||||
function parentID(id) {
|
||||
function getParentID(id) {
|
||||
return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
|
||||
* supplied `destinationID`. If they are equal, the ID is returned.
|
||||
*
|
||||
* @param {string} ancestorID ID of an ancestor node of `destinationID`.
|
||||
* @param {string} destinationID ID of the destination node.
|
||||
* @return {string} Next ID on the path from `ancestorID` to `destinationID`.
|
||||
* @private
|
||||
*/
|
||||
function getNextDescendantID(ancestorID, destinationID) {
|
||||
invariant(
|
||||
isValidID(ancestorID) && isValidID(destinationID),
|
||||
'getNextDescendantID(%s, %s): Received an invalid React DOM ID.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
invariant(
|
||||
isAncestorIDOf(ancestorID, destinationID),
|
||||
'getNextDescendantID(...): React has made an invalid assumption about ' +
|
||||
'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
if (ancestorID === destinationID) {
|
||||
return ancestorID;
|
||||
}
|
||||
// Skip over the ancestor and the immediate separator. Traverse until we hit
|
||||
// another separator or we reach the end of `destinationID`.
|
||||
var start = ancestorID.length + SEPARATOR_LENGTH;
|
||||
for (var i = start; i < destinationID.length; i++) {
|
||||
if (isBoundary(destinationID, i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return destinationID.substr(0, i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the nearest common ancestor ID of two IDs.
|
||||
*
|
||||
* Using this ID scheme, the nearest common ancestor ID is the longest common
|
||||
* prefix of the two IDs that immediately preceded a "marker" in both strings.
|
||||
*
|
||||
* @param {string} oneID
|
||||
* @param {string} twoID
|
||||
* @return {string} Nearest common ancestor ID, or the empty string if none.
|
||||
* @private
|
||||
*/
|
||||
function getFirstCommonAncestorID(oneID, twoID) {
|
||||
var minLength = Math.min(oneID.length, twoID.length);
|
||||
if (minLength === 0) {
|
||||
return '';
|
||||
}
|
||||
var lastCommonMarkerIndex = 0;
|
||||
// Use `<=` to traverse until the "EOL" of the shorter string.
|
||||
for (var i = 0; i <= minLength; i++) {
|
||||
if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
|
||||
lastCommonMarkerIndex = i;
|
||||
} else if (oneID.charAt(i) !== twoID.charAt(i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
|
||||
invariant(
|
||||
isValidID(longestCommonID),
|
||||
'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s',
|
||||
oneID,
|
||||
twoID,
|
||||
longestCommonID
|
||||
);
|
||||
return longestCommonID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the parent path between two IDs (either up or down). The IDs must
|
||||
* not be the same, and there must exist a parent path between them.
|
||||
@@ -96,10 +194,9 @@ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
|
||||
'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.',
|
||||
start
|
||||
);
|
||||
var ancestorID = ReactInstanceHandles.getFirstCommonAncestorID(start, stop);
|
||||
var traverseUp = ancestorID === stop;
|
||||
var traverseUp = isAncestorIDOf(stop, start);
|
||||
invariant(
|
||||
traverseUp || ancestorID === start,
|
||||
traverseUp || isAncestorIDOf(start, stop),
|
||||
'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' +
|
||||
'not have a parent path.',
|
||||
start,
|
||||
@@ -107,7 +204,7 @@ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
|
||||
);
|
||||
// Traverse from `start` to `stop` one depth at a time.
|
||||
var depth = 0;
|
||||
var traverse = traverseUp ? parentID : ReactInstanceHandles.nextDescendantID;
|
||||
var traverse = traverseUp ? getParentID : getNextDescendantID;
|
||||
for (var id = start; /* until break */; id = traverse(id, stop)) {
|
||||
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
|
||||
cb(id, traverseUp, arg);
|
||||
@@ -136,18 +233,40 @@ var ReactInstanceHandles = {
|
||||
|
||||
separator: SEPARATOR,
|
||||
|
||||
createReactRootID: function() {
|
||||
return getReactRootIDString(
|
||||
Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the supplied `node` is rendered by React.
|
||||
*
|
||||
* @param {*} node DOM Element to check.
|
||||
* @return {boolean} True if the DOM Element appears to be rendered by React.
|
||||
* @internal
|
||||
*/
|
||||
isRenderedByReact: function(node) {
|
||||
if (node.nodeType !== 1) {
|
||||
// Not a DOMElement, therefore not a React component
|
||||
return false;
|
||||
}
|
||||
var id = ReactID.getID(node);
|
||||
return id ? id.charAt(0) === SEPARATOR : false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Traverses up the ancestors of the supplied node to find a node that is a
|
||||
* DOM representation of a React component.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @return {?DOMElement}
|
||||
* @param {*} node
|
||||
* @return {?DOMEventTarget}
|
||||
* @internal
|
||||
*/
|
||||
getFirstReactDOM: function(node) {
|
||||
var current = node;
|
||||
while (current && current.parentNode !== current) {
|
||||
if (isRenderedByReact(current)) {
|
||||
if (ReactInstanceHandles.isRenderedByReact(current)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentNode;
|
||||
@@ -159,68 +278,62 @@ var ReactInstanceHandles = {
|
||||
* Finds a node with the supplied `id` inside of the supplied `ancestorNode`.
|
||||
* Exploits the ID naming scheme to perform the search quickly.
|
||||
*
|
||||
* @param {DOMElement} ancestorNode Search from this root.
|
||||
* @param {DOMEventTarget} ancestorNode Search from this root.
|
||||
* @pararm {string} id ID of the DOM representation of the component.
|
||||
* @return {?DOMElement} DOM element with the supplied `id`, if one exists.
|
||||
* @return {DOMEventTarget} DOM node with the supplied `id`.
|
||||
* @internal
|
||||
*/
|
||||
findComponentRoot: function(ancestorNode, id) {
|
||||
var child = ancestorNode.firstChild;
|
||||
while (child) {
|
||||
if (id === child.id) {
|
||||
return child;
|
||||
} else if (id.indexOf(child.id) === 0) {
|
||||
return ReactInstanceHandles.findComponentRoot(child, id);
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
// Effectively: return null;
|
||||
},
|
||||
var firstChildren = [ancestorNode.firstChild];
|
||||
var childIndex = 0;
|
||||
|
||||
/**
|
||||
* Gets the nearest common ancestor ID of two IDs.
|
||||
*
|
||||
* Using this ID scheme, the nearest common ancestor ID is the longest common
|
||||
* prefix of the two IDs that immediately preceded a "marker" in both strings.
|
||||
*
|
||||
* @param {string} oneID
|
||||
* @param {string} twoID
|
||||
* @return {string} Nearest common ancestor ID, or the empty string if none.
|
||||
* @internal
|
||||
*/
|
||||
getFirstCommonAncestorID: function(oneID, twoID) {
|
||||
var minLength = Math.min(oneID.length, twoID.length);
|
||||
if (minLength === 0) {
|
||||
return '';
|
||||
}
|
||||
var lastCommonMarkerIndex = 0;
|
||||
// Use `<=` to traverse until the "EOL" of the shorter string.
|
||||
for (var i = 0; i <= minLength; i++) {
|
||||
if (isMarker(oneID, i) && isMarker(twoID, i)) {
|
||||
lastCommonMarkerIndex = i;
|
||||
} else if (oneID.charAt(i) !== twoID.charAt(i)) {
|
||||
break;
|
||||
while (childIndex < firstChildren.length) {
|
||||
var child = firstChildren[childIndex++];
|
||||
while (child) {
|
||||
var childID = ReactID.getID(child);
|
||||
if (childID) {
|
||||
if (id === childID) {
|
||||
return child;
|
||||
} else if (isAncestorIDOf(childID, id)) {
|
||||
// If we find a child whose ID is an ancestor of the given ID,
|
||||
// then we can be sure that we only want to search the subtree
|
||||
// rooted at this child, so we can throw out the rest of the
|
||||
// search state.
|
||||
firstChildren.length = childIndex = 0;
|
||||
firstChildren.push(child.firstChild);
|
||||
break;
|
||||
} else {
|
||||
// TODO This should not be necessary if the ID hierarchy is
|
||||
// correct, but is occasionally necessary if the DOM has been
|
||||
// modified in unexpected ways.
|
||||
firstChildren.push(child.firstChild);
|
||||
}
|
||||
} else {
|
||||
// If this child had no ID, then there's a chance that it was
|
||||
// injected automatically by the browser, as when a `<table>`
|
||||
// element sprouts an extra `<tbody>` child as a side effect of
|
||||
// `.innerHTML` parsing. Optimistically continue down this
|
||||
// branch, but not before examining the other siblings.
|
||||
firstChildren.push(child.firstChild);
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
}
|
||||
var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'Error while invoking `findComponentRoot` with the following ' +
|
||||
'ancestor node:',
|
||||
ancestorNode
|
||||
);
|
||||
}
|
||||
invariant(
|
||||
isValidID(longestCommonID),
|
||||
'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s',
|
||||
oneID,
|
||||
twoID,
|
||||
longestCommonID
|
||||
false,
|
||||
'findComponentRoot(..., %s): Unable to find element. This probably ' +
|
||||
'means the DOM was unexpectedly mutated (e.g. by the browser).',
|
||||
id,
|
||||
ReactID.getID(ancestorNode)
|
||||
);
|
||||
return longestCommonID;
|
||||
},
|
||||
/**
|
||||
* Creates a DOM ID to use when mounting React components.
|
||||
*
|
||||
* @param {number} mountPointCount The count of React renders so far.
|
||||
* @return {string} React root ID.
|
||||
* @internal
|
||||
*/
|
||||
getReactRootID: function(mountPointCount) {
|
||||
return '.reactRoot[' + mountPointCount + ']';
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -228,11 +341,11 @@ var ReactInstanceHandles = {
|
||||
* contains the React component with the supplied DOM ID.
|
||||
*
|
||||
* @param {string} id DOM ID of a React component.
|
||||
* @return {string} DOM ID of the React component that is the root.
|
||||
* @return {?string} DOM ID of the React component that is the root.
|
||||
* @internal
|
||||
*/
|
||||
getReactRootIDFromNodeID: function(id) {
|
||||
var regexResult = /\.reactRoot\[[^\]]+\]/.exec(id);
|
||||
var regexResult = /\.r\[[^\]]+\]/.exec(id);
|
||||
return regexResult && regexResult[0];
|
||||
},
|
||||
|
||||
@@ -251,15 +364,12 @@ var ReactInstanceHandles = {
|
||||
* @internal
|
||||
*/
|
||||
traverseEnterLeave: function(leaveID, enterID, cb, upArg, downArg) {
|
||||
var longestCommonID = ReactInstanceHandles.getFirstCommonAncestorID(
|
||||
leaveID,
|
||||
enterID
|
||||
);
|
||||
if (longestCommonID !== leaveID) {
|
||||
traverseParentPath(leaveID, longestCommonID, cb, upArg, false, true);
|
||||
var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
|
||||
if (ancestorID !== leaveID) {
|
||||
traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
|
||||
}
|
||||
if (longestCommonID !== enterID) {
|
||||
traverseParentPath(longestCommonID, enterID, cb, downArg, true, false);
|
||||
if (ancestorID !== enterID) {
|
||||
traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -281,45 +391,16 @@ var ReactInstanceHandles = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
|
||||
* supplied `destinationID`. If they are equal, the ID is returned.
|
||||
*
|
||||
* @param {string} ancestorID ID of an ancestor node of `destinationID`.
|
||||
* @param {string} destinationID ID of the destination node.
|
||||
* @return {string} Next ID on the path from `ancestorID` to `destinationID`.
|
||||
* @internal
|
||||
* Exposed for unit testing.
|
||||
* @private
|
||||
*/
|
||||
nextDescendantID: function(ancestorID, destinationID) {
|
||||
invariant(
|
||||
isValidID(ancestorID) && isValidID(destinationID),
|
||||
'nextDescendantID(%s, %s): Received an invalid React DOM ID.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
var longestCommonID = ReactInstanceHandles.getFirstCommonAncestorID(
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
invariant(
|
||||
longestCommonID === ancestorID,
|
||||
'nextDescendantID(...): React has made an invalid assumption about the ' +
|
||||
'DOM hierarchy. Expected `%s` to be an ancestor of `%s`.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
if (ancestorID === destinationID) {
|
||||
return ancestorID;
|
||||
}
|
||||
// Skip over the ancestor and the immediate separator. Traverse until we hit
|
||||
// another separator or we reach the end of `destinationID`.
|
||||
var start = ancestorID.length + SEPARATOR_LENGTH;
|
||||
for (var i = start; i < destinationID.length; i++) {
|
||||
if (isMarker(destinationID, i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return destinationID.substr(0, i);
|
||||
}
|
||||
_getFirstCommonAncestorID: getFirstCommonAncestorID,
|
||||
|
||||
/**
|
||||
* Exposed for unit testing.
|
||||
* @private
|
||||
*/
|
||||
_getNextDescendantID: getNextDescendantID
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -18,26 +18,40 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactEvent = require('ReactEvent');
|
||||
var invariant = require('invariant');
|
||||
var ReactEventEmitter = require('ReactEventEmitter');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var $ = require('$');
|
||||
|
||||
var globalMountPointCounter = 0;
|
||||
|
||||
/** Mapping from reactRoot DOM ID to React component instance. */
|
||||
/** Mapping from reactRootID to React component instance. */
|
||||
var instanceByReactRootID = {};
|
||||
|
||||
/** Mapping from reactRoot DOM ID to `container` nodes. */
|
||||
/** Mapping from reactRootID to `container` nodes. */
|
||||
var containersByReactRootID = {};
|
||||
|
||||
if (__DEV__) {
|
||||
/** __DEV__-only mapping from reactRootID to root elements. */
|
||||
var rootElementsByReactRootID = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement} container DOM element that may contain a React component
|
||||
* @return {?*} DOM element that may have the reactRoot ID, or null.
|
||||
*/
|
||||
function getReactRootElementInContainer(container) {
|
||||
return container && container.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement} container DOM element that may contain a React component.
|
||||
* @return {?string} A "reactRoot" ID, if a React component is rendered.
|
||||
*/
|
||||
function getReactRootID(container) {
|
||||
return container.firstChild && container.firstChild.id;
|
||||
var rootElement = getReactRootElementInContainer(container);
|
||||
return rootElement && ReactID.getID(rootElement);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +61,9 @@ function getReactRootID(container) {
|
||||
*
|
||||
* ReactMount.renderComponent(component, $('container'));
|
||||
*
|
||||
* <div id="container"> <-- Supplied `container`.
|
||||
* <div id=".reactRoot[3]"> <-- Rendered reactRoot of React component.
|
||||
* // ...
|
||||
* <div id="container"> <-- Supplied `container`.
|
||||
* <div data-reactid=".r[3]"> <-- Rendered reactRoot of React
|
||||
* // ... component.
|
||||
* </div>
|
||||
* </div>
|
||||
*
|
||||
@@ -79,19 +93,86 @@ var ReactMount = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures tht the top-level event delegation listener is set up. This will be
|
||||
* invoked some time before the first time any React component is rendered.
|
||||
* Ensures that the top-level event delegation listener is set up. This will
|
||||
* be invoked some time before the first time any React component is rendered.
|
||||
*
|
||||
* @param {object} TopLevelCallbackCreator
|
||||
* @private
|
||||
*/
|
||||
prepareTopLevelEvents: function(TopLevelCallbackCreator) {
|
||||
ReactEvent.ensureListening(
|
||||
ReactEventEmitter.ensureListening(
|
||||
ReactMount.useTouchEvents,
|
||||
TopLevelCallbackCreator
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Take a component that's already mounted into the DOM and replace its props
|
||||
* @param {ReactComponent} prevComponent component instance already in the DOM
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @param {?function} callback function triggered on completion
|
||||
*/
|
||||
_updateRootComponent: function(
|
||||
prevComponent,
|
||||
nextComponent,
|
||||
container,
|
||||
callback) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps, callback);
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
// Record the root element in case it later gets transplanted.
|
||||
rootElementsByReactRootID[getReactRootID(container)] =
|
||||
getReactRootElementInContainer(container);
|
||||
}
|
||||
|
||||
return prevComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a component into the instance map and start the events system.
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @return {string} reactRoot ID prefix
|
||||
*/
|
||||
_registerComponent: function(nextComponent, container) {
|
||||
ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
return reactRootID;
|
||||
},
|
||||
|
||||
/**
|
||||
* Render a new component into the DOM.
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @param {boolean} shouldReuseMarkup if we should skip the markup insertion
|
||||
* @return {ReactComponent} nextComponent
|
||||
*/
|
||||
_renderNewRootComponent: function(
|
||||
nextComponent,
|
||||
container,
|
||||
shouldReuseMarkup) {
|
||||
var reactRootID = ReactMount._registerComponent(nextComponent, container);
|
||||
nextComponent.mountComponentIntoNode(
|
||||
reactRootID,
|
||||
container,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
// Record the root element in case it later gets transplanted.
|
||||
rootElementsByReactRootID[reactRootID] =
|
||||
getReactRootElementInContainer(container);
|
||||
}
|
||||
|
||||
return nextComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a React component into the DOM in the supplied `container`.
|
||||
*
|
||||
@@ -101,45 +182,39 @@ var ReactMount = {
|
||||
*
|
||||
* @param {ReactComponent} nextComponent Component instance to render.
|
||||
* @param {DOMElement} container DOM element to render into.
|
||||
* @param {?function} callback function triggered on completion
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
renderComponent: function(nextComponent, container) {
|
||||
var prevComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
if (prevComponent) {
|
||||
if (prevComponent.constructor === nextComponent.constructor) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps);
|
||||
});
|
||||
return prevComponent;
|
||||
renderComponent: function(nextComponent, container, callback) {
|
||||
var registeredComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
|
||||
if (registeredComponent) {
|
||||
if (registeredComponent.constructor === nextComponent.constructor) {
|
||||
return ReactMount._updateRootComponent(
|
||||
registeredComponent,
|
||||
nextComponent,
|
||||
container,
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
ReactMount.unmountAndReleaseReactRootNode(container);
|
||||
}
|
||||
}
|
||||
|
||||
ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
|
||||
var reactRootElement = getReactRootElementInContainer(container);
|
||||
var containerHasReactMarkup =
|
||||
reactRootElement &&
|
||||
ReactInstanceHandles.isRenderedByReact(reactRootElement);
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
nextComponent.mountComponentIntoNode(reactRootID, container);
|
||||
return nextComponent;
|
||||
},
|
||||
var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent;
|
||||
|
||||
/**
|
||||
* Creates a function that accepts a `container` and renders the supplied
|
||||
* React component instance into it.
|
||||
*
|
||||
* var renderInto = ReactMount.createComponentRenderer(component);
|
||||
* // ...
|
||||
* var component = renderInto($('container'));
|
||||
*
|
||||
* @param {ReactComponent} component Component instance to render.
|
||||
* @return {function(DOMElement): ReactComponent}
|
||||
*/
|
||||
createComponentRenderer: function(component) {
|
||||
return function(container) {
|
||||
return ReactMount.renderComponent(component, container);
|
||||
};
|
||||
var component = ReactMount._renderNewRootComponent(
|
||||
nextComponent,
|
||||
container,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
callback && callback();
|
||||
return component;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -184,9 +259,7 @@ var ReactMount = {
|
||||
}
|
||||
if (!reactRootID) {
|
||||
// No valid "reactRoot" ID found, create one.
|
||||
reactRootID = ReactInstanceHandles.getReactRootID(
|
||||
globalMountPointCounter++
|
||||
);
|
||||
reactRootID = ReactInstanceHandles.createReactRootID();
|
||||
}
|
||||
containersByReactRootID[reactRootID] = container;
|
||||
return reactRootID;
|
||||
@@ -196,14 +269,22 @@ var ReactMount = {
|
||||
* Unmounts and destroys the React component rendered in the `container`.
|
||||
*
|
||||
* @param {DOMElement} container DOM element containing a React component.
|
||||
* @return {boolean} True if a component was found in and unmounted from
|
||||
* `container`
|
||||
*/
|
||||
unmountAndReleaseReactRootNode: function(container) {
|
||||
var reactRootID = getReactRootID(container);
|
||||
var component = instanceByReactRootID[reactRootID];
|
||||
// TODO: Consider throwing if no `component` was found.
|
||||
if (!component) {
|
||||
return false;
|
||||
}
|
||||
component.unmountComponentFromNode(container);
|
||||
delete instanceByReactRootID[reactRootID];
|
||||
delete containersByReactRootID[reactRootID];
|
||||
if (__DEV__) {
|
||||
delete rootElementsByReactRootID[reactRootID];
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -214,19 +295,46 @@ var ReactMount = {
|
||||
* @return {?DOMElement} DOM element that contains the `id`.
|
||||
*/
|
||||
findReactContainerForID: function(id) {
|
||||
var reatRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
|
||||
// TODO: Consider throwing if `id` is not a valid React element ID.
|
||||
return containersByReactRootID[reatRootID];
|
||||
var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
|
||||
var container = containersByReactRootID[reactRootID];
|
||||
|
||||
if (__DEV__) {
|
||||
var rootElement = rootElementsByReactRootID[reactRootID];
|
||||
if (rootElement && rootElement.parentNode !== container) {
|
||||
invariant(
|
||||
// Call rawGetID here because getID calls isValid which calls
|
||||
// findReactContainerForID (this function).
|
||||
ReactID.rawGetID(rootElement) === reactRootID,
|
||||
'ReactMount: Root element ID differed from reactRootID.'
|
||||
);
|
||||
|
||||
var containerChild = container.firstChild;
|
||||
if (containerChild &&
|
||||
reactRootID === ReactID.rawGetID(containerChild)) {
|
||||
// If the container has a new child with the same ID as the old
|
||||
// root element, then rootElementsByReactRootID[reactRootID] is
|
||||
// just stale and needs to be updated. The case that deserves a
|
||||
// warning is when the container is empty.
|
||||
rootElementsByReactRootID[reactRootID] = containerChild;
|
||||
} else {
|
||||
console.warn(
|
||||
'ReactMount: Root element has been removed from its original ' +
|
||||
'container. New container:', rootElement.parentNode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given the ID of a DOM node rendered by a React component, finds the root
|
||||
* DOM node of the React component.
|
||||
* Finds an element rendered by React with the supplied ID.
|
||||
*
|
||||
* @param {string} id ID of a DOM node in the React component.
|
||||
* @return {?DOMElement} Root DOM node of the React component.
|
||||
* @return {DOMElement} Root DOM node of the React component.
|
||||
*/
|
||||
findReactRenderedDOMNodeSlow: function(id) {
|
||||
findReactNodeByID: function(id) {
|
||||
var reactRoot = ReactMount.findReactContainerForID(id);
|
||||
return ReactInstanceHandles.findComponentRoot(reactRoot, id);
|
||||
}
|
||||
|
||||
@@ -167,8 +167,7 @@ var ReactMultiChildMixin = {
|
||||
this.enqueueMove(curChild._domIndex, loopDomIndex);
|
||||
}
|
||||
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
|
||||
!nextChild.props.isStatic &&
|
||||
curChild.receiveProps(nextChild.props, transaction);
|
||||
curChild.receiveProps(nextChild.props, transaction);
|
||||
curChild._domIndex = loopDomIndex;
|
||||
} else {
|
||||
if (curChild) { // !shouldUpdate && curChild => delete
|
||||
|
||||
@@ -14,16 +14,18 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactNativeComponent
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CSSPropertyOperations = require('CSSPropertyOperations');
|
||||
var DOMProperty = require('DOMProperty');
|
||||
var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactEvent = require('ReactEvent');
|
||||
var ReactEventEmitter = require('ReactEventEmitter');
|
||||
var ReactMultiChild = require('ReactMultiChild');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var escapeTextForBrowser = require('escapeTextForBrowser');
|
||||
var flattenChildren = require('flattenChildren');
|
||||
@@ -32,13 +34,13 @@ var keyOf = require('keyOf');
|
||||
var merge = require('merge');
|
||||
var mixInto = require('mixInto');
|
||||
|
||||
var putListener = ReactEvent.putListener;
|
||||
var registrationNames = ReactEvent.registrationNames;
|
||||
var putListener = ReactEventEmitter.putListener;
|
||||
var deleteListener = ReactEventEmitter.deleteListener;
|
||||
var registrationNames = ReactEventEmitter.registrationNames;
|
||||
|
||||
// For quickly matching children type, to test if can be treated as content.
|
||||
var CONTENT_TYPES = {'string': true, 'number': true};
|
||||
|
||||
var CONTENT = keyOf({content: null});
|
||||
var DANGEROUSLY_SET_INNER_HTML = keyOf({dangerouslySetInnerHTML: null});
|
||||
var STYLE = keyOf({style: null});
|
||||
|
||||
@@ -49,14 +51,10 @@ function assertValidProps(props) {
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
// Note the use of `!=` which checks for null or undefined.
|
||||
var hasChildren = props.children != null ? 1 : 0;
|
||||
var hasContent = props.content != null ? 1 : 0;
|
||||
var hasInnerHTML = props.dangerouslySetInnerHTML != null ? 1 : 0;
|
||||
// Note the use of `==` which checks for null or undefined.
|
||||
invariant(
|
||||
hasChildren + hasContent + hasInnerHTML <= 1,
|
||||
'Can only set one of `children`, `props.content`, or ' +
|
||||
'`props.dangerouslySetInnerHTML`.'
|
||||
props.children == null || props.dangerouslySetInnerHTML == null,
|
||||
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
invariant(
|
||||
props.style == null || typeof props.style === 'object',
|
||||
@@ -71,7 +69,7 @@ function assertValidProps(props) {
|
||||
* @extends ReactMultiChild
|
||||
*/
|
||||
function ReactNativeComponent(tag, omitClose) {
|
||||
this._tagOpen = '<' + tag + ' ';
|
||||
this._tagOpen = '<' + tag;
|
||||
this._tagClose = omitClose ? '' : '</' + tag + '>';
|
||||
this.tagName = tag.toUpperCase();
|
||||
}
|
||||
@@ -137,7 +135,8 @@ ReactNativeComponent.Mixin = {
|
||||
}
|
||||
}
|
||||
|
||||
return ret + ' id="' + this._rootNodeID + '">';
|
||||
var escapedID = escapeTextForBrowser(this._rootNodeID);
|
||||
return ret + ' ' + ReactID.ATTR_NAME + '="' + escapedID + '">';
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -155,7 +154,7 @@ ReactNativeComponent.Mixin = {
|
||||
return innerHTML.__html;
|
||||
}
|
||||
} else {
|
||||
var contentToUse = this.props.content != null ? this.props.content :
|
||||
var contentToUse =
|
||||
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
||||
var childrenToUse = contentToUse != null ? null : this.props.children;
|
||||
if (contentToUse != null) {
|
||||
@@ -170,24 +169,24 @@ ReactNativeComponent.Mixin = {
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Controls a native DOM component after it has already been allocated and
|
||||
* attached to the DOM. Reconciles the root DOM node, then recurses.
|
||||
*
|
||||
* @internal
|
||||
* @param {object} nextProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
invariant(
|
||||
this._rootNodeID,
|
||||
'Trying to control a native dom element without a backing id'
|
||||
);
|
||||
assertValidProps(nextProps);
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
this._updateDOMProperties(nextProps);
|
||||
this._updateDOMChildren(nextProps, transaction);
|
||||
this.props = nextProps;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a native DOM component after it has already been allocated and
|
||||
* attached to the DOM. Reconciles the root DOM node, then recurses.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
updateComponent: function(transaction, prevProps) {
|
||||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
|
||||
this._updateDOMProperties(prevProps);
|
||||
this._updateDOMChildren(prevProps, transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -202,11 +201,42 @@ ReactNativeComponent.Mixin = {
|
||||
* TODO: Benchmark areas that can be improved with caching.
|
||||
*
|
||||
* @private
|
||||
* @param {object} nextProps
|
||||
* @param {object} lastProps
|
||||
*/
|
||||
_updateDOMProperties: function(nextProps) {
|
||||
var lastProps = this.props;
|
||||
for (var propKey in nextProps) {
|
||||
_updateDOMProperties: function(lastProps) {
|
||||
var nextProps = this.props;
|
||||
var propKey;
|
||||
var styleName;
|
||||
var styleUpdates;
|
||||
for (propKey in lastProps) {
|
||||
if (nextProps.hasOwnProperty(propKey) ||
|
||||
!lastProps.hasOwnProperty(propKey)) {
|
||||
continue;
|
||||
}
|
||||
if (propKey === STYLE) {
|
||||
var lastStyle = lastProps[propKey];
|
||||
for (styleName in lastStyle) {
|
||||
if (lastStyle.hasOwnProperty(styleName)) {
|
||||
styleUpdates = styleUpdates || {};
|
||||
styleUpdates[styleName] = '';
|
||||
}
|
||||
}
|
||||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
||||
// http://jsperf.com/emptying-speed
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
''
|
||||
);
|
||||
} else if (registrationNames[propKey]) {
|
||||
deleteListener(this._rootNodeID, propKey);
|
||||
} else {
|
||||
ReactComponent.DOMIDOperations.deletePropertyByID(
|
||||
this._rootNodeID,
|
||||
propKey
|
||||
);
|
||||
}
|
||||
}
|
||||
for (propKey in nextProps) {
|
||||
var nextProp = nextProps[propKey];
|
||||
var lastProp = lastProps[propKey];
|
||||
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
|
||||
@@ -216,23 +246,26 @@ ReactNativeComponent.Mixin = {
|
||||
if (nextProp) {
|
||||
nextProp = nextProps.style = merge(nextProp);
|
||||
}
|
||||
var styleUpdates;
|
||||
for (var styleName in nextProp) {
|
||||
if (!nextProp.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
|
||||
if (!styleUpdates) {
|
||||
styleUpdates = {};
|
||||
if (lastProp) {
|
||||
// Unset styles on `lastProp` but not on `nextProp`.
|
||||
for (styleName in lastProp) {
|
||||
if (lastProp.hasOwnProperty(styleName) &&
|
||||
!nextProp.hasOwnProperty(styleName)) {
|
||||
styleUpdates = styleUpdates || {};
|
||||
styleUpdates[styleName] = '';
|
||||
}
|
||||
styleUpdates[styleName] = nextProp[styleName];
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
ReactComponent.DOMIDOperations.updateStylesByID(
|
||||
this._rootNodeID,
|
||||
styleUpdates
|
||||
);
|
||||
// Update styles that changed since `lastProp`.
|
||||
for (styleName in nextProp) {
|
||||
if (nextProp.hasOwnProperty(styleName) &&
|
||||
lastProp[styleName] !== nextProp[styleName]) {
|
||||
styleUpdates = styleUpdates || {};
|
||||
styleUpdates[styleName] = nextProp[styleName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Relies on `updateStylesByID` not mutating `styleUpdates`.
|
||||
styleUpdates = nextProp;
|
||||
}
|
||||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
||||
var lastHtml = lastProp && lastProp.__html;
|
||||
@@ -243,14 +276,11 @@ ReactNativeComponent.Mixin = {
|
||||
nextProp
|
||||
);
|
||||
}
|
||||
} else if (propKey === CONTENT) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
'' + nextProp
|
||||
);
|
||||
} else if (registrationNames[propKey]) {
|
||||
putListener(this._rootNodeID, propKey, nextProp);
|
||||
} else {
|
||||
} else if (
|
||||
DOMProperty.isStandardName[propKey] ||
|
||||
DOMProperty.isCustomAttribute(propKey)) {
|
||||
ReactComponent.DOMIDOperations.updatePropertyByID(
|
||||
this._rootNodeID,
|
||||
propKey,
|
||||
@@ -258,33 +288,33 @@ ReactNativeComponent.Mixin = {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
ReactComponent.DOMIDOperations.updateStylesByID(
|
||||
this._rootNodeID,
|
||||
styleUpdates
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reconciles the children with the various properties that affect the
|
||||
* children content.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {object} lastProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
*/
|
||||
_updateDOMChildren: function(nextProps, transaction) {
|
||||
var thisPropsContentType = typeof this.props.content;
|
||||
var thisPropsContentEmpty =
|
||||
this.props.content == null || thisPropsContentType === 'boolean';
|
||||
var nextPropsContentType = typeof nextProps.content;
|
||||
var nextPropsContentEmpty =
|
||||
nextProps.content == null || nextPropsContentType === 'boolean';
|
||||
_updateDOMChildren: function(lastProps, transaction) {
|
||||
var nextProps = this.props;
|
||||
|
||||
var lastUsedContent = !thisPropsContentEmpty ? this.props.content :
|
||||
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
||||
|
||||
var contentToUse = !nextPropsContentEmpty ? nextProps.content :
|
||||
var lastUsedContent =
|
||||
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
|
||||
var contentToUse =
|
||||
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
|
||||
|
||||
// Note the use of `!=` which checks for null or undefined.
|
||||
|
||||
var lastUsedChildren =
|
||||
lastUsedContent != null ? null : this.props.children;
|
||||
lastUsedContent != null ? null : lastProps.children;
|
||||
var childrenToUse = contentToUse != null ? null : nextProps.children;
|
||||
|
||||
if (contentToUse != null) {
|
||||
@@ -317,9 +347,9 @@ ReactNativeComponent.Mixin = {
|
||||
* @internal
|
||||
*/
|
||||
unmountComponent: function() {
|
||||
ReactEventEmitter.deleteAllListeners(this._rootNodeID);
|
||||
ReactComponent.Mixin.unmountComponent.call(this);
|
||||
this.unmountMultiChild();
|
||||
ReactEvent.deleteAllListeners(this._rootNodeID);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -37,9 +37,9 @@ var invariant = require('invariant');
|
||||
* </div>
|
||||
* );
|
||||
* },
|
||||
* handleClick: React.autoBind(function() {
|
||||
* handleClick: function() {
|
||||
* this.refs.custom.handleClick();
|
||||
* }),
|
||||
* },
|
||||
* componentDidMount: function() {
|
||||
* this.refs.custom.initialize();
|
||||
* }
|
||||
|
||||
@@ -44,13 +44,17 @@ function createTransferStrategy(mergeStrategy) {
|
||||
*/
|
||||
var TransferStrategies = {
|
||||
/**
|
||||
* Never transfer the `ref` prop.
|
||||
* Never transfer `children`.
|
||||
*/
|
||||
ref: emptyFunction,
|
||||
children: emptyFunction,
|
||||
/**
|
||||
* Transfer the `className` prop by merging them.
|
||||
*/
|
||||
className: createTransferStrategy(joinClasses),
|
||||
/**
|
||||
* Never transfer the `ref` prop.
|
||||
*/
|
||||
ref: emptyFunction,
|
||||
/**
|
||||
* Transfer the `style` prop (which is an object) by merging them.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactProps
|
||||
* @providesModule ReactPropTypes
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -25,9 +25,9 @@ var invariant = require('invariant');
|
||||
* Collection of methods that allow declaration and validation of props that are
|
||||
* supplied to React components. Example usage:
|
||||
*
|
||||
* var Props = require('ReactProps');
|
||||
* var Props = require('ReactPropTypes');
|
||||
* var MyArticle = React.createClass({
|
||||
* props: {
|
||||
* propTypes: {
|
||||
* // An optional string prop named "description".
|
||||
* description: Props.string,
|
||||
*
|
||||
@@ -43,14 +43,14 @@ var invariant = require('invariant');
|
||||
* A more formal specification of how these methods are used:
|
||||
*
|
||||
* type := array|bool|object|number|string|oneOf([...])|instanceOf(...)
|
||||
* decl := ReactProps.{type}(.isRequired)?
|
||||
* decl := ReactPropTypes.{type}(.isRequired)?
|
||||
*
|
||||
* Each and every declaration produces a function with the same signature. This
|
||||
* allows the creation of custom validation functions. For example:
|
||||
*
|
||||
* var Props = require('ReactProps');
|
||||
* var Props = require('ReactPropTypes');
|
||||
* var MyLink = React.createClass({
|
||||
* props: {
|
||||
* propTypes: {
|
||||
* // An optional string or URI prop named "href".
|
||||
* href: function(props, propName, componentName) {
|
||||
* var propValue = props[propName];
|
||||
@@ -73,6 +73,7 @@ var Props = {
|
||||
|
||||
array: createPrimitiveTypeChecker('array'),
|
||||
bool: createPrimitiveTypeChecker('boolean'),
|
||||
func: createPrimitiveTypeChecker('function'),
|
||||
number: createPrimitiveTypeChecker('number'),
|
||||
object: createPrimitiveTypeChecker('object'),
|
||||
string: createPrimitiveTypeChecker('string'),
|
||||
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactReconcileTransaction
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var PooledClass = require('PooledClass');
|
||||
var ReactEvent = require('ReactEvent');
|
||||
var ReactEventEmitter = require('ReactEventEmitter');
|
||||
var ReactInputSelection = require('ReactInputSelection');
|
||||
var ReactOnDOMReady = require('ReactOnDOMReady');
|
||||
var Transaction = require('Transaction');
|
||||
@@ -50,21 +50,21 @@ var SELECTION_RESTORATION = {
|
||||
*/
|
||||
var EVENT_SUPPRESSION = {
|
||||
/**
|
||||
* @return {boolean} The enabled status of `ReactEvent` before the
|
||||
* @return {boolean} The enabled status of `ReactEventEmitter` before the
|
||||
* reconciliation.
|
||||
*/
|
||||
initialize: function() {
|
||||
var currentlyEnabled = ReactEvent.isEnabled();
|
||||
ReactEvent.setEnabled(false);
|
||||
var currentlyEnabled = ReactEventEmitter.isEnabled();
|
||||
ReactEventEmitter.setEnabled(false);
|
||||
return currentlyEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {boolean} previouslyEnabled The enabled status of `ReactEvent`
|
||||
* @param {boolean} previouslyEnabled Enabled status of `ReactEventEmitter`
|
||||
* before the reconciliation occured. `close` restores the previous value.
|
||||
*/
|
||||
close: function(previouslyEnabled) {
|
||||
ReactEvent.setEnabled(previouslyEnabled);
|
||||
ReactEventEmitter.setEnabled(previouslyEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactTextComponent
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var escapeTextForBrowser = require('escapeTextForBrowser');
|
||||
var mixInto = require('mixInto');
|
||||
@@ -57,7 +58,7 @@ mixInto(ReactTextComponent, {
|
||||
mountComponent: function(rootID) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID);
|
||||
return (
|
||||
'<span id="' + rootID + '">' +
|
||||
'<span ' + ReactID.ATTR_NAME + '="' + rootID + '">' +
|
||||
escapeTextForBrowser(this.props.text) +
|
||||
'</span>'
|
||||
);
|
||||
|
||||
105
src/core/ReactUpdates.js
Normal file
105
src/core/ReactUpdates.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactUpdates
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var isBatchingUpdates = false;
|
||||
|
||||
var dirtyComponents = [];
|
||||
|
||||
/**
|
||||
* Call the provided function in a context within which calls to `setState` and
|
||||
* friends are batched such that components aren't updated unnecessarily.
|
||||
*/
|
||||
function batchedUpdates(callback) {
|
||||
if (isBatchingUpdates) {
|
||||
// We're already executing in an environment where updates will be batched,
|
||||
// so this is a no-op.
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
isBatchingUpdates = true;
|
||||
|
||||
try {
|
||||
callback();
|
||||
// TODO: Sort components by depth such that parent components update first
|
||||
for (var i = 0; i < dirtyComponents.length; i++) {
|
||||
// If a component is unmounted before pending changes apply, ignore them
|
||||
// TODO: Queue unmounts in the same list to avoid this happening at all
|
||||
var component = dirtyComponents[i];
|
||||
if (component.isMounted()) {
|
||||
// If performUpdateIfNecessary happens to enqueue any new updates, we
|
||||
// shouldn't execute the callbacks until the next render happens, so
|
||||
// stash the callbacks first
|
||||
var callbacks = component._pendingCallbacks;
|
||||
component._pendingCallbacks = null;
|
||||
component.performUpdateIfNecessary();
|
||||
if (callbacks) {
|
||||
for (var j = 0; j < callbacks.length; j++) {
|
||||
callbacks[j]();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// IE8 requires `catch` in order to use `finally`.
|
||||
throw error;
|
||||
} finally {
|
||||
dirtyComponents.length = 0;
|
||||
isBatchingUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a component as needing a rerender, adding an optional callback to a
|
||||
* list of functions which will be executed once the rerender occurs.
|
||||
*/
|
||||
function enqueueUpdate(component, callback) {
|
||||
invariant(
|
||||
!callback || typeof callback === "function",
|
||||
'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' +
|
||||
'`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
|
||||
'isn\'t callable.'
|
||||
);
|
||||
|
||||
if (!isBatchingUpdates) {
|
||||
component.performUpdateIfNecessary();
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyComponents.push(component);
|
||||
|
||||
if (callback) {
|
||||
if (component._pendingCallbacks) {
|
||||
component._pendingCallbacks.push(callback);
|
||||
} else {
|
||||
component._pendingCallbacks = [callback];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ReactUpdates = {
|
||||
batchedUpdates: batchedUpdates,
|
||||
enqueueUpdate: enqueueUpdate
|
||||
};
|
||||
|
||||
module.exports = ReactUpdates;
|
||||
@@ -21,26 +21,32 @@
|
||||
|
||||
var mocks = require('mocks');
|
||||
var React = require('React');
|
||||
var ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
var reactComponentExpect = require('reactComponentExpect');
|
||||
|
||||
describe('React.autoBind', function() {
|
||||
// TODO: Test render and all stock methods.
|
||||
describe('autobinding', function() {
|
||||
|
||||
it('Holds reference to instance', function() {
|
||||
|
||||
var mouseDidEnter = mocks.getMockFunction();
|
||||
var mouseDidLeave = mocks.getMockFunction();
|
||||
var mouseDidClick = mocks.getMockFunction();
|
||||
var didBadIdea = mocks.getMockFunction();
|
||||
|
||||
var TestBindComponent = React.createClass({
|
||||
onMouseEnter: mouseDidEnter,
|
||||
onMouseLeave: mouseDidLeave,
|
||||
onClick: React.autoBind(mouseDidClick),
|
||||
getInitialState: function() {
|
||||
return {something: 'hi'};
|
||||
},
|
||||
onMouseEnter: ReactDoNotBindDeprecated.doNotBind(mouseDidEnter),
|
||||
onMouseLeave: ReactDoNotBindDeprecated.doNotBind(mouseDidLeave),
|
||||
onClick: mouseDidClick,
|
||||
|
||||
// autoBind needs to be on the top-level spec.
|
||||
// auto binding only occurs on top level functions in class defs.
|
||||
badIdeas: {
|
||||
badBind: React.autoBind(didBadIdea)
|
||||
badBind: function() {
|
||||
this.state.something;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -48,7 +54,8 @@ describe('React.autoBind', function() {
|
||||
<div
|
||||
onMouseEnter={this.onMouseEnter.bind(this)}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onClick={this.onClick} />
|
||||
onClick={this.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -103,7 +110,7 @@ describe('React.autoBind', function() {
|
||||
var mouseDidClick = mocks.getMockFunction();
|
||||
|
||||
var TestMixin = {
|
||||
onClick: React.autoBind(mouseDidClick)
|
||||
onClick: mouseDidClick
|
||||
};
|
||||
|
||||
var TestBindComponent = React.createClass({
|
||||
|
||||
@@ -31,6 +31,24 @@ describe('ReactComponent', function() {
|
||||
reactComponentExpect = require('reactComponentExpect');
|
||||
});
|
||||
|
||||
it('should throw on invalid render targets', function() {
|
||||
var container = document.createElement('div');
|
||||
// jQuery objects are basically arrays; people often pass them in by mistake
|
||||
expect(function() {
|
||||
React.renderComponent(<div></div>, [container]);
|
||||
}).toThrow(
|
||||
'Invariant Violation: mountComponentIntoNode(...): Target container is ' +
|
||||
'not a DOM element.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
React.renderComponent(<div></div>, null);
|
||||
}).toThrow(
|
||||
'Invariant Violation: mountComponentIntoNode(...): Target container is ' +
|
||||
'not a DOM element.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when supplying a ref outside of render method', function() {
|
||||
var instance = <div ref="badDiv" />;
|
||||
expect(function() {
|
||||
@@ -95,4 +113,23 @@ describe('ReactComponent', function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
});
|
||||
|
||||
it('should correctly determine if a component is mounted', function() {
|
||||
var Component = React.createClass({
|
||||
componentWillMount: function() {
|
||||
expect(this.isMounted()).toBeFalsy();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
expect(this.isMounted()).toBeTruthy();
|
||||
},
|
||||
render: function() {
|
||||
return <div/>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = <Component />;
|
||||
|
||||
expect(instance.isMounted()).toBeFalsy();
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.isMounted()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,6 +286,8 @@ describe('ReactComponentLifeCycle', function() {
|
||||
if (isInitialRender) {
|
||||
this._testJournal.stateInInitialRender = clone(this.state);
|
||||
this._testJournal.lifeCycleInInitialRender = this._lifeCycleState;
|
||||
this._testJournal.compositeLifeCycleInInitialRender =
|
||||
this._compositeLifeCycleState;
|
||||
} else {
|
||||
this._testJournal.stateInLaterRender = clone(this.state);
|
||||
this._testJournal.lifeCycleInLaterRender = this._lifeCycleState;
|
||||
@@ -319,7 +321,7 @@ describe('ReactComponentLifeCycle', function() {
|
||||
GET_INIT_STATE_RETURN_VAL
|
||||
);
|
||||
expect(instance._testJournal.lifeCycleAtStartOfGetInitialState)
|
||||
.toBe(ComponentLifeCycle.UNMOUNTED);
|
||||
.toBe(ComponentLifeCycle.MOUNTED);
|
||||
expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState)
|
||||
.toBe(CompositeComponentLifeCycle.MOUNTING);
|
||||
|
||||
@@ -328,7 +330,7 @@ describe('ReactComponentLifeCycle', function() {
|
||||
instance._testJournal.returnedFromGetInitialState
|
||||
);
|
||||
expect(instance._testJournal.lifeCycleAtStartOfWillMount)
|
||||
.toBe(ComponentLifeCycle.UNMOUNTED);
|
||||
.toBe(ComponentLifeCycle.MOUNTED);
|
||||
expect(instance._testJournal.compositeLifeCycleAtStartOfWillMount)
|
||||
.toBe(CompositeComponentLifeCycle.MOUNTING);
|
||||
|
||||
@@ -343,7 +345,10 @@ describe('ReactComponentLifeCycle', function() {
|
||||
expect(instance._testJournal.stateInInitialRender)
|
||||
.toEqual(INIT_RENDER_STATE);
|
||||
expect(instance._testJournal.lifeCycleInInitialRender).toBe(
|
||||
ComponentLifeCycle.UNMOUNTED
|
||||
ComponentLifeCycle.MOUNTED
|
||||
);
|
||||
expect(instance._testJournal.compositeLifeCycleInInitialRender).toBe(
|
||||
CompositeComponentLifeCycle.MOUNTING
|
||||
);
|
||||
|
||||
expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED);
|
||||
@@ -429,5 +434,69 @@ describe('ReactComponentLifeCycle', function() {
|
||||
expect(instance.state.stateField).toBe('goodbye');
|
||||
});
|
||||
|
||||
it('should call nested lifecycle methods in the right order', function() {
|
||||
var log;
|
||||
var logger = function(msg) {
|
||||
return function() {
|
||||
// return true for shouldComponentUpdate
|
||||
log.push(msg);
|
||||
return true;
|
||||
};
|
||||
};
|
||||
var Outer = React.createClass({
|
||||
render: function() {
|
||||
return <div><Inner x={this.props.x} /></div>;
|
||||
},
|
||||
componentWillMount: logger('outer componentWillMount'),
|
||||
componentDidMount: logger('outer componentDidMount'),
|
||||
componentWillReceiveProps: logger('outer componentWillReceiveProps'),
|
||||
shouldComponentUpdate: logger('outer shouldComponentUpdate'),
|
||||
componentWillUpdate: logger('outer componentWillUpdate'),
|
||||
componentDidUpdate: logger('outer componentDidUpdate'),
|
||||
componentWillUnmount: logger('outer componentWillUnmount')
|
||||
});
|
||||
var Inner = React.createClass({
|
||||
render: function() {
|
||||
return <span>{this.props.x}</span>;
|
||||
},
|
||||
componentWillMount: logger('inner componentWillMount'),
|
||||
componentDidMount: logger('inner componentDidMount'),
|
||||
componentWillReceiveProps: logger('inner componentWillReceiveProps'),
|
||||
shouldComponentUpdate: logger('inner shouldComponentUpdate'),
|
||||
componentWillUpdate: logger('inner componentWillUpdate'),
|
||||
componentDidUpdate: logger('inner componentDidUpdate'),
|
||||
componentWillUnmount: logger('inner componentWillUnmount')
|
||||
});
|
||||
var instance;
|
||||
|
||||
log = [];
|
||||
instance = ReactTestUtils.renderIntoDocument(<Outer x={17} />);
|
||||
expect(log).toEqual([
|
||||
'outer componentWillMount',
|
||||
'inner componentWillMount',
|
||||
'inner componentDidMount',
|
||||
'outer componentDidMount'
|
||||
]);
|
||||
|
||||
log = [];
|
||||
instance.setProps({x: 42});
|
||||
expect(log).toEqual([
|
||||
'outer componentWillReceiveProps',
|
||||
'outer shouldComponentUpdate',
|
||||
'outer componentWillUpdate',
|
||||
'inner componentWillReceiveProps',
|
||||
'inner shouldComponentUpdate',
|
||||
'inner componentWillUpdate',
|
||||
'inner componentDidUpdate',
|
||||
'outer componentDidUpdate'
|
||||
]);
|
||||
|
||||
log = [];
|
||||
instance.unmountComponent();
|
||||
expect(log).toEqual([
|
||||
'outer componentWillUnmount',
|
||||
'inner componentWillUnmount'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
"use strict";
|
||||
|
||||
var MorphingComponent;
|
||||
var MorphingAutoBindComponent;
|
||||
var ChildUpdates;
|
||||
var React;
|
||||
var ReactProps;
|
||||
var ReactCurrentOwner;
|
||||
var ReactPropTypes;
|
||||
var ReactTestUtils;
|
||||
var ReactID;
|
||||
var ReactDoNotBindDeprecated;
|
||||
|
||||
var cx;
|
||||
var reactComponentExpect;
|
||||
@@ -35,8 +37,11 @@ describe('ReactCompositeComponent', function() {
|
||||
cx = require('cx');
|
||||
reactComponentExpect = require('reactComponentExpect');
|
||||
React = require('React');
|
||||
ReactProps = require('ReactProps');
|
||||
ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated');
|
||||
ReactPropTypes = require('ReactPropTypes');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ReactID = require('ReactID');
|
||||
|
||||
MorphingComponent = React.createClass({
|
||||
getInitialState: function() {
|
||||
@@ -48,29 +53,13 @@ describe('ReactCompositeComponent', function() {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var toggleActivatedState = this._toggleActivatedState.bind(this);
|
||||
var toggleActivatedState = this._toggleActivatedState;
|
||||
return !this.state.activated ?
|
||||
<a ref="x" onClick={toggleActivatedState} /> :
|
||||
<b ref="x" onClick={toggleActivatedState} />;
|
||||
}
|
||||
});
|
||||
|
||||
MorphingAutoBindComponent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {activated: false};
|
||||
},
|
||||
|
||||
_toggleActivatedState: React.autoBind(function() {
|
||||
this.setState({activated: !this.state.activated});
|
||||
}),
|
||||
|
||||
render: function() {
|
||||
return !this.state.activated ?
|
||||
<a ref="x" onClick={this._toggleActivatedState} /> :
|
||||
<b ref="x" onClick={this._toggleActivatedState} />;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* We'll use this to ensure that an old version is not cached when it is
|
||||
* reallocated again.
|
||||
@@ -132,24 +121,6 @@ describe('ReactCompositeComponent', function() {
|
||||
reactComponentExpect(instance.refs.x).toBeDOMComponentWithTag('a');
|
||||
});
|
||||
|
||||
it('should behave the same with React.autoBind', function() {
|
||||
var instance = <MorphingAutoBindComponent />;
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.toBeDOMComponentWithTag('a');
|
||||
|
||||
var renderedChild = reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.instance();
|
||||
|
||||
ReactTestUtils.Simulate.click(renderedChild);
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.toBeDOMComponentWithTag('b');
|
||||
});
|
||||
|
||||
it('should not cache old DOM nodes when switching constructors', function() {
|
||||
var instance = <ChildUpdates renderAnchor={true} anchorClassOn={false}/>;
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
@@ -158,20 +129,23 @@ describe('ReactCompositeComponent', function() {
|
||||
// rerender
|
||||
instance.setProps({renderAnchor: true, anchorClassOn: false});
|
||||
var anchorID = instance.getAnchorID();
|
||||
var actualDOMAnchorNode = document.getElementById(anchorID);
|
||||
var actualDOMAnchorNode = ReactID.getNode(anchorID);
|
||||
expect(actualDOMAnchorNode.className).toBe('');
|
||||
});
|
||||
|
||||
it('should auto bind methods and values correctly', function() {
|
||||
var RETURN_VALUE_AFTER_MOUNT = 'returnValue';
|
||||
var ComponentClass = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
valueToReturn: RETURN_VALUE_AFTER_MOUNT
|
||||
};
|
||||
return {valueToReturn: 'hi'};
|
||||
},
|
||||
methodBoundOnMount: React.autoBind(function() {
|
||||
return this.state.valueToReturn;
|
||||
methodToBeExplicitlyBound: function() {
|
||||
return this;
|
||||
},
|
||||
methodAutoBound: function() {
|
||||
return this;
|
||||
},
|
||||
methodExplicitlyNotBound: ReactDoNotBindDeprecated.doNotBind(function() {
|
||||
return this;
|
||||
}),
|
||||
render: function() {
|
||||
return <div> </div>;
|
||||
@@ -179,25 +153,87 @@ describe('ReactCompositeComponent', function() {
|
||||
});
|
||||
var instance = <ComponentClass />;
|
||||
|
||||
// Autobound methods will throw before mounting.
|
||||
// These are controversial assertions for now, they just exist
|
||||
// because existing code depends on these assumptions.
|
||||
expect(function() {
|
||||
instance.methodBoundOnMount();
|
||||
}).toThrow();
|
||||
instance.methodToBeExplicitlyBound.bind(instance)();
|
||||
}).not.toThrow();
|
||||
expect(function() {
|
||||
instance.methodAutoBound();
|
||||
}).not.toThrow();
|
||||
expect(function() {
|
||||
instance.methodExplicitlyNotBound();
|
||||
}).not.toThrow();
|
||||
|
||||
// Next, prove that once mounted, the scope is bound correctly to the actual
|
||||
// component.
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
var retValAfterMount = instance.methodBoundOnMount();
|
||||
expect(retValAfterMount).toBe(RETURN_VALUE_AFTER_MOUNT);
|
||||
var retValAfterMountWithCrazyScope =
|
||||
instance.methodBoundOnMount.call({thisIsACrazyScope:null});
|
||||
expect(retValAfterMountWithCrazyScope).toBe(RETURN_VALUE_AFTER_MOUNT);
|
||||
var explicitlyBound = instance.methodToBeExplicitlyBound.bind(instance);
|
||||
var autoBound = instance.methodAutoBound;
|
||||
var explicitlyNotBound = instance.methodExplicitlyNotBound;
|
||||
|
||||
var context = {};
|
||||
expect(explicitlyBound.call(context)).toBe(instance);
|
||||
expect(autoBound.call(context)).toBe(instance);
|
||||
expect(explicitlyNotBound.call(context)).toBe(context);
|
||||
|
||||
expect(explicitlyBound.call(instance)).toBe(instance);
|
||||
expect(autoBound.call(instance)).toBe(instance);
|
||||
expect(explicitlyNotBound.call(instance)).toBe(instance);
|
||||
|
||||
});
|
||||
|
||||
it('should normalize props with default values', function() {
|
||||
var Component = React.createClass({
|
||||
propTypes: {key: ReactPropTypes.string.isRequired},
|
||||
getDefaultProps: function() {
|
||||
return {key: 'testKey'};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {key: this.props.key + 'State'};
|
||||
},
|
||||
render: function() {
|
||||
return <span>{this.props.key}</span>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = <Component />;
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
reactComponentExpect(instance).scalarPropsEqual({key: 'testKey'});
|
||||
reactComponentExpect(instance).scalarStateEqual({key: 'testKeyState'});
|
||||
|
||||
expect(function() {
|
||||
ReactTestUtils.renderIntoDocument(<Component key={null} />);
|
||||
}).toThrow(
|
||||
'Invariant Violation: Required prop `key` was not specified in ' +
|
||||
'`Component`.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should check default prop values', function() {
|
||||
var Component = React.createClass({
|
||||
propTypes: {key: ReactPropTypes.string.isRequired},
|
||||
getDefaultProps: function() {
|
||||
return {key: null};
|
||||
},
|
||||
render: function() {
|
||||
return <span>{this.props.key}</span>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = <Component />;
|
||||
expect(function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
}).toThrow(
|
||||
'Invariant Violation: Required prop `key` was not specified in ' +
|
||||
'`Component`.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should check declared prop types', function() {
|
||||
var Component = React.createClass({
|
||||
props: {
|
||||
key: ReactProps.string.isRequired
|
||||
propTypes: {
|
||||
key: ReactPropTypes.string.isRequired
|
||||
},
|
||||
render: function() {
|
||||
return <span>{this.props.key}</span>;
|
||||
@@ -223,4 +259,53 @@ describe('ReactCompositeComponent', function() {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not allow `forceUpdate` on unmounted components', function() {
|
||||
var container = document.createElement('div');
|
||||
document.documentElement.appendChild(container);
|
||||
|
||||
var Component = React.createClass({
|
||||
render: function() {
|
||||
return <div />;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = <Component />;
|
||||
expect(function() {
|
||||
instance.forceUpdate();
|
||||
}).toThrow(
|
||||
'Invariant Violation: forceUpdate(...): Can only force an update on ' +
|
||||
'mounted or mounting components.'
|
||||
);
|
||||
|
||||
React.renderComponent(instance, container);
|
||||
expect(function() {
|
||||
instance.forceUpdate();
|
||||
}).not.toThrow();
|
||||
|
||||
React.unmountAndReleaseReactRootNode(container);
|
||||
expect(function() {
|
||||
instance.forceUpdate();
|
||||
}).toThrow(
|
||||
'Invariant Violation: forceUpdate(...): Can only force an update on ' +
|
||||
'mounted or mounting components.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should cleanup even if render() fatals', function() {
|
||||
var BadComponent = React.createClass({
|
||||
render: function() {
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
var instance = <BadComponent />;
|
||||
|
||||
expect(ReactCurrentOwner.current).toBe(null);
|
||||
|
||||
expect(function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
}).toThrow();
|
||||
|
||||
expect(ReactCurrentOwner.current).toBe(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -26,6 +26,9 @@ var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
|
||||
var TestComponent;
|
||||
var TestComponentWithPropTypes;
|
||||
var mixinPropValidator;
|
||||
var componentPropValidator;
|
||||
|
||||
describe('ReactCompositeComponent-mixin', function() {
|
||||
|
||||
@@ -33,6 +36,8 @@ describe('ReactCompositeComponent-mixin', function() {
|
||||
React = require('React');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
reactComponentExpect = require('reactComponentExpect');
|
||||
mixinPropValidator = mocks.getMockFunction();
|
||||
componentPropValidator = mocks.getMockFunction();
|
||||
|
||||
var MixinA = {
|
||||
componentDidMount: function() {
|
||||
@@ -53,8 +58,14 @@ describe('ReactCompositeComponent-mixin', function() {
|
||||
}
|
||||
};
|
||||
|
||||
var MixinD = {
|
||||
propTypes: {
|
||||
value: mixinPropValidator
|
||||
}
|
||||
};
|
||||
|
||||
TestComponent = React.createClass({
|
||||
mixins: [MixinB, MixinC],
|
||||
mixins: [MixinB, MixinC, MixinD],
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.listener('Component didMount');
|
||||
@@ -65,6 +76,15 @@ describe('ReactCompositeComponent-mixin', function() {
|
||||
}
|
||||
});
|
||||
|
||||
TestComponentWithPropTypes = React.createClass({
|
||||
mixins: [MixinD],
|
||||
propTypes: {
|
||||
value: componentPropValidator
|
||||
},
|
||||
render: function() {
|
||||
return <div />;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should support chaining delegate functions', function() {
|
||||
@@ -79,4 +99,22 @@ describe('ReactCompositeComponent-mixin', function() {
|
||||
['Component didMount']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should validate prop types via mixins', function() {
|
||||
expect(TestComponent.componentConstructor.propTypes).toBeDefined();
|
||||
expect(TestComponent.componentConstructor.propTypes.value)
|
||||
.toBe(mixinPropValidator);
|
||||
});
|
||||
|
||||
it('should override mixin prop types with class prop types', function() {
|
||||
// Sanity check...
|
||||
expect(componentPropValidator).toNotBe(mixinPropValidator);
|
||||
// Actually check...
|
||||
expect(TestComponentWithPropTypes.componentConstructor.propTypes)
|
||||
.toBeDefined();
|
||||
expect(TestComponentWithPropTypes.componentConstructor.propTypes.value)
|
||||
.toNotBe(mixinPropValidator);
|
||||
expect(TestComponentWithPropTypes.componentConstructor.propTypes.value)
|
||||
.toBe(componentPropValidator);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,10 +53,10 @@ describe('ReactCompositeComponent-spec', function() {
|
||||
.toBe('TestComponent');
|
||||
});
|
||||
|
||||
it('should copy prop declarations onto the Constructor', function() {
|
||||
it('should copy prop types onto the Constructor', function() {
|
||||
var propValidator = mocks.getMockFunction();
|
||||
var TestComponent = React.createClass({
|
||||
props: {
|
||||
propTypes: {
|
||||
value: propValidator
|
||||
},
|
||||
render: function() {
|
||||
@@ -64,8 +64,8 @@ describe('ReactCompositeComponent-spec', function() {
|
||||
}
|
||||
});
|
||||
|
||||
expect(TestComponent.componentConstructor.propDeclarations).toBeDefined();
|
||||
expect(TestComponent.componentConstructor.propDeclarations.value)
|
||||
expect(TestComponent.componentConstructor.propTypes).toBeDefined();
|
||||
expect(TestComponent.componentConstructor.propTypes.value)
|
||||
.toBe(propValidator);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,8 +51,8 @@ describe('ReactCompositeComponent-transferProps', function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild(instance)
|
||||
.toBeDOMComponentWithTag('input')
|
||||
.expectRenderedChild()
|
||||
.toBeComponentOfType(React.DOM.input)
|
||||
.scalarPropsEqual({
|
||||
className: 'textinput',
|
||||
style: {display: 'block'},
|
||||
@@ -66,8 +66,8 @@ describe('ReactCompositeComponent-transferProps', function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild(instance)
|
||||
.toBeDOMComponentWithTag('input')
|
||||
.expectRenderedChild()
|
||||
.toBeComponentOfType(React.DOM.input)
|
||||
.scalarPropsEqual({placeholder: 'Type here...'});
|
||||
});
|
||||
|
||||
@@ -80,8 +80,8 @@ describe('ReactCompositeComponent-transferProps', function() {
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild(instance)
|
||||
.toBeDOMComponentWithTag('input')
|
||||
.expectRenderedChild()
|
||||
.toBeComponentOfType(React.DOM.input)
|
||||
.scalarPropsEqual({
|
||||
className: 'textinput hidden_elem',
|
||||
style: {
|
||||
@@ -91,6 +91,25 @@ describe('ReactCompositeComponent-transferProps', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transfer children', function() {
|
||||
var ChildrenTestComponent = React.createClass({
|
||||
render: function() {
|
||||
return this.transferPropsTo(<div />);
|
||||
}
|
||||
});
|
||||
|
||||
var instance =
|
||||
<ChildrenTestComponent>
|
||||
<span>Hello!</span>
|
||||
</ChildrenTestComponent>;
|
||||
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.toBeDOMComponentWithTag('div')
|
||||
.toBeDOMComponentWithNoChildren();
|
||||
});
|
||||
|
||||
it('should not transfer ref', function() {
|
||||
var RefTestComponent = React.createClass({
|
||||
render: function() {
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var React = require('React');
|
||||
var ReactDOM = require('ReactDOM');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
var React = require('React');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
describe('ref swapping', function() {
|
||||
// TODO: uncomment this test once we can run in phantom, which
|
||||
@@ -58,7 +61,7 @@ describe('ref swapping', function() {
|
||||
var argDiv = ReactTestUtils.renderIntoDocument(
|
||||
ReactDOM.div(null, 'child')
|
||||
);
|
||||
var argNode = document.getElementById(argDiv._rootNodeID);
|
||||
var argNode = ReactID.getNode(argDiv._rootNodeID);
|
||||
expect(argNode.innerHTML).toBe('child');
|
||||
});
|
||||
|
||||
@@ -66,7 +69,7 @@ describe('ref swapping', function() {
|
||||
var conflictDiv = ReactTestUtils.renderIntoDocument(
|
||||
ReactDOM.div({children: 'fakechild'}, 'child')
|
||||
);
|
||||
var conflictNode = document.getElementById(conflictDiv._rootNodeID);
|
||||
var conflictNode = ReactID.getNode(conflictDiv._rootNodeID);
|
||||
expect(conflictNode.innerHTML).toBe('child');
|
||||
});
|
||||
|
||||
@@ -81,31 +84,34 @@ describe('ref swapping', function() {
|
||||
theBird: <div class="bird" />
|
||||
}}</div>
|
||||
);
|
||||
// Warm the cache with theDog
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div class="dogbeforedelete" />, // Warm the cache with theDog
|
||||
theDog: <div class="dogbeforedelete" />,
|
||||
theBird: <div class="bird" />
|
||||
}
|
||||
});
|
||||
myDiv.setProps({
|
||||
children: { // This better purge that cache
|
||||
theBird: <div class="bird" />
|
||||
}
|
||||
});
|
||||
// Now, put the dog back.
|
||||
// Remove theDog - this should purge the cache
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div class="dog" />, // This is a different node than before
|
||||
theBird: <div class="bird" />
|
||||
}
|
||||
});
|
||||
// Now, put theDog back. It's now a different DOM node.
|
||||
myDiv.setProps({
|
||||
children: { // className changed to bigdog.
|
||||
theDog: <div class="bigdog" />, // but will it use the proper element
|
||||
children: {
|
||||
theDog: <div class="dog" />,
|
||||
theBird: <div class="bird" />
|
||||
}
|
||||
});
|
||||
var root = document.getElementById(myDiv._rootNodeID);
|
||||
// Change the className of theDog. It will use the same element
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: <div class="bigdog" />,
|
||||
theBird: <div class="bird" />
|
||||
}
|
||||
});
|
||||
var root = ReactID.getNode(myDiv._rootNodeID);
|
||||
var dog = root.childNodes[0];
|
||||
expect(dog.className).toBe('bigdog');
|
||||
});
|
||||
|
||||
@@ -23,23 +23,23 @@
|
||||
describe('ReactDOMIDOperations', function() {
|
||||
var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var ReactDOMIDOperations = require('ReactDOMIDOperations');
|
||||
var ReactDOMNodeCache = require('ReactDOMNodeCache');
|
||||
var ReactID = require('ReactID');
|
||||
var keyOf = require('keyOf');
|
||||
|
||||
it('should disallow updating special properties', function() {
|
||||
spyOn(ReactDOMNodeCache, "getCachedNodeByID");
|
||||
spyOn(ReactID, "getNode");
|
||||
spyOn(DOMPropertyOperations, "setValueForProperty");
|
||||
|
||||
expect(function() {
|
||||
ReactDOMIDOperations.updatePropertyByID(
|
||||
'testID',
|
||||
keyOf({content: null}),
|
||||
'testContent'
|
||||
keyOf({dangerouslySetInnerHTML: null}),
|
||||
{__html: 'testContent'}
|
||||
);
|
||||
}).toThrow();
|
||||
|
||||
expect(
|
||||
ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0]
|
||||
ReactID.getNode.argsForCall[0][0]
|
||||
).toBe('testID');
|
||||
|
||||
expect(
|
||||
@@ -49,7 +49,7 @@ describe('ReactDOMIDOperations', function() {
|
||||
|
||||
it('should update innerHTML and special-case whitespace', function() {
|
||||
var stubNode = document.createElement('div');
|
||||
spyOn(ReactDOMNodeCache, "getCachedNodeByID").andReturn(stubNode);
|
||||
spyOn(ReactID, "getNode").andReturn(stubNode);
|
||||
|
||||
ReactDOMIDOperations.updateInnerHTMLByID(
|
||||
'testID',
|
||||
@@ -57,7 +57,7 @@ describe('ReactDOMIDOperations', function() {
|
||||
);
|
||||
|
||||
expect(
|
||||
ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0]
|
||||
ReactID.getNode.argsForCall[0][0]
|
||||
).toBe('testID');
|
||||
|
||||
expect(stubNode.innerHTML).toBe(' testContent');
|
||||
|
||||
@@ -22,7 +22,8 @@ require('mock-modules')
|
||||
.dontMock('BrowserScroll')
|
||||
.dontMock('CallbackRegistry')
|
||||
.dontMock('EventPluginHub')
|
||||
.dontMock('ReactEvent')
|
||||
.dontMock('ReactID')
|
||||
.dontMock('ReactEventEmitter')
|
||||
.dontMock('ReactInstanceHandles')
|
||||
.dontMock('EventPluginHub')
|
||||
.dontMock('TapEventPlugin')
|
||||
@@ -34,7 +35,10 @@ var keyOf = require('keyOf');
|
||||
var mocks = require('mocks');
|
||||
|
||||
var EventPluginHub;
|
||||
var ReactEvent;
|
||||
var ReactID = require('ReactID');
|
||||
var getID = ReactID.getID;
|
||||
var setID = ReactID.setID;
|
||||
var ReactEventEmitter;
|
||||
var ReactEventTopLevelCallback;
|
||||
var ReactTestUtils;
|
||||
var TapEventPlugin;
|
||||
@@ -58,39 +62,42 @@ var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null});
|
||||
|
||||
|
||||
/**
|
||||
* Since `ReactEvent` is fairly well separated from the DOM, we can test almost
|
||||
* all of `ReactEvent` without ever rendering anything in the DOM. As long as we
|
||||
* provide IDs that follow `React's` conventional id namespace hierarchy.
|
||||
* The only reason why we create these DOM nodes, is so that when we feed them
|
||||
* into `ReactEvent` (through `ReactTestUtils`), the event handlers may receive
|
||||
* a DOM node to inspect.
|
||||
* Since `ReactEventEmitter` is fairly well separated from the DOM, we can test
|
||||
* almost all of `ReactEventEmitter` without ever rendering anything in the DOM.
|
||||
* As long as we provide IDs that follow `React's` conventional id namespace
|
||||
* hierarchy. The only reason why we create these DOM nodes, is so that when we
|
||||
* feed them into `ReactEventEmitter` (through `ReactTestUtils`), the event
|
||||
* handlers may receive a DOM node to inspect.
|
||||
*/
|
||||
var CHILD = document.createElement('div');
|
||||
var PARENT = document.createElement('div');
|
||||
var GRANDPARENT = document.createElement('div');
|
||||
CHILD.id = '.reactRoot.[0].[0].[0]';
|
||||
PARENT.id = '.reactRoot.[0].[0]';
|
||||
GRANDPARENT.id = '.reactRoot.[0]';
|
||||
setID(CHILD, '.reactRoot.[0].[0].[0]');
|
||||
setID(PARENT, '.reactRoot.[0].[0]');
|
||||
setID(GRANDPARENT, '.reactRoot.[0]');
|
||||
|
||||
function registerSimpleTestHandler() {
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, LISTENER);
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, LISTENER);
|
||||
var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY);
|
||||
expect(listener).toEqual(LISTENER);
|
||||
return ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
return ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY);
|
||||
}
|
||||
|
||||
|
||||
describe('ReactEvent', function() {
|
||||
describe('ReactEventEmitter', function() {
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
EventPluginHub = require('EventPluginHub');
|
||||
TapEventPlugin = require('TapEventPlugin');
|
||||
ReactEvent = require('ReactEvent');
|
||||
ReactID = require('ReactID');
|
||||
getID = ReactID.getID;
|
||||
setID = ReactID.setID;
|
||||
ReactEventEmitter = require('ReactEventEmitter');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
|
||||
idCallOrder = [];
|
||||
tapMoveThreshold = TapEventPlugin.tapMoveThreshold;
|
||||
ReactEvent.ensureListening(false, ReactEventTopLevelCallback);
|
||||
ReactEventEmitter.ensureListening(false, ReactEventTopLevelCallback);
|
||||
EventPluginHub.injection.injectEventPluginsByName({
|
||||
TapEventPlugin: TapEventPlugin
|
||||
});
|
||||
@@ -98,20 +105,20 @@ describe('ReactEvent', function() {
|
||||
|
||||
it('should store a listener correctly', function() {
|
||||
registerSimpleTestHandler();
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY);
|
||||
expect(listener).toBe(LISTENER);
|
||||
});
|
||||
|
||||
it('should retrieve a listener correctly', function() {
|
||||
registerSimpleTestHandler();
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY);
|
||||
expect(listener).toEqual(LISTENER);
|
||||
});
|
||||
|
||||
it('should clear all handlers when asked to', function() {
|
||||
registerSimpleTestHandler();
|
||||
ReactEvent.deleteAllListeners(CHILD.id);
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
ReactEventEmitter.deleteAllListeners(getID(CHILD));
|
||||
var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY);
|
||||
expect(listener).toBe(undefined);
|
||||
});
|
||||
|
||||
@@ -121,101 +128,101 @@ describe('ReactEvent', function() {
|
||||
expect(LISTENER.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not invoke handlers when ReactEvent is disabled', function() {
|
||||
it('should not invoke handlers if ReactEventEmitter is disabled', function() {
|
||||
registerSimpleTestHandler();
|
||||
ReactEvent.setEnabled(false);
|
||||
ReactEventEmitter.setEnabled(false);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(LISTENER.mock.calls.length).toBe(0);
|
||||
ReactEvent.setEnabled(true);
|
||||
ReactEventEmitter.setEnabled(true);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(LISTENER.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should bubble simply', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
recordID.bind(null, getID(PARENT))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(GRANDPARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
recordID.bind(null, getID(GRANDPARENT))
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(3);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
expect(idCallOrder[2]).toBe(GRANDPARENT.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
expect(idCallOrder[1]).toBe(getID(PARENT));
|
||||
expect(idCallOrder[2]).toBe(getID(GRANDPARENT));
|
||||
});
|
||||
|
||||
it('should support stopPropagation()', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndStopPropagation.bind(null, PARENT.id)
|
||||
recordIDAndStopPropagation.bind(null, getID(PARENT))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(GRANDPARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
recordID.bind(null, getID(GRANDPARENT))
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(2);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
expect(idCallOrder[1]).toBe(getID(PARENT));
|
||||
});
|
||||
|
||||
it('should stop after first dispatch if stopPropagation', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndStopPropagation.bind(null, CHILD.id)
|
||||
recordIDAndStopPropagation.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
recordID.bind(null, getID(PARENT))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(GRANDPARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
recordID.bind(null, getID(GRANDPARENT))
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
});
|
||||
|
||||
it('should stopPropagation if false is returned', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndReturnFalse.bind(null, CHILD.id)
|
||||
recordIDAndReturnFalse.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
recordID.bind(null, getID(PARENT))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(GRANDPARENT),
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
recordID.bind(null, getID(GRANDPARENT))
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -229,30 +236,38 @@ describe('ReactEvent', function() {
|
||||
|
||||
it('should invoke handlers that were removed while bubbling', function() {
|
||||
var handleParentClick = mocks.getMockFunction();
|
||||
var handleChildClick = function(abstractEvent) {
|
||||
ReactEvent.deleteAllListeners(PARENT.id);
|
||||
var handleChildClick = function(event) {
|
||||
ReactEventEmitter.deleteAllListeners(getID(PARENT));
|
||||
};
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick);
|
||||
ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick);
|
||||
ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick);
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
handleParentClick
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(handleParentClick.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not invoke newly inserted handlers while bubbling', function() {
|
||||
var handleParentClick = mocks.getMockFunction();
|
||||
var handleChildClick = function(abstractEvent) {
|
||||
ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick);
|
||||
var handleChildClick = function(event) {
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_CLICK_KEY,
|
||||
handleParentClick
|
||||
);
|
||||
};
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick);
|
||||
ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(handleParentClick.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should infer onTouchTap from a touchStart/End', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
@@ -263,14 +278,14 @@ describe('ReactEvent', function() {
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
});
|
||||
|
||||
it('should infer onTouchTap from when dragging below threshold', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
@@ -281,14 +296,14 @@ describe('ReactEvent', function() {
|
||||
ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
});
|
||||
|
||||
it('should not onTouchTap from when dragging beyond threshold', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
@@ -303,20 +318,20 @@ describe('ReactEvent', function() {
|
||||
|
||||
|
||||
it('should bubble onTouchTap', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(CHILD),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
recordID.bind(null, getID(CHILD))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(PARENT),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
recordID.bind(null, getID(PARENT))
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ReactEventEmitter.putListener(
|
||||
getID(GRANDPARENT),
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
recordID.bind(null, getID(GRANDPARENT))
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
@@ -327,9 +342,9 @@ describe('ReactEvent', function() {
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(3);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
expect(idCallOrder[2]).toBe(GRANDPARENT.id);
|
||||
expect(idCallOrder[0]).toBe(getID(CHILD));
|
||||
expect(idCallOrder[1]).toBe(getID(PARENT));
|
||||
expect(idCallOrder[2]).toBe(getID(GRANDPARENT));
|
||||
});
|
||||
|
||||
});
|
||||
294
src/core/__tests__/ReactIdentity-test.js
Normal file
294
src/core/__tests__/ReactIdentity-test.js
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
var ReactID;
|
||||
|
||||
describe('ReactIdentity', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
React = require('React');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
reactComponentExpect = require('reactComponentExpect');
|
||||
ReactID = require('ReactID');
|
||||
});
|
||||
|
||||
var idExp = /^\.r\[.+?\](.*)$/;
|
||||
function checkId(child, expectedId) {
|
||||
var actual = idExp.exec(ReactID.getID(child));
|
||||
var expected = idExp.exec(expectedId);
|
||||
expect(actual).toBeTruthy();
|
||||
expect(expected).toBeTruthy();
|
||||
expect(actual[1]).toEqual(expected[1]);
|
||||
}
|
||||
|
||||
it('should allow keyed objects to express identity', function() {
|
||||
var instance =
|
||||
<div>
|
||||
{{
|
||||
first: <div />,
|
||||
second: <div />
|
||||
}}
|
||||
</div>;
|
||||
|
||||
React.renderComponent(instance, document.createElement('div'));
|
||||
var node = instance.getDOMNode();
|
||||
reactComponentExpect(instance).toBeDOMComponentWithChildCount(2);
|
||||
checkId(node.childNodes[0], '.r[0].{first}');
|
||||
checkId(node.childNodes[1], '.r[0].{second}');
|
||||
});
|
||||
|
||||
it('should allow key property to express identity', function() {
|
||||
var instance =
|
||||
<div>
|
||||
<div key="apple" />
|
||||
<div key="banana" />
|
||||
<div key={0} />
|
||||
<div key={123} />
|
||||
</div>;
|
||||
|
||||
React.renderComponent(instance, document.createElement('div'));
|
||||
var node = instance.getDOMNode();
|
||||
reactComponentExpect(instance).toBeDOMComponentWithChildCount(4);
|
||||
checkId(node.childNodes[0], '.r[0].[apple]');
|
||||
checkId(node.childNodes[1], '.r[0].[banana]');
|
||||
checkId(node.childNodes[2], '.r[0].[0]');
|
||||
checkId(node.childNodes[3], '.r[0].[123]');
|
||||
});
|
||||
|
||||
it('should use instance identity', function() {
|
||||
|
||||
var Wrapper = React.createClass({
|
||||
render: function() {
|
||||
return <a key="i_get_overwritten">{this.props.children}</a>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance =
|
||||
<div>
|
||||
<Wrapper key="wrap1"><span key="squirrel" /></Wrapper>
|
||||
<Wrapper key="wrap2"><span key="bunny" /></Wrapper>
|
||||
<Wrapper><span key="chipmunk" /></Wrapper>
|
||||
</div>;
|
||||
|
||||
React.renderComponent(instance, document.createElement('div'));
|
||||
var node = instance.getDOMNode();
|
||||
reactComponentExpect(instance).toBeDOMComponentWithChildCount(3);
|
||||
|
||||
checkId(node.childNodes[0], '.r[0].[wrap1]');
|
||||
checkId(node.childNodes[0].firstChild, '.r[0].[wrap1].[squirrel]');
|
||||
checkId(node.childNodes[1], '.r[0].[wrap2]');
|
||||
checkId(node.childNodes[1].firstChild, '.r[0].[wrap2].[bunny]');
|
||||
checkId(node.childNodes[2], '.r[0].[2]');
|
||||
checkId(node.childNodes[2].firstChild, '.r[0].[2].[chipmunk]');
|
||||
});
|
||||
|
||||
function renderAComponentWithKeyIntoContainer(key, container) {
|
||||
var span1 = <span key={key} />;
|
||||
var span2 = <span />;
|
||||
|
||||
var map = {};
|
||||
map[key] = span2;
|
||||
|
||||
React.renderComponent(<div>{[span1, map]}</div>, container);
|
||||
|
||||
expect(span1.getDOMNode()).not.toBe(null);
|
||||
expect(span2.getDOMNode()).not.toBe(null);
|
||||
|
||||
checkId(span1.getDOMNode(), '.r[0].[' + key + ']');
|
||||
checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}');
|
||||
}
|
||||
|
||||
it('should allow any character as a key, in a detached parent', function() {
|
||||
var detachedContainer = document.createElement('div');
|
||||
renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", detachedContainer);
|
||||
});
|
||||
|
||||
it('should allow any character as a key, in an attached parent', function() {
|
||||
// This test exists to protect against implementation details that
|
||||
// incorrectly query escaped IDs using DOM tools like getElementById.
|
||||
var attachedContainer = document.createElement('div');
|
||||
document.body.appendChild(attachedContainer);
|
||||
|
||||
renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", attachedContainer);
|
||||
|
||||
document.body.removeChild(attachedContainer);
|
||||
});
|
||||
|
||||
it('should not allow scripts in keys to execute', function() {
|
||||
var h4x0rKey = '"><script>window.YOUVEBEENH4X0RED=true;</script><div id="';
|
||||
|
||||
var attachedContainer = document.createElement('div');
|
||||
document.body.appendChild(attachedContainer);
|
||||
|
||||
renderAComponentWithKeyIntoContainer(h4x0rKey, attachedContainer);
|
||||
|
||||
document.body.removeChild(attachedContainer);
|
||||
|
||||
// If we get this far, make sure we haven't executed the code
|
||||
expect(window.YOUVEBEENH4X0RED).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should let restructured components retain their uniqueness', function() {
|
||||
var instance0 = <span />;
|
||||
var instance1 = <span />;
|
||||
var instance2 = <span />;
|
||||
|
||||
var TestComponent = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
{instance2}
|
||||
{this.props.children[0]}
|
||||
{this.props.children[1]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TestContainer = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return <TestComponent>{instance0} {instance1}</TestComponent>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
expect(function() {
|
||||
|
||||
React.renderComponent(<TestContainer />, document.createElement('div'));
|
||||
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should let nested restructures retain their uniqueness', function() {
|
||||
var instance0 = <span />;
|
||||
var instance1 = <span />;
|
||||
var instance2 = <span />;
|
||||
|
||||
var TestComponent = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
{instance2}
|
||||
{this.props.children[0]}
|
||||
{this.props.children[1]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TestContainer = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<TestComponent>{instance0} {instance1}</TestComponent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
expect(function() {
|
||||
|
||||
React.renderComponent(<TestContainer />, document.createElement('div'));
|
||||
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should let text nodes retain their uniqueness', function() {
|
||||
var TestComponent = React.createClass({
|
||||
render: function() {
|
||||
return <div>{this.props.children}<span /></div>;
|
||||
}
|
||||
});
|
||||
|
||||
var TestContainer = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<TestComponent>
|
||||
<div />
|
||||
{'second'}
|
||||
</TestComponent>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
expect(function() {
|
||||
|
||||
React.renderComponent(<TestContainer />, document.createElement('div'));
|
||||
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should retain key during updates in composite components', function() {
|
||||
|
||||
var TestComponent = React.createClass({
|
||||
render: function() {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var TestContainer = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return { swapped: false };
|
||||
},
|
||||
|
||||
swap: function() {
|
||||
this.setState({ swapped: true });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<TestComponent>
|
||||
{this.state.swapped ? this.props.second : this.props.first}
|
||||
{this.state.swapped ? this.props.first : this.props.second}
|
||||
</TestComponent>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var instance0 = <span key="A" />;
|
||||
var instance1 = <span key="B" />;
|
||||
|
||||
var wrapped = <TestContainer first={instance0} second={instance1} />;
|
||||
|
||||
React.renderComponent(wrapped, document.createElement('div'));
|
||||
|
||||
var beforeID = ReactID.getID(wrapped.getDOMNode().firstChild);
|
||||
|
||||
wrapped.swap();
|
||||
|
||||
var afterID = ReactID.getID(wrapped.getDOMNode().firstChild);
|
||||
|
||||
expect(beforeID).not.toEqual(afterID);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -19,15 +19,9 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
require('mock-modules')
|
||||
.dontMock('ReactInstanceHandles');
|
||||
|
||||
var React = require('React');
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
|
||||
var reactComponentExpect= require('reactComponentExpect');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
/**
|
||||
* Ensure that all callbacks are invoked, passing this unique argument.
|
||||
@@ -64,251 +58,347 @@ function renderParentIntoDocument() {
|
||||
return ReactTestUtils.renderIntoDocument(<ParentComponent />);
|
||||
}
|
||||
|
||||
var aggregatedArgs = [];
|
||||
function argAggregator(id, isUp, arg) {
|
||||
aggregatedArgs.push({
|
||||
id: id,
|
||||
isUp: isUp,
|
||||
arg: arg
|
||||
});
|
||||
}
|
||||
describe('ReactInstanceHandles', function() {
|
||||
var ReactInstanceHandles;
|
||||
|
||||
var nextDescendantID;
|
||||
var traverseTwoPhase;
|
||||
var traverseEnterLeave;
|
||||
var getFirstCommonAncestorID;
|
||||
var aggregatedArgs;
|
||||
function argAggregator(id, isUp, arg) {
|
||||
aggregatedArgs.push({
|
||||
id: id,
|
||||
isUp: isUp,
|
||||
arg: arg
|
||||
});
|
||||
}
|
||||
|
||||
describe('ReactInstanceHandles traversal', function() {
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
aggregatedArgs = [];
|
||||
ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
nextDescendantID = ReactInstanceHandles.nextDescendantID;
|
||||
getFirstCommonAncestorID = ReactInstanceHandles.getFirstCommonAncestorID;
|
||||
traverseTwoPhase = ReactInstanceHandles.traverseTwoPhase;
|
||||
traverseEnterLeave = ReactInstanceHandles.traverseEnterLeave;
|
||||
aggregatedArgs = [];
|
||||
});
|
||||
|
||||
it("should return next descendent from window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
expect(nextDescendantID('', parent.refs.P_P1._rootNodeID)).toBe(
|
||||
parent.refs.P._rootNodeID
|
||||
);
|
||||
describe('isRenderedByReact', function() {
|
||||
it('should not crash on text nodes', function() {
|
||||
expect(function() {
|
||||
ReactInstanceHandles.isRenderedByReact(document.createTextNode('yolo'));
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return window for next descendent towards window", function() {
|
||||
expect(nextDescendantID('', '')).toBe('');
|
||||
});
|
||||
describe('findComponentRoot', function() {
|
||||
it('should find the correct node with prefix sibling IDs', function() {
|
||||
var parentNode = document.createElement('div');
|
||||
var childNodeA = document.createElement('div');
|
||||
var childNodeB = document.createElement('div');
|
||||
parentNode.appendChild(childNodeA);
|
||||
parentNode.appendChild(childNodeB);
|
||||
|
||||
it("should return self for next descendent towards self", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
expect(
|
||||
nextDescendantID(
|
||||
parent.refs.P_P1._rootNodeID,
|
||||
parent.refs.P_P1._rootNodeID
|
||||
)
|
||||
).toBe(parent.refs.P_P1._rootNodeID);
|
||||
});
|
||||
ReactID.setID(parentNode, '.react[0]');
|
||||
ReactID.setID(childNodeA, '.react[0].0');
|
||||
ReactID.setID(childNodeB, '.react[0].0:1');
|
||||
|
||||
it("should not traverse when traversing outside DOM", function() {
|
||||
var targetID = '';
|
||||
var expectedAggregation = [];
|
||||
traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
expect(
|
||||
ReactInstanceHandles.findComponentRoot(
|
||||
parentNode,
|
||||
ReactID.getID(childNodeB)
|
||||
)
|
||||
).toBe(childNodeB);
|
||||
});
|
||||
|
||||
it("should not traverse when enter/leaving outside DOM", function() {
|
||||
var targetID = '';
|
||||
var expectedAggregation = [];
|
||||
traverseEnterLeave(targetID, targetID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
it('should work around unidentified nodes', function() {
|
||||
var parentNode = document.createElement('div');
|
||||
var childNodeA = document.createElement('div');
|
||||
var childNodeB = document.createElement('div');
|
||||
parentNode.appendChild(childNodeA);
|
||||
parentNode.appendChild(childNodeB);
|
||||
|
||||
it("should not traverse if enter/leave the same node", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var expectedAggregation = [];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
ReactID.setID(parentNode, '.react[0]');
|
||||
// No ID on `childNodeA`.
|
||||
ReactID.setID(childNodeB, '.react[0].0:1');
|
||||
|
||||
it("should traverse two phase across component boundary", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var targetID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: false, arg: ARG},
|
||||
expect(
|
||||
ReactInstanceHandles.findComponentRoot(
|
||||
parentNode,
|
||||
ReactID.getID(childNodeB)
|
||||
)
|
||||
).toBe(childNodeB);
|
||||
});
|
||||
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
it('should throw if a rendered element cannot be found', function() {
|
||||
var parentNode = document.createElement('table');
|
||||
var childNodeA = document.createElement('tbody');
|
||||
var childNodeB = document.createElement('tr');
|
||||
parentNode.appendChild(childNodeA);
|
||||
childNodeA.appendChild(childNodeB);
|
||||
|
||||
it("should traverse two phase at shallowest node", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var targetID = parent.refs.P._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
ReactID.setID(parentNode, '.react[0]');
|
||||
// No ID on `childNodeA`, it was "rendered by the browser".
|
||||
ReactID.setID(childNodeB, '.react[0].1:0');
|
||||
|
||||
it("should traverse enter/leave to sibling - avoids parent", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV_2._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
|
||||
// enter/leave shouldn't fire antyhing on the parent
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_2._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
expect(ReactInstanceHandles.findComponentRoot(
|
||||
parentNode,
|
||||
ReactID.getID(childNodeB)
|
||||
)).toBe(childNodeB);
|
||||
|
||||
it("should traverse enter/leave to parent - avoids parent", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should enter from the window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = ''; // From the window or outside of the React sandbox.
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG2},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should enter from the window to the shallowest", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = ''; // From the window or outside of the React sandbox.
|
||||
var enterID = parent.refs.P._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should leave to the window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var enterID = ''; // From the window or outside of the React sandbox.
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should leave to the window from the shallowest", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var enterID = ''; // From the window or outside of the React sandbox.
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
traverseEnterLeave(leaveID, enterID, argAggregator, ARG, ARG2);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should determine the first common ancestor correctly", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var ancestors = [
|
||||
// Common ancestor from window to deep element is ''.
|
||||
{ one: {_rootNodeID: ''},
|
||||
two: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Same as previous - reversed direction.
|
||||
{ one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: {_rootNodeID: ''},
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Common ancestor from window to shallow id is ''.
|
||||
{ one: parent.refs.P,
|
||||
two: {_rootNodeID: ''},
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Common ancestor with self is self.
|
||||
{ one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
com: parent.refs.P_P1_C1.refs.DIV_1
|
||||
},
|
||||
// Common ancestor with self is self - even if topmost DOM.
|
||||
{ one: parent.refs.P, two: parent.refs.P, com: parent.refs.P },
|
||||
// Siblings
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV_2,
|
||||
com: parent.refs.P_P1_C1.refs.DIV
|
||||
},
|
||||
// Common ancestor with parent is the parent.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV,
|
||||
com: parent.refs.P_P1_C1.refs.DIV
|
||||
},
|
||||
// Common ancestor with grandparent is the grandparent.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1,
|
||||
com: parent.refs.P_P1_C1
|
||||
},
|
||||
// Grantparent across subcomponent boundaries.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C2.refs.DIV_1,
|
||||
com: parent.refs.P_P1
|
||||
},
|
||||
// Something deep with something one-off.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_OneOff,
|
||||
com: parent.refs.P
|
||||
}
|
||||
];
|
||||
var i;
|
||||
for (i = 0; i < ancestors.length; i++) {
|
||||
var plan = ancestors[i];
|
||||
var firstCommon = getFirstCommonAncestorID(
|
||||
plan.one._rootNodeID,
|
||||
plan.two._rootNodeID
|
||||
expect(function() {
|
||||
ReactInstanceHandles.findComponentRoot(
|
||||
parentNode,
|
||||
ReactID.getID(childNodeB) + ":junk"
|
||||
);
|
||||
}).toThrow(
|
||||
'Invariant Violation: findComponentRoot(..., .react[0].1:0:junk): ' +
|
||||
'Unable to find element. This probably means the DOM was ' +
|
||||
'unexpectedly mutated (e.g. by the browser).'
|
||||
);
|
||||
expect(firstCommon).toBe(plan.com._rootNodeID);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReactInstanceHandles.getReactRootIDFromNodeID', function() {
|
||||
it('should support strings', function() {
|
||||
var test = '.reactRoot[s_0_1][0]..[1]';
|
||||
var expected = '.reactRoot[s_0_1]';
|
||||
var actual = ReactInstanceHandles.getReactRootIDFromNodeID(test);
|
||||
expect(actual).toEqual(expected);
|
||||
describe('getReactRootIDFromNodeID', function() {
|
||||
it('should support strings', function() {
|
||||
var test = '.r[s_0_1][0]..[1]';
|
||||
var expected = '.r[s_0_1]';
|
||||
var actual = ReactInstanceHandles.getReactRootIDFromNodeID(test);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseTwoPhase', function() {
|
||||
it("should not traverse when traversing outside DOM", function() {
|
||||
var targetID = '';
|
||||
var expectedAggregation = [];
|
||||
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should traverse two phase across component boundary", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var targetID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: false, arg: ARG},
|
||||
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should traverse two phase at shallowest node", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var targetID = parent.refs.P._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseEnterLeave', function() {
|
||||
it("should not traverse when enter/leaving outside DOM", function() {
|
||||
var targetID = '';
|
||||
var expectedAggregation = [];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
targetID, targetID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should not traverse if enter/leave the same node", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var expectedAggregation = [];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should traverse enter/leave to sibling - avoids parent", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV_2._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
|
||||
// enter/leave shouldn't fire antyhing on the parent
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_2._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should traverse enter/leave to parent - avoids parent", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should enter from the window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = ''; // From the window or outside of the React sandbox.
|
||||
var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG2},
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should enter from the window to the shallowest", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var leaveID = ''; // From the window or outside of the React sandbox.
|
||||
var enterID = parent.refs.P._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should leave to the window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var enterID = ''; // From the window or outside of the React sandbox.
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
|
||||
it("should leave to the window from the shallowest", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var enterID = ''; // From the window or outside of the React sandbox.
|
||||
var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
|
||||
var expectedAggregation = [
|
||||
{id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
|
||||
{id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
|
||||
];
|
||||
ReactInstanceHandles.traverseEnterLeave(
|
||||
leaveID, enterID, argAggregator, ARG, ARG2
|
||||
);
|
||||
expect(aggregatedArgs).toEqual(expectedAggregation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextDescendantID', function() {
|
||||
it("should return next descendent from window", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
expect(
|
||||
ReactInstanceHandles._getNextDescendantID(
|
||||
'',
|
||||
parent.refs.P_P1._rootNodeID
|
||||
)
|
||||
).toBe(parent.refs.P._rootNodeID);
|
||||
});
|
||||
|
||||
it("should return window for next descendent towards window", function() {
|
||||
expect(ReactInstanceHandles._getNextDescendantID('', '')).toBe('');
|
||||
});
|
||||
|
||||
it("should return self for next descendent towards self", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
expect(
|
||||
ReactInstanceHandles._getNextDescendantID(
|
||||
parent.refs.P_P1._rootNodeID,
|
||||
parent.refs.P_P1._rootNodeID
|
||||
)
|
||||
).toBe(parent.refs.P_P1._rootNodeID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFirstCommonAncestorID', function() {
|
||||
it("should determine the first common ancestor correctly", function() {
|
||||
var parent = renderParentIntoDocument();
|
||||
var ancestors = [
|
||||
// Common ancestor from window to deep element is ''.
|
||||
{ one: {_rootNodeID: ''},
|
||||
two: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Same as previous - reversed direction.
|
||||
{ one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: {_rootNodeID: ''},
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Common ancestor from window to shallow id is ''.
|
||||
{ one: parent.refs.P,
|
||||
two: {_rootNodeID: ''},
|
||||
com: {_rootNodeID: ''}
|
||||
},
|
||||
// Common ancestor with self is self.
|
||||
{ one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
com: parent.refs.P_P1_C1.refs.DIV_1
|
||||
},
|
||||
// Common ancestor with self is self - even if topmost DOM.
|
||||
{ one: parent.refs.P, two: parent.refs.P, com: parent.refs.P },
|
||||
// Siblings
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV_2,
|
||||
com: parent.refs.P_P1_C1.refs.DIV
|
||||
},
|
||||
// Common ancestor with parent is the parent.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1.refs.DIV,
|
||||
com: parent.refs.P_P1_C1.refs.DIV
|
||||
},
|
||||
// Common ancestor with grandparent is the grandparent.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C1,
|
||||
com: parent.refs.P_P1_C1
|
||||
},
|
||||
// Grantparent across subcomponent boundaries.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_P1_C2.refs.DIV_1,
|
||||
com: parent.refs.P_P1
|
||||
},
|
||||
// Something deep with something one-off.
|
||||
{
|
||||
one: parent.refs.P_P1_C1.refs.DIV_1,
|
||||
two: parent.refs.P_OneOff,
|
||||
com: parent.refs.P
|
||||
}
|
||||
];
|
||||
var i;
|
||||
for (i = 0; i < ancestors.length; i++) {
|
||||
var plan = ancestors[i];
|
||||
var firstCommon = ReactInstanceHandles._getFirstCommonAncestorID(
|
||||
plan.one._rootNodeID,
|
||||
plan.two._rootNodeID
|
||||
);
|
||||
expect(firstCommon).toBe(plan.com._rootNodeID);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
@@ -19,12 +19,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactMount = require('ReactMount');
|
||||
var React = require('React');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
var ReactMount = require('ReactMount');
|
||||
|
||||
var reactComponentExpect = require('reactComponentExpect');
|
||||
|
||||
describe('ReactMount', function() {
|
||||
it("should destroy a react root upon request", function() {
|
||||
|
||||
@@ -23,6 +23,7 @@ require('mock-modules');
|
||||
|
||||
var React = require('React');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var objMapKeyVal = require('objMapKeyVal');
|
||||
|
||||
@@ -41,9 +42,8 @@ var stripEmptyValues = function(obj) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Children are currently named '{<providedKey>}' so we can retrieve the
|
||||
* originally provided key by stripping out the braces. This relies on a tiny
|
||||
* implementation detail of the rendering system.
|
||||
* Child names is are wrapped in an prefix and suffix character. We strip those
|
||||
* out. This relies on a tiny implementation detail of the rendering system.
|
||||
*/
|
||||
var getOriginalKey = function(childName) {
|
||||
return childName.substr(1, childName.length - 2);
|
||||
@@ -191,7 +191,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) {
|
||||
var i;
|
||||
var orderedDomIds = [];
|
||||
for (i=0; i < statusDisplayNodes.length; i++) {
|
||||
orderedDomIds.push(statusDisplayNodes[i].id);
|
||||
orderedDomIds.push(ReactID.getID(statusDisplayNodes[i]));
|
||||
}
|
||||
|
||||
var orderedLogicalIds = [];
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
require('mock-modules');
|
||||
|
||||
var React = require('React');
|
||||
var ReactTestUtils = require('ReactTestUtils');
|
||||
var ReactTextComponent = require('ReactTextComponent');
|
||||
|
||||
var reactComponentExpect = require('reactComponentExpect');
|
||||
|
||||
@@ -70,10 +71,6 @@ var assertSingleChild = function(instance, text) {
|
||||
};
|
||||
|
||||
// Helpers
|
||||
var renderSingleContentChild = function(text) {
|
||||
var d = ReactTestUtils.renderIntoDocument(<div content={text} />);
|
||||
return d;
|
||||
};
|
||||
var renderSingleTextChild = function(text) {
|
||||
var d = ReactTestUtils.renderIntoDocument(<div>{text}</div>);
|
||||
return d;
|
||||
@@ -141,7 +138,7 @@ describe('ReactMultiChildText', function() {
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
});
|
||||
|
||||
it('should render zero string as text node then switch to spans', function() {
|
||||
it('should render zero string as text node then switch to spans', function() {
|
||||
var d = renderSingleTextChild('0');
|
||||
// false should act exactly as a null child
|
||||
assertNodeText(d, '0');
|
||||
@@ -149,7 +146,7 @@ describe('ReactMultiChildText', function() {
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
});
|
||||
|
||||
it('should render zero number as text node then switch to spans', function() {
|
||||
it('should render zero number as text node then switch to spans', function() {
|
||||
var d = renderSingleTextChild('0');
|
||||
// false should act exactly as a null child
|
||||
assertNodeText(d, 0);
|
||||
@@ -157,11 +154,6 @@ describe('ReactMultiChildText', function() {
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
});
|
||||
|
||||
it('should render content to single text node', function() {
|
||||
var d = renderSingleContentChild('hello');
|
||||
assertNodeText(d, 'hello');
|
||||
});
|
||||
|
||||
it('should render a single text child to a single text node', function() {
|
||||
var d = renderSingleTextChild('hello');
|
||||
assertNodeText(d, 'hello');
|
||||
@@ -233,12 +225,6 @@ describe('ReactMultiChildText', function() {
|
||||
assertNodeText(d, '0');
|
||||
});
|
||||
|
||||
it('should render content number zero as text node', function() {
|
||||
var d = renderSingleContentChild(0);
|
||||
// false should act exactly as a null child
|
||||
assertNodeText(d, '0');
|
||||
});
|
||||
|
||||
it('should render zero string as string child', function() {
|
||||
var d = renderMultipleTextChildren('0', 234.2);
|
||||
// false should act exactly as a null child
|
||||
@@ -249,7 +235,7 @@ describe('ReactMultiChildText', function() {
|
||||
var d = renderMultipleTextChildren('0', 234.2);
|
||||
// false should act exactly as a null child
|
||||
assertMultiChild(d, '0', '234.2');
|
||||
d.replaceProps({content: '0'});
|
||||
d.replaceProps({children: '0'});
|
||||
assertNodeText(d, '0');
|
||||
});
|
||||
|
||||
@@ -257,7 +243,7 @@ describe('ReactMultiChildText', function() {
|
||||
var d = renderMultipleTextChildren(0, 234.2);
|
||||
// false should act exactly as a null child
|
||||
assertMultiChild(d, '0', '234.2');
|
||||
d.replaceProps({content: 0});
|
||||
d.replaceProps({children: 0});
|
||||
// BELOW REVEALS A BUG IN JSDOM
|
||||
// assertNodeText(d, '0'); // This works in the browser.
|
||||
});
|
||||
@@ -265,7 +251,7 @@ describe('ReactMultiChildText', function() {
|
||||
it('should render multiple children then switch to inline', function() {
|
||||
var d = renderMultipleTextChildren('hello', 'goodbye');
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
d.replaceProps({content: 'hello'});
|
||||
d.replaceProps({children: 'hello'});
|
||||
assertNodeText(d, 'hello');
|
||||
});
|
||||
|
||||
@@ -285,13 +271,6 @@ describe('ReactMultiChildText', function() {
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
});
|
||||
|
||||
it('should render content, then switch to text components ', function() {
|
||||
var d = renderSingleContentChild('hello');
|
||||
assertNodeText(d, 'hello');
|
||||
d.replaceProps({children: ['hello', 'goodbye']});
|
||||
assertMultiChild(d, 'hello', 'goodbye');
|
||||
});
|
||||
|
||||
it('should render inline child, then switch to composite', function() {
|
||||
var d = renderSingleTextChild('hello');
|
||||
assertNodeText(d, 'hello');
|
||||
@@ -301,9 +280,11 @@ describe('ReactMultiChildText', function() {
|
||||
.toBeCompositeComponentWithType(TestCompositeComponent);
|
||||
});
|
||||
|
||||
it('should throw if rendering both content and children', function() {
|
||||
it('should throw if rendering both HTML and children', function() {
|
||||
expect(function() {
|
||||
ReactTestUtils.renderIntoDocument(<div content="asdf">ghjkl</div>);
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<div dangerouslySetInnerHTML={{_html: 'abcdef'}}>ghjkl</div>
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var mocks = require('mocks');
|
||||
|
||||
describe('ReactNativeComponent', function() {
|
||||
|
||||
describe('updateDOM', function() {
|
||||
@@ -68,7 +70,7 @@ describe('ReactNativeComponent', function() {
|
||||
});
|
||||
|
||||
it("should update styles when mutating style object", function() {
|
||||
var styles = { display: 'none', fontFamily: 'Arial' };
|
||||
var styles = { display: 'none', fontFamily: 'Arial', opacity: 0 };
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
@@ -80,12 +82,21 @@ describe('ReactNativeComponent', function() {
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.fontFamily).toEqual('Arial');
|
||||
expect(stubStyle.opacity).toEqual('0');
|
||||
|
||||
styles.fontFamily = 'Helvetica';
|
||||
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.fontFamily).toEqual('Helvetica');
|
||||
expect(stubStyle.opacity).toEqual('0');
|
||||
|
||||
styles.opacity = 0.5;
|
||||
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.fontFamily).toEqual('Helvetica');
|
||||
expect(stubStyle.opacity).toEqual('0.5');
|
||||
});
|
||||
|
||||
it("should update styles if initially null", function() {
|
||||
@@ -100,6 +111,76 @@ describe('ReactNativeComponent', function() {
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
});
|
||||
|
||||
it("should remove attributes", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(<img height='17' />);
|
||||
|
||||
expect(stub.getDOMNode().hasAttribute('height')).toBe(true);
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().hasAttribute('height')).toBe(false);
|
||||
});
|
||||
|
||||
it("should remove properties", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div className='monkey' />);
|
||||
|
||||
expect(stub.getDOMNode().className).toEqual('monkey');
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().className).toEqual('');
|
||||
});
|
||||
|
||||
it("should clear a single style prop when changing 'style'", function() {
|
||||
var styles = {display: 'none', color: 'red'};
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
styles = {color: 'green'};
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('');
|
||||
expect(stubStyle.color).toEqual('green');
|
||||
});
|
||||
|
||||
it("should clear all the styles when removing 'style'", function() {
|
||||
var styles = {display: 'none', color: 'red'};
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stubStyle.display).toEqual('');
|
||||
expect(stubStyle.color).toEqual('');
|
||||
});
|
||||
|
||||
it("should empty element when removing innerHTML", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(
|
||||
<div dangerouslySetInnerHTML={{__html: ":)"}} />
|
||||
);
|
||||
|
||||
expect(stub.getDOMNode().innerHTML).toEqual(':)');
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(stub.getDOMNode().innerHTML).toEqual('');
|
||||
});
|
||||
|
||||
it("should not incur unnecessary DOM mutations", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(<div value="" />);
|
||||
|
||||
var node = stub.getDOMNode();
|
||||
var nodeValue = node.value;
|
||||
var nodeValueSetter = mocks.getMockFunction();
|
||||
Object.defineProperty(node, 'value', {
|
||||
get: function() {
|
||||
return nodeValue;
|
||||
},
|
||||
set: nodeValueSetter.mockImplementation(function(newValue) {
|
||||
nodeValue = newValue;
|
||||
})
|
||||
});
|
||||
|
||||
stub.receiveProps({value: ''}, transaction);
|
||||
expect(nodeValueSetter.mock.calls.length).toBe(0);
|
||||
|
||||
stub.receiveProps({}, transaction);
|
||||
expect(nodeValueSetter.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOpenTagMarkup', function() {
|
||||
@@ -112,6 +193,9 @@ describe('ReactNativeComponent', function() {
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
|
||||
var ReactDefaultInjection = require('ReactDefaultInjection');
|
||||
ReactDefaultInjection.inject();
|
||||
|
||||
var mixInto = require('mixInto');
|
||||
var ReactNativeComponent = require('ReactNativeComponent');
|
||||
|
||||
@@ -136,7 +220,7 @@ describe('ReactNativeComponent', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle className", function() {
|
||||
it("should generate the correct markup with className", function() {
|
||||
expect(genMarkup({ className: 'a' })).toHaveAttribute('class', 'a');
|
||||
expect(genMarkup({ className: 'a b' })).toHaveAttribute('class', 'a b');
|
||||
expect(genMarkup({ className: '' })).toHaveAttribute('class', '');
|
||||
@@ -192,40 +276,30 @@ describe('ReactNativeComponent', function() {
|
||||
|
||||
var mixInto = require('mixInto');
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactMultiChild = require('ReactMultiChild');
|
||||
var ReactNativeComponent = require('ReactNativeComponent');
|
||||
var ReactReconcileTransaction = require('ReactReconcileTransaction');
|
||||
|
||||
var NodeStub = function(initialProps) {
|
||||
var StubNativeComponent = function(initialProps) {
|
||||
ReactComponent.Mixin.construct.call(this, initialProps);
|
||||
};
|
||||
mixInto(NodeStub, ReactNativeComponent.Mixin);
|
||||
mixInto(StubNativeComponent, ReactComponent.Mixin);
|
||||
mixInto(StubNativeComponent, ReactNativeComponent.Mixin);
|
||||
mixInto(StubNativeComponent, ReactMultiChild.Mixin);
|
||||
|
||||
mountComponent = function(props) {
|
||||
var transaction = new ReactReconcileTransaction();
|
||||
return (new NodeStub(props)).mountComponent('test', transaction);
|
||||
var stubComponent = new StubNativeComponent(props);
|
||||
return stubComponent.mountComponent('test', transaction);
|
||||
};
|
||||
});
|
||||
|
||||
it("should validate against multiple children props", function() {
|
||||
expect(function() {
|
||||
mountComponent({ content: '', children: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
mountComponent({ content: '', dangerouslySetInnerHTML: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
mountComponent({ children: '', dangerouslySetInnerHTML: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
'Invariant Violation: Can only set one of `children` or ' +
|
||||
'`props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -239,4 +313,31 @@ describe('ReactNativeComponent', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('unmountComponent', function() {
|
||||
it("should clean up listeners", function() {
|
||||
var React = require('React');
|
||||
var ReactEventEmitter = require('ReactEventEmitter');
|
||||
var ReactID = require('ReactID');
|
||||
|
||||
var container = document.createElement('div');
|
||||
document.documentElement.appendChild(container);
|
||||
|
||||
var callback = function() {};
|
||||
var instance = <div onClick={callback} />;
|
||||
React.renderComponent(instance, container);
|
||||
|
||||
var rootNode = instance.getDOMNode();
|
||||
var rootNodeID = ReactID.getID(rootNode);
|
||||
expect(
|
||||
ReactEventEmitter.getListener(rootNodeID, 'onClick')
|
||||
).toBe(callback);
|
||||
|
||||
React.unmountAndReleaseReactRootNode(container);
|
||||
|
||||
expect(
|
||||
ReactEventEmitter.getListener(rootNodeID, 'onClick')
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var Props = require('ReactProps');
|
||||
var Props = require('ReactPropTypes');
|
||||
|
||||
function typeCheck(declaration, value) {
|
||||
var props = {};
|
||||
if (arguments.length > 1) {
|
||||
props.testProp = value;
|
||||
}
|
||||
return declaration.bind(this, props, 'testProp', 'testComponent');
|
||||
return declaration.bind(null, props, 'testProp', 'testComponent');
|
||||
}
|
||||
|
||||
describe('Primitive Types', function() {
|
||||
@@ -56,6 +56,7 @@ describe('Primitive Types', function() {
|
||||
it("should not throw for valid values", function() {
|
||||
expect(typeCheck(Props.array, [])).not.toThrow();
|
||||
expect(typeCheck(Props.bool, false)).not.toThrow();
|
||||
expect(typeCheck(Props.func, function() {})).not.toThrow();
|
||||
expect(typeCheck(Props.number, 0)).not.toThrow();
|
||||
expect(typeCheck(Props.object, {})).not.toThrow();
|
||||
expect(typeCheck(Props.string, '')).not.toThrow();
|
||||
295
src/core/__tests__/ReactUpdates-test.js
Normal file
295
src/core/__tests__/ReactUpdates-test.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var ReactUpdates;
|
||||
|
||||
describe('ReactUpdates', function() {
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ReactUpdates = require('ReactUpdates');
|
||||
});
|
||||
|
||||
it('should batch state when updating state twice', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
instance.setState({x: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch state when updating two different state keys', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0, y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>({this.state.x}, {this.state.y})</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
instance.setState({y: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(instance.state.y).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch state and props together', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>({this.props.x}, {this.state.y})</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component x={0} />);
|
||||
expect(instance.props.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setProps({x: 1});
|
||||
instance.setState({y: 2});
|
||||
expect(instance.props.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.props.x).toBe(1);
|
||||
expect(instance.state.y).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch parent/child state updates together', function() {
|
||||
var parentUpdateCount = 0;
|
||||
var Parent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
parentUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div><Child ref="child" x={this.state.x} /></div>;
|
||||
}
|
||||
});
|
||||
var childUpdateCount = 0;
|
||||
var Child = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
childUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.props.x + this.state.y}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
var child = instance.refs.child;
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
child.setState({y: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
expect(parentUpdateCount).toBe(0);
|
||||
expect(childUpdateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(child.state.y).toBe(2);
|
||||
expect(parentUpdateCount).toBe(1);
|
||||
expect(childUpdateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch child/parent state updates together', function() {
|
||||
var parentUpdateCount = 0;
|
||||
var Parent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
parentUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div><Child ref="child" x={this.state.x} /></div>;
|
||||
}
|
||||
});
|
||||
var childUpdateCount = 0;
|
||||
var Child = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
childUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.props.x + this.state.y}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
var child = instance.refs.child;
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
child.setState({y: 2});
|
||||
instance.setState({x: 1});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
expect(parentUpdateCount).toBe(0);
|
||||
expect(childUpdateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(child.state.y).toBe(2);
|
||||
expect(parentUpdateCount).toBe(1);
|
||||
|
||||
// When we update the child first, we currently incur two updates because
|
||||
// we aren't smart about what order to process the components in.
|
||||
// TODO: Reduce the update count here to 1
|
||||
expect(childUpdateCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should support chained state updates', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
var innerCallbackRun = false;
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1}, function() {
|
||||
instance.setState({x: 2}, function() {
|
||||
innerCallbackRun = true;
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(2);
|
||||
});
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(innerCallbackRun).toBeTruthy();
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should batch forceUpdate together', function() {
|
||||
var shouldUpdateCount = 0;
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
shouldComponentUpdate: function() {
|
||||
shouldUpdateCount++;
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
var callbacksRun = 0;
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1}, function() {
|
||||
callbacksRun++;
|
||||
});
|
||||
instance.forceUpdate(function() {
|
||||
callbacksRun++;
|
||||
});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(callbacksRun).toBe(2);
|
||||
// shouldComponentUpdate shouldn't be called since we're forcing
|
||||
expect(shouldUpdateCount).toBe(0);
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,9 @@ var ClickCounter = React.createClass({
|
||||
triggerReset: function() {
|
||||
this.setState({count: this.props.initialCount});
|
||||
},
|
||||
handleClick: function() {
|
||||
this.setState({count: this.state.count + 1});
|
||||
},
|
||||
render: function() {
|
||||
var children = [];
|
||||
var i;
|
||||
@@ -45,8 +48,7 @@ var ClickCounter = React.createClass({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="clickIncrementer"
|
||||
onClick={this.setState.bind(this, {count: this.state.count + 1})}>
|
||||
<span className="clickIncrementer" onClick={this.handleClick}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -75,7 +77,7 @@ var TestRefsComponent = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div ref="resetDiv" onClick={this.doReset.bind(this)}>
|
||||
<div ref="resetDiv" onClick={this.doReset}>
|
||||
Reset Me By Clicking This.
|
||||
</div>
|
||||
<GeneralContainerComponent ref="myContainer">
|
||||
|
||||
89
src/dom/CSSProperty.js
Normal file
89
src/dom/CSSProperty.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule CSSProperty
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* CSS properties which accept numbers but are not in units of "px".
|
||||
*/
|
||||
var isUnitlessNumber = {
|
||||
fillOpacity: true,
|
||||
fontWeight: true,
|
||||
opacity: true,
|
||||
orphans: true,
|
||||
zIndex: true,
|
||||
zoom: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Most style properties can be unset by doing .style[prop] = '' but IE8
|
||||
* doesn't like doing that with shorthand properties so for the properties that
|
||||
* IE8 breaks on, which are listed here, we instead unset each of the
|
||||
* individual properties. See http://bugs.jquery.com/ticket/12385.
|
||||
* The 4-value 'clock' properties like margin, padding, border-width seem to
|
||||
* behave without any problems. Curiously, list-style works too without any
|
||||
* special prodding.
|
||||
*/
|
||||
var shorthandPropertyExpansions = {
|
||||
background: {
|
||||
backgroundImage: true,
|
||||
backgroundPosition: true,
|
||||
backgroundRepeat: true,
|
||||
backgroundColor: true
|
||||
},
|
||||
border: {
|
||||
borderWidth: true,
|
||||
borderStyle: true,
|
||||
borderColor: true
|
||||
},
|
||||
borderBottom: {
|
||||
borderBottomWidth: true,
|
||||
borderBottomStyle: true,
|
||||
borderBottomColor: true
|
||||
},
|
||||
borderLeft: {
|
||||
borderLeftWidth: true,
|
||||
borderLeftStyle: true,
|
||||
borderLeftColor: true
|
||||
},
|
||||
borderRight: {
|
||||
borderRightWidth: true,
|
||||
borderRightStyle: true,
|
||||
borderRightColor: true
|
||||
},
|
||||
borderTop: {
|
||||
borderTopWidth: true,
|
||||
borderTopStyle: true,
|
||||
borderTopColor: true
|
||||
},
|
||||
font: {
|
||||
fontStyle: true,
|
||||
fontVariant: true,
|
||||
fontWeight: true,
|
||||
fontSize: true,
|
||||
lineHeight: true,
|
||||
fontFamily: true
|
||||
}
|
||||
};
|
||||
|
||||
var CSSProperty = {
|
||||
isUnitlessNumber: isUnitlessNumber,
|
||||
shorthandPropertyExpansions: shorthandPropertyExpansions
|
||||
};
|
||||
|
||||
module.exports = CSSProperty;
|
||||
@@ -14,11 +14,13 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule CSSPropertyOperations
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CSSProperty = require('CSSProperty');
|
||||
|
||||
var dangerousStyleValue = require('dangerousStyleValue');
|
||||
var escapeTextForBrowser = require('escapeTextForBrowser');
|
||||
var hyphenate = require('hyphenate');
|
||||
@@ -42,7 +44,7 @@ var CSSPropertyOperations = {
|
||||
* Undefined values are ignored so that declarative programming is easier.
|
||||
*
|
||||
* @param {object} styles
|
||||
* @return {string}
|
||||
* @return {?string}
|
||||
*/
|
||||
createMarkupForStyles: function(styles) {
|
||||
var serialized = '';
|
||||
@@ -51,16 +53,17 @@ var CSSPropertyOperations = {
|
||||
continue;
|
||||
}
|
||||
var styleValue = styles[styleName];
|
||||
if (typeof styleValue !== 'undefined') {
|
||||
if (styleValue != null) {
|
||||
serialized += processStyleName(styleName) + ':';
|
||||
serialized += dangerousStyleValue(styleName, styleValue) + ';';
|
||||
}
|
||||
}
|
||||
return serialized;
|
||||
return serialized || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the value for multiple styles on a node.
|
||||
* Sets the value for multiple styles on a node. If a value is specified as
|
||||
* '' (empty string), the corresponding style property will be unset.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {object} styles
|
||||
@@ -71,8 +74,21 @@ var CSSPropertyOperations = {
|
||||
if (!styles.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
var styleValue = styles[styleName];
|
||||
style[styleName] = dangerousStyleValue(styleName, styleValue);
|
||||
var styleValue = dangerousStyleValue(styleName, styles[styleName]);
|
||||
if (styleValue) {
|
||||
style[styleName] = styleValue;
|
||||
} else {
|
||||
var expansion = CSSProperty.shorthandPropertyExpansions[styleName];
|
||||
if (expansion) {
|
||||
// Shorthand property that IE8 won't like unsetting, so unset each
|
||||
// component to placate it
|
||||
for (var individualStyleName in expansion) {
|
||||
style[individualStyleName] = '';
|
||||
}
|
||||
} else {
|
||||
style[styleName] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
* @providesModule DOMChildrenOperations
|
||||
*/
|
||||
|
||||
// Empty blocks improve readability so disable that warning
|
||||
// jshint -W035
|
||||
|
||||
"use strict";
|
||||
|
||||
var Danger = require('Danger');
|
||||
@@ -44,7 +47,7 @@ var REMOVE_AT = keyOf({removeAt: null});
|
||||
*
|
||||
* @param {Element} parent Parent DOM node.
|
||||
* @param {Object} childOperations Description of child operations.
|
||||
* @returns {Array?} Sparse array containing elements by their current index in
|
||||
* @return {Array?} Sparse array containing elements by their current index in
|
||||
* the DOM.
|
||||
*/
|
||||
var _getNodesByOriginalIndex = function(parent, childOperations) {
|
||||
@@ -129,18 +132,13 @@ var manageChildren = function(parent, childOperations) {
|
||||
_placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
|
||||
};
|
||||
|
||||
var setTextNodeValueAtIndex = function(parent, index, val) {
|
||||
parent.childNodes[index].nodeValue = val;
|
||||
};
|
||||
|
||||
/**
|
||||
* Also reexport all of the dangerous functions. It helps to have all dangerous
|
||||
* functions located in a single module `Danger`.
|
||||
*/
|
||||
var DOMChildrenOperations = {
|
||||
dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
|
||||
manageChildren: manageChildren,
|
||||
setTextNodeValueAtIndex: setTextNodeValueAtIndex
|
||||
manageChildren: manageChildren
|
||||
};
|
||||
|
||||
module.exports = DOMChildrenOperations;
|
||||
233
src/dom/DOMProperty.js
Normal file
233
src/dom/DOMProperty.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule DOMProperty
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
/*jslint bitwise: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var DOMPropertyInjection = {
|
||||
/**
|
||||
* Mapping from normalized, camelcased property names to a configuration that
|
||||
* specifies how the associated DOM property should be accessed or rendered.
|
||||
*/
|
||||
MUST_USE_ATTRIBUTE: 0x1,
|
||||
MUST_USE_PROPERTY: 0x2,
|
||||
HAS_BOOLEAN_VALUE: 0x4,
|
||||
HAS_SIDE_EFFECTS: 0x8,
|
||||
|
||||
/**
|
||||
* Inject some specialized knowledge about the DOM. This takes a config object
|
||||
* with the following properties:
|
||||
*
|
||||
* isCustomAttribute: function that given an attribute name will return true
|
||||
* if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
|
||||
* attributes where it's impossible to enumerate all of the possible
|
||||
* attribute names,
|
||||
*
|
||||
* Properties: object mapping DOM property name to one of the
|
||||
* DOMPropertyInjection constants or null. If your attribute isn't in here,
|
||||
* it won't get written to the DOM.
|
||||
*
|
||||
* DOMAttributeNames: object mapping React attribute name to the DOM
|
||||
* attribute name. Attribute names not specified use the **lowercase**
|
||||
* normalized name.
|
||||
*
|
||||
* DOMPropertyNames: similar to DOMAttributeNames but for DOM properties.
|
||||
* Property names not specified use the normalized name.
|
||||
*
|
||||
* DOMMutationMethods: Properties that require special mutation methods. If
|
||||
* `value` is undefined, the mutation method should unset the property.
|
||||
*
|
||||
* @param {object} domPropertyConfig the config as described above.
|
||||
*/
|
||||
injectDOMPropertyConfig: function(domPropertyConfig) {
|
||||
var Properties = domPropertyConfig.Properties || {};
|
||||
var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
|
||||
var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
|
||||
var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
|
||||
|
||||
if (domPropertyConfig.isCustomAttribute) {
|
||||
DOMProperty._isCustomAttributeFunctions.push(
|
||||
domPropertyConfig.isCustomAttribute
|
||||
);
|
||||
}
|
||||
|
||||
for (var propName in Properties) {
|
||||
invariant(
|
||||
!DOMProperty.isStandardName[propName],
|
||||
'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' +
|
||||
'\'%s\' which has already been injected. You may be accidentally ' +
|
||||
'injecting the same DOM property config twice, or you may be ' +
|
||||
'injecting two configs that have conflicting property names.',
|
||||
propName
|
||||
);
|
||||
|
||||
DOMProperty.isStandardName[propName] = true;
|
||||
|
||||
DOMProperty.getAttributeName[propName] =
|
||||
DOMAttributeNames[propName] || propName.toLowerCase();
|
||||
|
||||
DOMProperty.getPropertyName[propName] =
|
||||
DOMPropertyNames[propName] || propName;
|
||||
|
||||
var mutationMethod = DOMMutationMethods[propName];
|
||||
if (mutationMethod) {
|
||||
DOMProperty.getMutationMethod[propName] = mutationMethod;
|
||||
}
|
||||
|
||||
var propConfig = Properties[propName];
|
||||
DOMProperty.mustUseAttribute[propName] =
|
||||
propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE;
|
||||
DOMProperty.mustUseProperty[propName] =
|
||||
propConfig & DOMPropertyInjection.MUST_USE_PROPERTY;
|
||||
DOMProperty.hasBooleanValue[propName] =
|
||||
propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE;
|
||||
DOMProperty.hasSideEffects[propName] =
|
||||
propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS;
|
||||
|
||||
invariant(
|
||||
!DOMProperty.mustUseAttribute[propName] ||
|
||||
!DOMProperty.mustUseProperty[propName],
|
||||
'DOMProperty: Cannot use require using both attribute and property: %s',
|
||||
propName
|
||||
);
|
||||
invariant(
|
||||
DOMProperty.mustUseProperty[propName] ||
|
||||
!DOMProperty.hasSideEffects[propName],
|
||||
'DOMProperty: Properties that have side effects must use property: %s',
|
||||
propName
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
var defaultValueCache = {};
|
||||
|
||||
/**
|
||||
* DOMProperty exports lookup objects that can be used like functions:
|
||||
*
|
||||
* > DOMProperty.isValid['id']
|
||||
* true
|
||||
* > DOMProperty.isValid['foobar']
|
||||
* undefined
|
||||
*
|
||||
* Although this may be confusing, it performs better in general.
|
||||
*
|
||||
* @see http://jsperf.com/key-exists
|
||||
* @see http://jsperf.com/key-missing
|
||||
*/
|
||||
var DOMProperty = {
|
||||
|
||||
/**
|
||||
* Checks whether a property name is a standard property.
|
||||
* @type {Object}
|
||||
*/
|
||||
isStandardName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to attribute names that differ. Attribute
|
||||
* names are used when rendering markup or with `*Attribute()`.
|
||||
* @type {Object}
|
||||
*/
|
||||
getAttributeName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to properties on DOM node instances.
|
||||
* (This includes properties that mutate due to external factors.)
|
||||
* @type {Object}
|
||||
*/
|
||||
getPropertyName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to mutation methods. This will only exist if
|
||||
* mutation cannot be set simply by the property or `setAttribute()`.
|
||||
* @type {Object}
|
||||
*/
|
||||
getMutationMethod: {},
|
||||
|
||||
/**
|
||||
* Whether the property must be accessed and mutated as an object property.
|
||||
* @type {Object}
|
||||
*/
|
||||
mustUseAttribute: {},
|
||||
|
||||
/**
|
||||
* Whether the property must be accessed and mutated using `*Attribute()`.
|
||||
* (This includes anything that fails `<propName> in <element>`.)
|
||||
* @type {Object}
|
||||
*/
|
||||
mustUseProperty: {},
|
||||
|
||||
/**
|
||||
* Whether the property should be removed when set to a falsey value.
|
||||
* @type {Object}
|
||||
*/
|
||||
hasBooleanValue: {},
|
||||
|
||||
/**
|
||||
* Whether or not setting a value causes side effects such as triggering
|
||||
* resources to be loaded or text selection changes. We must ensure that
|
||||
* the value is only set if it has changed.
|
||||
* @type {Object}
|
||||
*/
|
||||
hasSideEffects: {},
|
||||
|
||||
/**
|
||||
* All of the isCustomAttribute() functions that have been injected.
|
||||
*/
|
||||
_isCustomAttributeFunctions: [],
|
||||
|
||||
/**
|
||||
* Checks whether a property name is a custom attribute.
|
||||
* @method
|
||||
*/
|
||||
isCustomAttribute: function(attributeName) {
|
||||
return DOMProperty._isCustomAttributeFunctions.some(
|
||||
function(isCustomAttributeFn) {
|
||||
return isCustomAttributeFn.call(null, attributeName);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the default property value for a DOM property (i.e., not an
|
||||
* attribute). Most default values are '' or false, but not all. Worse yet,
|
||||
* some (in particular, `type`) vary depending on the type of element.
|
||||
*
|
||||
* TODO: Is it better to grab all the possible properties when creating an
|
||||
* element to avoid having to create the same element twice?
|
||||
*/
|
||||
getDefaultValueForProperty: function(nodeName, prop) {
|
||||
var nodeDefaults = defaultValueCache[nodeName];
|
||||
var testElement;
|
||||
if (!nodeDefaults) {
|
||||
defaultValueCache[nodeName] = nodeDefaults = {};
|
||||
}
|
||||
if (!(prop in nodeDefaults)) {
|
||||
testElement = document.createElement(nodeName);
|
||||
nodeDefaults[prop] = testElement[prop];
|
||||
}
|
||||
return nodeDefaults[prop];
|
||||
},
|
||||
|
||||
injection: DOMPropertyInjection
|
||||
};
|
||||
|
||||
module.exports = DOMProperty;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule DOMPropertyOperations
|
||||
* @typechecks
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@@ -86,6 +86,31 @@ var DOMPropertyOperations = {
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the value for a property on a node.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {string} name
|
||||
*/
|
||||
deleteValueForProperty: function(node, name) {
|
||||
if (DOMProperty.isStandardName[name]) {
|
||||
var mutationMethod = DOMProperty.getMutationMethod[name];
|
||||
if (mutationMethod) {
|
||||
mutationMethod(node, undefined);
|
||||
} else if (DOMProperty.mustUseAttribute[name]) {
|
||||
node.removeAttribute(DOMProperty.getAttributeName[name]);
|
||||
} else {
|
||||
var propName = DOMProperty.getPropertyName[name];
|
||||
node[propName] = DOMProperty.getDefaultValueForProperty(
|
||||
node.nodeName,
|
||||
name
|
||||
);
|
||||
}
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -16,6 +16,8 @@
|
||||
* @providesModule Danger
|
||||
*/
|
||||
|
||||
/*jslint evil: true, sub: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
@@ -48,12 +50,86 @@ if (__DEV__) {
|
||||
};
|
||||
}
|
||||
|
||||
var dummies = {};
|
||||
/**
|
||||
* Dummy container used to render all markup.
|
||||
*/
|
||||
var dummyNode = ExecutionEnvironment.canUseDOM ?
|
||||
document.createElement('div') :
|
||||
null;
|
||||
|
||||
function getParentDummy(parent) {
|
||||
var parentTag = parent.tagName;
|
||||
return dummies[parentTag] ||
|
||||
(dummies[parentTag] = document.createElement(parentTag));
|
||||
/**
|
||||
* Some browsers cannot use `innerHTML` to render certain elements standalone,
|
||||
* so we wrap them, render the wrapped nodes, then extract the desired node.
|
||||
*/
|
||||
var markupWrap = {
|
||||
'option': [1, '<select multiple="true">', '</select>'],
|
||||
'legend': [1, '<fieldset>', '</fieldset>'],
|
||||
'area': [1, '<map>', '</map>'],
|
||||
'param': [1, '<object>', '</object>'],
|
||||
'thead': [1, '<table>', '</table>'],
|
||||
'tr': [2, '<table><tbody>', '</tbody></table>'],
|
||||
'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
|
||||
'td': [3, '<table><tbody><tr>', '</tr></tbody></table>']
|
||||
};
|
||||
markupWrap['optgroup'] = markupWrap['option'];
|
||||
markupWrap['tbody'] = markupWrap['thead'];
|
||||
markupWrap['tfoot'] = markupWrap['thead'];
|
||||
markupWrap['colgroup'] = markupWrap['thead'];
|
||||
markupWrap['caption'] = markupWrap['thead'];
|
||||
markupWrap['th'] = markupWrap['td'];
|
||||
|
||||
/**
|
||||
* In IE8, certain elements cannot render alone, so wrap all elements.
|
||||
*/
|
||||
var defaultWrap = [1, '?<div>', '</div>'];
|
||||
|
||||
/**
|
||||
* Feature detection, remove wraps that are unnecessary for the current browser.
|
||||
*/
|
||||
if (dummyNode) {
|
||||
for (var nodeName in markupWrap) {
|
||||
if (!markupWrap.hasOwnProperty(nodeName)) {
|
||||
continue;
|
||||
}
|
||||
dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
|
||||
if (dummyNode.firstChild) {
|
||||
markupWrap[nodeName] = null;
|
||||
}
|
||||
}
|
||||
dummyNode.innerHTML = '<link />';
|
||||
if (dummyNode.firstChild) {
|
||||
defaultWrap = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders markup into nodes. The returned HTMLCollection is live and should be
|
||||
* used immediately (or at least before the next invocation to `renderMarkup`).
|
||||
*
|
||||
* NOTE: Extracting the `nodeName` does not require a regular expression match
|
||||
* because we make assumptions about React-generated markup (i.e. there are no
|
||||
* spaces surrounding the opening tag and there is at least one attribute).
|
||||
* @see http://jsperf.com/extract-nodename
|
||||
*
|
||||
* @param {string} markup
|
||||
* @return {*} An HTMLCollection.
|
||||
*/
|
||||
function renderMarkup(markup) {
|
||||
var node = dummyNode;
|
||||
var nodeName = markup.substring(1, markup.indexOf(' '));
|
||||
|
||||
var wrap = markupWrap[nodeName.toLowerCase()] || defaultWrap;
|
||||
if (wrap) {
|
||||
node.innerHTML = wrap[1] + markup + wrap[2];
|
||||
|
||||
var wrapDepth = wrap[0];
|
||||
while (wrapDepth--) {
|
||||
node = node.lastChild;
|
||||
}
|
||||
} else {
|
||||
node.innerHTML = markup;
|
||||
}
|
||||
return node.childNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +139,7 @@ function getParentDummy(parent) {
|
||||
* @param {Element} elem Parent element.
|
||||
* @param {Element} insert Element to insert.
|
||||
* @param {Element} after Element to insert after.
|
||||
* @returns {Element} Element that was inserted.
|
||||
* @return {Element} Element that was inserted.
|
||||
*/
|
||||
function insertNodeAfterNode(elem, insert, after) {
|
||||
if (__DEV__) {
|
||||
@@ -120,9 +196,7 @@ function dangerouslyInsertMarkupAt(parentNode, markup, index) {
|
||||
if (__DEV__) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var parentDummy = getParentDummy(parentNode);
|
||||
parentDummy.innerHTML = markup;
|
||||
var htmlCollection = parentDummy.childNodes;
|
||||
var htmlCollection = renderMarkup(markup);
|
||||
var afterNode = index ? parentNode.childNodes[index - 1] : null;
|
||||
inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
|
||||
}
|
||||
@@ -141,9 +215,7 @@ function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
|
||||
if (__DEV__) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var parentDummy = getParentDummy(parentNode);
|
||||
parentDummy.innerHTML = markup;
|
||||
var htmlCollection = parentDummy.childNodes;
|
||||
var htmlCollection = renderMarkup(markup);
|
||||
if (__DEV__) {
|
||||
throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user