Merge branch '15.0.2-dev' into 15-stable

This commit is contained in:
Paul O’Shannessy
2016-04-29 17:27:26 -07:00
84 changed files with 3814 additions and 1794 deletions
+1
View File
@@ -4,6 +4,7 @@
"plugins": [
"fbjs-scripts/babel-6/dev-expression",
"syntax-trailing-function-commas",
"babel-plugin-transform-object-rest-spread",
"transform-es2015-template-literals",
"transform-es2015-literals",
"transform-es2015-arrow-functions",
+1
View File
@@ -58,6 +58,7 @@ script:
-F "react-dom-server.min=@build/react-dom-server.min.js" \
-F "npm-react=@build/packages/react.tgz" \
-F "npm-react-dom=@build/packages/react-dom.tgz" \
-F "npm-react-native=@build/packages/react-native-renderer.tgz" \
-F "commit=$TRAVIS_COMMIT" \
-F "date=`git log --format='%ct' -1`" \
-F "pull_request=$TRAVIS_PULL_REQUEST" \
+24
View File
@@ -1,3 +1,27 @@
## 15.0.2 (April 29, 2016)
### React
- Removed extraneous files from npm package. ([@gaearon](https://github.com/gaearon) in [#6388](https://github.com/facebook/react/pull/6388))
- Ensure `componentWillUnmount` is only called once. ([@jimfb](https://github.com/jimfb) in [#6613](https://github.com/facebook/react/pull/6613))
### ReactDOM
- Fixed bug resulting in disabled buttons responding to mouse events in IE. ([@nhunzaker](https://github.com/nhunzaker) in [#6215](https://github.com/facebook/react/pull/6215))
- Ensure `<option>`s are correctly selected when inside `<optgroup>`. ([@trevorsmith](https://github.com/trevorsmith) in [#6442](https://github.com/facebook/react/pull/6442))
- Restore support for rendering into a shadow root. ([@Wildhoney](https://github.com/Wildhoney) in [#6462](https://github.com/facebook/react/pull/6462))
- Ensure nested `<body>` elements are caught when warning for invalid markup. ([@keyanzhang](https://github.com/keyanzhang) in [#6469](https://github.com/facebook/react/pull/6469))
- Improve warning when encountering multiple elements with the same key. ([@hkal](https://github.com/hkal) in [#6500](https://github.com/facebook/react/pull/6500))
### React TestUtils Add-on
- Ensure that functional components do not have an owner. ([@gaearon](https://github.com/gaearon) in [#6362](https://github.com/facebook/react/pull/6362))
- Handle invalid arguments to `scryRenderedDOMComponentsWithClass` better. ([@ipeters90](https://github.com/ipeters90) in [#6529](https://github.com/facebook/react/pull/6529))
### React Perf Add-on
- Ignore DOM operations that occur outside the batch operation. ([@gaearon](https://github.com/gaearon) in [#6516](https://github.com/facebook/react/pull/6516))
### React Native Renderer
- These files are now shipped inside the React npm package. They have no impact on React core or ReactDOM.
## 15.0.1 (April 8, 2016)
### React
+6
View File
@@ -74,6 +74,10 @@ module.exports = function(grunt) {
grunt.registerTask('npm-react-dom:release', npmReactDOMTasks.buildRelease);
grunt.registerTask('npm-react-dom:pack', npmReactDOMTasks.packRelease);
var npmReactNativeTasks = require('./grunt/tasks/npm-react-native');
grunt.registerTask('npm-react-native:release', npmReactNativeTasks.buildRelease);
grunt.registerTask('npm-react-native:pack', npmReactNativeTasks.packRelease);
var npmReactAddonsTasks = require('./grunt/tasks/npm-react-addons');
grunt.registerTask('npm-react-addons:release', npmReactAddonsTasks.buildReleases);
grunt.registerTask('npm-react-addons:pack', npmReactAddonsTasks.packReleases);
@@ -127,6 +131,8 @@ module.exports = function(grunt) {
'npm-react:pack',
'npm-react-dom:release',
'npm-react-dom:pack',
'npm-react-native:release',
'npm-react-native:pack',
'npm-react-addons:release',
'npm-react-addons:pack',
'compare_size',
+46
View File
@@ -0,0 +1,46 @@
'use strict';
var fs = require('fs');
var grunt = require('grunt');
var src = 'packages/react-native-renderer/';
var dest = 'build/packages/react-native-renderer/';
function buildRelease() {
if (grunt.file.exists(dest)) {
grunt.file.delete(dest);
}
// Copy to build/packages/react-native-renderer
var mappings = [].concat(
grunt.file.expandMapping('**/*', dest, {cwd: src}),
grunt.file.expandMapping('{LICENSE,PATENTS}', dest)
);
mappings.forEach(function(mapping) {
var mappingSrc = mapping.src[0];
var mappingDest = mapping.dest;
if (grunt.file.isDir(mappingSrc)) {
grunt.file.mkdir(mappingDest);
} else {
grunt.file.copy(mappingSrc, mappingDest);
}
});
}
function packRelease() {
var done = this.async();
var spawnCmd = {
cmd: 'npm',
args: ['pack', 'packages/react-native-renderer'],
};
grunt.util.spawn(spawnCmd, function() {
var buildSrc = 'react-native-renderer-' + grunt.config.data.pkg.version + '.tgz';
var buildDest = 'build/packages/react-native-renderer.tgz';
fs.rename(buildSrc, buildDest, done);
});
}
module.exports = {
buildRelease: buildRelease,
packRelease: packRelease,
};
+1 -1
View File
@@ -16,7 +16,7 @@ var BOWER_FILES = [
];
var EXAMPLES_PATH = 'examples/';
var EXAMPLES_GLOB = [EXAMPLES_PATH + '**/*.*'];
var EXAMPLES_GLOB = [EXAMPLES_PATH + '**/*.*', EXAMPLES_PATH + '**/.babelrc'];
var STARTER_PATH = 'starter/';
var STARTER_GLOB = [STARTER_PATH + '/**/*.*'];
+2
View File
@@ -11,6 +11,8 @@ module.exports = function() {
grunt.file.readJSON('./packages/react/package.json').version,
'packages/react-dom/package.json':
grunt.file.readJSON('./packages/react-dom/package.json').version,
'packages/react-native-renderer/package.json':
grunt.file.readJSON('./packages/react-native-renderer/package.json').version,
'packages/react-addons/package.json (version)': addonsData.version,
// Get the "version" without the range bit
'packages/react-addons/package.json (react dependency)': addonsData.peerDependencies.react.slice(1),
+29 -9
View File
@@ -20,6 +20,7 @@ var paths = {
react: {
src: [
'src/**/*.js',
'!src/**/__benchmarks__/**/*.js',
'!src/**/__tests__/**/*.js',
'!src/**/__mocks__/**/*.js',
'!src/shared/vendor/**/*.js',
@@ -28,17 +29,36 @@ var paths = {
},
};
var fbjsModuleMap = require('fbjs/module-map');
var moduleMap = {};
for (var key in fbjsModuleMap) {
moduleMap[key] = fbjsModuleMap[key];
}
var whiteListNames = [
'deepDiffer',
'deepFreezeAndThrowOnMutationInDev',
'flattenStyle',
'InitializeJavaScriptAppEngine',
'InteractionManager',
'JSTimersExecution',
'merge',
'Platform',
'RCTEventEmitter',
'RCTLog',
'TextInputState',
'UIManager',
'View',
];
whiteListNames.forEach(function(name) {
moduleMap[name] = name;
});
moduleMap['object-assign'] = 'object-assign';
var babelOpts = {
plugins: [
[babelPluginModules, {
map: Object.assign(
{},
require('fbjs/module-map'),
{
'object-assign': 'object-assign',
}
),
}],
[babelPluginModules, { map: moduleMap }],
],
};
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "react-build",
"private": true,
"version": "15.0.1",
"version": "15.0.2-alpha.4",
"devDependencies": {
"async": "^1.5.0",
"babel-cli": "^6.6.5",
@@ -25,6 +25,7 @@
"babel-plugin-transform-es2015-template-literals": "^6.5.2",
"babel-plugin-transform-es3-member-expression-literals": "^6.5.0",
"babel-plugin-transform-es3-property-literals": "^6.5.0",
"babel-plugin-transform-object-rest-spread": "^6.6.5",
"babel-preset-react": "^6.5.0",
"browserify": "^13.0.0",
"bundle-collapser": "^1.1.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "react-addons-template",
"version": "15.0.1",
"version": "15.0.2-alpha.4",
"main": "index.js",
"repository": "facebook/react",
"keywords": [
@@ -10,6 +10,6 @@
"license": "BSD-3-Clause",
"dependencies": {},
"peerDependencies": {
"react": "^15.0.1"
"react": "^15.0.2-alpha.4"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "react-dom",
"version": "15.0.1",
"version": "15.0.2-alpha.4",
"description": "React package for working with the DOM.",
"main": "index.js",
"repository": "facebook/react",
@@ -14,6 +14,6 @@
"homepage": "https://facebook.github.io/react/",
"dependencies": {},
"peerDependencies": {
"react": "^15.0.1"
"react": "^15.0.2-alpha.4"
}
}
+5
View File
@@ -0,0 +1,5 @@
# `react-native-renderer`
This package is the renderer that is used by the react-native package.
It is intended to be used inside the react-native environment. It is not
intended to be used stand alone.
+3
View File
@@ -0,0 +1,3 @@
'use strict';
module.exports = require('react/lib/ReactNative');
@@ -0,0 +1,19 @@
{
"name": "react-native-renderer",
"version": "15.0.2-alpha.4",
"description": "React package for use inside react-native.",
"main": "index.js",
"repository": "facebook/react",
"keywords": [
"react",
"react-native"
],
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/facebook/react/issues"
},
"homepage": "https://facebook.github.io/react-native/",
"dependencies": {
"react": "^15.0.2-alpha.4"
}
}
-12
View File
@@ -1,12 +0,0 @@
'use strict';
var ReactUpdates = require('./ReactUpdates');
// TODO: In React Native, ReactTestUtils depends on ./ReactDOM (for
// renderIntoDocument, which should never be called) and Relay depends on
// react-dom (for batching). Once those are fixed, nothing in RN should import
// this module and this file can go away.
module.exports = {
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "react",
"description": "React is a JavaScript library for building user interfaces.",
"version": "15.0.1",
"version": "15.0.2-alpha.4",
"keywords": [
"react"
],
+1 -1
View File
@@ -11,4 +11,4 @@
'use strict';
module.exports = '15.0.1';
module.exports = '15.0.2-alpha.4';
+1 -1
View File
@@ -146,7 +146,7 @@ function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
/**
* Maps children that are typically specified as `props.children`.
*
* The provided mapFunction(child, key, index) will be called for each
* The provided mapFunction(child, index) will be called for each
* leaf child.
*
* @param {?*} children Children tree container.
+1 -1
View File
@@ -22,7 +22,7 @@ var invariant = require('invariant');
* of children.
*
* @param {?object} children Child collection structure.
* @return {ReactComponent} The first and only `ReactComponent` contained in the
* @return {ReactElement} The first and only `ReactElement` contained in the
* structure.
*/
function onlyChild(children) {
-505
View File
@@ -1,505 +0,0 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule OrderedMap
*/
'use strict';
var invariant = require('invariant');
var PREFIX = 'key:';
/**
* Utility to extract a backing object from an initialization `Array`, allowing
* the caller to assist in resolving the unique ID for each entry via the
* `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or
* numbers.
* @param {Array<Object!>} arr Array of items.
* @param {function} keyExtractor Extracts a unique key from each item.
* @return {Object} Map from unique key to originating value that the key was
* extracted from.
* @throws Exception if the initialization array has duplicate extracted keys.
*/
function extractObjectFromArray(arr, keyExtractor) {
var normalizedObj = {};
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
var key = keyExtractor(item);
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
invariant(
!(normalizedKey in normalizedObj),
'OrderedMap: IDs returned by the key extraction function must be unique.'
);
normalizedObj[normalizedKey] = item;
}
return normalizedObj;
}
/**
* Utility class for mappings with ordering. This class is to be used in an
* immutable manner. A `OrderedMap` is very much like the native JavaScript
* object, where keys map to values via the `get()` function. Also, like the
* native JavaScript object, there is an ordering associated with the mapping.
* This class is helpful because it eliminates many of the pitfalls that come
* with the native JavaScript ordered mappings. Specifically, there are
* inconsistencies with numeric keys in some JavaScript implementations
* (enumeration ordering). This class protects against those pitfalls and
* provides functional utilities for dealing with these `OrderedMap`s.
*
* - TODO:
* - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s.
* - mapReverse().
*
* @class {OrderedMap}
* @constructor {OrderedMap}
* @param {Object} normalizedObj Object that is known to be a defensive copy of
* caller supplied data. We require a defensive copy to guard against callers
* mutating. It is also assumed that the keys of `normalizedObj` have been
* normalized and do not contain any numeric-appearing strings.
* @param {number} computedLength The precomputed length of `_normalizedObj`
* keys.
* @private
*/
function OrderedMapImpl(normalizedObj, computedLength) {
this._normalizedObj = normalizedObj;
this._computedPositions = null;
this.length = computedLength;
}
/**
* Validates a "public" key - that is, one that the public facing API supplies.
* The key is then normalized for internal storage. In order to be considered
* valid, all keys must be non-empty, defined, non-null strings or numbers.
*
* @param {string?} key Validates that the key is suitable for use in a
* `OrderedMap`.
* @throws Error if key is not appropriate for use in `OrderedMap`.
*/
function assertValidPublicKey(key) {
invariant(
key !== '' && (typeof key === 'string' || typeof key === 'number'),
'OrderedMap: Key must be non-empty, non-null string or number.'
);
}
/**
* Validates that arguments to range operations are within the correct limits.
*
* @param {number} start Start of range.
* @param {number} length Length of range.
* @param {number} actualLen Actual length of range that should not be
* exceeded.
* @throws Error if range arguments are out of bounds.
*/
function assertValidRangeIndices(start, length, actualLen) {
invariant(
typeof start === 'number' &&
typeof length === 'number' &&
length >= 0 &&
start >= 0 &&
start + length <= actualLen,
'OrderedMap: `mapRange` and `forEachRange` expect non-negative start and ' +
'length arguments within the bounds of the instance.'
);
}
/**
* Merges two "normalized" objects (objects who's key have been normalized) into
* a `OrderedMap`.
*
* @param {Object} a Object of key value pairs.
* @param {Object} b Object of key value pairs.
* @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`.
*/
function _fromNormalizedObjects(a, b) {
// Second optional, both must be plain JavaScript objects.
invariant(
a && a.constructor === Object && (!b || b.constructor === Object),
'OrderedMap: Corrupted instance of OrderedMap detected.'
);
var newSet = {};
var length = 0;
var key;
for (key in a) {
if (a.hasOwnProperty(key)) {
newSet[key] = a[key];
length++;
}
}
for (key in b) {
if (b.hasOwnProperty(key)) {
// Increment length if not already added via first object (a)
if (!(key in newSet)) {
length++;
}
newSet[key] = b[key];
}
}
return new OrderedMapImpl(newSet, length);
}
/**
* Methods for `OrderedMap` instances.
*
* @lends OrderedMap.prototype
* TODO: Make this data structure lazy, unify with LazyArray.
* TODO: Unify this with ImmutableObject - it is to be used immutably.
* TODO: If so, consider providing `fromObject` API.
* TODO: Create faster implementation of merging/mapping from original Array,
* without having to first create an object - simply for the sake of merging.
*/
var OrderedMapMethods = {
/**
* Returns whether or not a given key is present in the map.
*
* @param {string} key Valid string key to lookup membership for.
* @return {boolean} Whether or not `key` is a member of the map.
* @throws Error if provided known invalid key.
*/
has: function(key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
return normalizedKey in this._normalizedObj;
},
/**
* Returns the object for a given key, or `undefined` if not present. To
* distinguish an undefined entry vs not being in the set, use `has()`.
*
* @param {string} key String key to lookup the value for.
* @return {Object?} Object at key `key`, or undefined if not in map.
* @throws Error if provided known invalid key.
*/
get: function(key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
return this.has(key) ? this._normalizedObj[normalizedKey] : undefined;
},
/**
* Merges, appending new keys to the end of the ordering. Keys in `orderedMap`
* that are redundant with `this`, maintain the same ordering index that they
* had in `this`. This is how standard JavaScript object merging would work.
* If you wish to prepend a `OrderedMap` to the beginning of another
* `OrderedMap` then simply reverse the order of operation. This is the analog
* to `merge(x, y)`.
*
* @param {OrderedMap} orderedMap OrderedMap to merge onto the end.
* @return {OrderedMap} New OrderedMap that represents the result of the
* merge.
*/
merge: function(orderedMap) {
invariant(
orderedMap instanceof OrderedMapImpl,
'OrderedMap.merge(...): Expected an OrderedMap instance.'
);
return _fromNormalizedObjects(
this._normalizedObj,
orderedMap._normalizedObj
);
},
/**
* Functional map API. Returns a new `OrderedMap`.
*
* @param {Function} cb Callback to invoke for each item.
* @param {Object?=} context Context to invoke callback from.
* @return {OrderedMap} OrderedMap that results from mapping.
*/
map: function(cb, context) {
return this.mapRange(cb, 0, this.length, context);
},
/**
* The callback `cb` is invoked with the arguments (item, key,
* indexInOriginal).
*
* @param {Function} cb Determines result for each item.
* @param {number} start Start index of map range.
* @param {end} length End index of map range.
* @param {*!?} context Context of callback invocation.
* @return {OrderedMap} OrderedMap resulting from mapping the range.
*/
mapRange: function(cb, start, length, context) {
var thisSet = this._normalizedObj;
var newSet = {};
var i = 0;
assertValidRangeIndices(start, length, this.length);
var end = start + length - 1;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
if (i >= start) {
if (i > end) {
break;
}
var item = thisSet[key];
newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i);
}
i++;
}
}
return new OrderedMapImpl(newSet, length);
},
/**
* Function filter API. Returns new `OrderedMap`.
*
* @param {Function} cb Callback to invoke for each item.
* @param {Object?=} context Context to invoke callback from.
* @return {OrderedMap} OrderedMap that results from filtering.
*/
filter: function(cb, context) {
return this.filterRange(cb, 0, this.length, context);
},
/**
* The callback `cb` is invoked with the arguments (item, key,
* indexInOriginal).
*
* @param {Function} cb Returns true if item should be in result.
* @param {number} start Start index of filter range.
* @param {number} length End index of map range.
* @param {*!?} context Context of callback invocation.
* @return {OrderedMap} OrderedMap resulting from filtering the range.
*/
filterRange: function(cb, start, length, context) {
var newSet = {};
var newSetLength = 0;
this.forEachRange(function(item, key, originalIndex) {
if (cb.call(context, item, key, originalIndex)) {
var normalizedKey = PREFIX + key;
newSet[normalizedKey] = item;
newSetLength++;
}
}, start, length);
return new OrderedMapImpl(newSet, newSetLength);
},
forEach: function(cb, context) {
this.forEachRange(cb, 0, this.length, context);
},
forEachRange: function(cb, start, length, context) {
assertValidRangeIndices(start, length, this.length);
var thisSet = this._normalizedObj;
var i = 0;
var end = start + length - 1;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
if (i >= start) {
if (i > end) {
break;
}
var item = thisSet[key];
cb.call(context, item, key.substr(PREFIX.length), i);
}
i++;
}
}
},
/**
* Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll
* impose an additional restriction here that the length of mapping be greater
* than zero - the only reason is that there are many ways to express length
* zero in terms of two keys and that is confusing.
*/
mapKeyRange: function(cb, startKey, endKey, context) {
var startIndex = this.indexOfKey(startKey);
var endIndex = this.indexOfKey(endKey);
invariant(
startIndex !== undefined && endIndex !== undefined,
'mapKeyRange must be given keys that are present.'
);
invariant(
endIndex >= startIndex,
'OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.'
);
return this.mapRange(cb, startIndex, (endIndex - startIndex) + 1, context);
},
forEachKeyRange: function(cb, startKey, endKey, context) {
var startIndex = this.indexOfKey(startKey);
var endIndex = this.indexOfKey(endKey);
invariant(
startIndex !== undefined && endIndex !== undefined,
'forEachKeyRange must be given keys that are present.'
);
invariant(
endIndex >= startIndex,
'OrderedMap.forEachKeyRange(...): `endKey` must not come before ' +
'`startIndex`.'
);
this.forEachRange(cb, startIndex, (endIndex - startIndex) + 1, context);
},
/**
* @param {number} pos Index to search for key at.
* @return {string|undefined} Either the key at index `pos` or undefined if
* not in map.
*/
keyAtIndex: function(pos) {
var computedPositions = this._getOrComputePositions();
var keyAtPos = computedPositions.keyByIndex[pos];
return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined;
},
/**
* @param {string} key String key from which to find the next key.
* @return {string|undefined} Either the next key, or undefined if there is no
* next key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
keyAfter: function(key) {
return this.nthKeyAfter(key, 1);
},
/**
* @param {string} key String key from which to find the preceding key.
* @return {string|undefined} Either the preceding key, or undefined if there
* is no preceding.key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
keyBefore: function(key) {
return this.nthKeyBefore(key, 1);
},
/**
* @param {string} key String key from which to find a following key.
* @param {number} n Distance to scan forward after `key`.
* @return {string|undefined} Either the nth key after `key`, or undefined if
* there is no next key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
nthKeyAfter: function(key, n) {
var curIndex = this.indexOfKey(key);
invariant(
curIndex !== undefined,
'OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.',
key
);
return this.keyAtIndex(curIndex + n);
},
/**
* @param {string} key String key from which to find a preceding key.
* @param {number} n Distance to scan backwards before `key`.
* @return {string|undefined} Either the nth key before `key`, or undefined if
* there is no previous key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
nthKeyBefore: function(key, n) {
return this.nthKeyAfter(key, -n);
},
/**
* @param {string} key Key to find the index of.
* @return {number|undefined} Index of the provided key, or `undefined` if the
* key is not found.
*/
indexOfKey: function(key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
var computedPositions = this._getOrComputePositions();
var computedPosition = computedPositions.indexByKey[normalizedKey];
// Just writing it this way to make it clear this is intentional.
return computedPosition === undefined ? undefined : computedPosition;
},
/**
* @return {Array} An ordered array of this object's values.
*/
toArray: function() {
var result = [];
var thisSet = this._normalizedObj;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
result.push(thisSet[key]);
}
}
return result;
},
/**
* Finds the key at a given position, or indicates via `undefined` that that
* position does not exist in the `OrderedMap`. It is appropriate to return
* undefined, indicating that the key doesn't exist in the `OrderedMap`
* because `undefined` is not ever a valid `OrderedMap` key.
*
* @private
* @return {string?} Name of the item at position `pos`, or `undefined` if
* there is no item at that position.
*/
_getOrComputePositions: function() {
// TODO: Entertain computing this at construction time in some less
// performance critical paths.
var computedPositions = this._computedPositions;
if (!computedPositions) {
this._computePositions();
}
return this._computedPositions;
},
/**
* Precomputes the index/key mapping for future lookup. Since `OrderedMap`s
* are immutable, there is only ever a need to perform this once.
* @private
*/
_computePositions: function() {
this._computedPositions = {
keyByIndex: {},
indexByKey: {},
};
var keyByIndex = this._computedPositions.keyByIndex;
var indexByKey = this._computedPositions.indexByKey;
var index = 0;
var thisSet = this._normalizedObj;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
keyByIndex[index] = key;
indexByKey[key] = index;
index++;
}
}
},
};
Object.assign(OrderedMapImpl.prototype, OrderedMapMethods);
var OrderedMap = {
from: function(orderedMap) {
invariant(
orderedMap instanceof OrderedMapImpl,
'OrderedMap.from(...): Expected an OrderedMap instance.'
);
return _fromNormalizedObjects(orderedMap._normalizedObj, null);
},
fromArray: function(arr, keyExtractor) {
invariant(
Array.isArray(arr),
'OrderedMap.fromArray(...): First argument must be an array.'
);
invariant(
typeof keyExtractor === 'function',
'OrderedMap.fromArray(...): Second argument must be a function used ' +
'to determine the unique key for each entry.'
);
return new OrderedMapImpl(
extractObjectFromArray(arr, keyExtractor),
arr.length
);
},
};
module.exports = OrderedMap;
@@ -1,107 +0,0 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactPropTransferer
*/
'use strict';
var emptyFunction = require('emptyFunction');
var joinClasses = require('joinClasses');
/**
* Creates a transfer strategy that will merge prop values using the supplied
* `mergeStrategy`. If a prop was previously unset, this just sets it.
*
* @param {function} mergeStrategy
* @return {function}
*/
function createTransferStrategy(mergeStrategy) {
return function(props, key, value) {
if (!props.hasOwnProperty(key)) {
props[key] = value;
} else {
props[key] = mergeStrategy(props[key], value);
}
};
}
var transferStrategyMerge = createTransferStrategy(function(a, b) {
// `merge` overrides the first object's (`props[key]` above) keys using the
// second object's (`value`) keys. An object's style's existing `propA` would
// get overridden. Flip the order here.
return Object.assign({}, b, a);
});
/**
* Transfer strategies dictate how props are transferred by `transferPropsTo`.
* NOTE: if you add any more exceptions to this list you should be sure to
* update `cloneWithProps()` accordingly.
*/
var TransferStrategies = {
/**
* Never transfer `children`.
*/
children: emptyFunction,
/**
* Transfer the `className` prop by merging them.
*/
className: createTransferStrategy(joinClasses),
/**
* Transfer the `style` prop (which is an object) by merging them.
*/
style: transferStrategyMerge,
};
/**
* Mutates the first argument by transferring the properties from the second
* argument.
*
* @param {object} props
* @param {object} newProps
* @return {object}
*/
function transferInto(props, newProps) {
for (var thisKey in newProps) {
if (!newProps.hasOwnProperty(thisKey)) {
continue;
}
var transferStrategy = TransferStrategies[thisKey];
if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) {
transferStrategy(props, thisKey, newProps[thisKey]);
} else if (!props.hasOwnProperty(thisKey)) {
props[thisKey] = newProps[thisKey];
}
}
return props;
}
/**
* ReactPropTransferer are capable of transferring props to another component
* using a `transferPropsTo` method.
*
* @class ReactPropTransferer
*/
var ReactPropTransferer = {
/**
* Merge two props objects using TransferStrategies.
*
* @param {object} oldProps original props (they take precedence)
* @param {object} newProps new props to merge in
* @return {object} a new object containing both sets of props merged.
*/
mergeProps: function(oldProps, newProps) {
return transferInto(Object.assign({}, oldProps), newProps);
},
};
module.exports = ReactPropTransferer;
@@ -83,5 +83,7 @@ describe('ReactContextValidator', function() {
expect(isTagStackValid(['table', 'tr'])).toBe(false);
expect(isTagStackValid(['div', 'ul', 'li', 'div', 'li'])).toBe(false);
expect(isTagStackValid(['div', 'html'])).toBe(false);
expect(isTagStackValid(['body', 'body'])).toBe(false);
expect(isTagStackValid(['svg', 'foreignObject', 'body', 'p'])).toBe(false);
});
});
@@ -208,6 +208,7 @@ if (__DEV__) {
case 'rt':
return impliedEndTags.indexOf(parentTag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
@@ -0,0 +1,50 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule DisabledInputUtils
*/
'use strict';
var disableableMouseListenerNames = {
onClick: true,
onDoubleClick: true,
onMouseDown: true,
onMouseMove: true,
onMouseUp: true,
onClickCapture: true,
onDoubleClickCapture: true,
onMouseDownCapture: true,
onMouseMoveCapture: true,
onMouseUpCapture: true,
};
/**
* Implements a native component that does not receive mouse events
* when `disabled` is set.
*/
var DisabledInputUtils = {
getNativeProps: function(inst, props) {
if (!props.disabled) {
return props;
}
// Copy the props, except the mouse listeners
var nativeProps = {};
for (var key in props) {
if (!disableableMouseListenerNames[key] && props.hasOwnProperty(key)) {
nativeProps[key] = props[key];
}
}
return nativeProps;
},
};
module.exports = DisabledInputUtils;
@@ -11,40 +11,14 @@
'use strict';
var mouseListenerNames = {
onClick: true,
onDoubleClick: true,
onMouseDown: true,
onMouseMove: true,
onMouseUp: true,
onClickCapture: true,
onDoubleClickCapture: true,
onMouseDownCapture: true,
onMouseMoveCapture: true,
onMouseUpCapture: true,
};
var DisabledInputUtils = require('DisabledInputUtils');
/**
* Implements a <button> native component that does not receive mouse events
* when `disabled` is set.
*/
var ReactDOMButton = {
getNativeProps: function(inst, props) {
if (!props.disabled) {
return props;
}
// Copy the props, except the mouse listeners
var nativeProps = {};
for (var key in props) {
if (props.hasOwnProperty(key) && !mouseListenerNames[key]) {
nativeProps[key] = props[key];
}
}
return nativeProps;
},
getNativeProps: DisabledInputUtils.getNativeProps,
};
module.exports = ReactDOMButton;
@@ -11,6 +11,7 @@
'use strict';
var DisabledInputUtils = require('DisabledInputUtils');
var DOMPropertyOperations = require('DOMPropertyOperations');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
@@ -72,7 +73,7 @@ var ReactDOMInput = {
// Make sure we set .type before any other properties (setting .value
// before .type means .value is lost in IE11 and below)
type: undefined,
}, props, {
}, DisabledInputUtils.getNativeProps(inst, props), {
defaultChecked: undefined,
defaultValue: undefined,
value: value != null ? value : inst._wrapperState.initialValue,
@@ -33,8 +33,16 @@ var ReactDOMOption = {
// Look up whether this option is 'selected'
var selectValue = null;
if (nativeParent != null && nativeParent._tag === 'select') {
selectValue = ReactDOMSelect.getSelectValueContext(nativeParent);
if (nativeParent != null) {
var selectParent = nativeParent;
if (selectParent._tag === 'optgroup') {
selectParent = selectParent._nativeParent;
}
if (selectParent != null && selectParent._tag === 'select') {
selectValue = ReactDOMSelect.getSelectValueContext(selectParent);
}
}
// If the value is null (e.g., no specified value or after initial mount)
@@ -11,6 +11,7 @@
'use strict';
var DisabledInputUtils = require('DisabledInputUtils');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactUpdates = require('ReactUpdates');
@@ -158,7 +159,7 @@ function updateOptions(inst, multiple, propValue) {
*/
var ReactDOMSelect = {
getNativeProps: function(inst, props) {
return Object.assign({}, props, {
return Object.assign({}, DisabledInputUtils.getNativeProps(inst, props), {
onChange: inst._wrapperState.onChange,
value: undefined,
});
@@ -11,6 +11,7 @@
'use strict';
var DisabledInputUtils = require('DisabledInputUtils');
var DOMPropertyOperations = require('DOMPropertyOperations');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
@@ -67,7 +68,7 @@ var ReactDOMTextarea = {
// Always set children to the same thing. In IE9, the selection range will
// get reset if `textContent` is mutated.
var nativeProps = Object.assign({}, props, {
var nativeProps = Object.assign({}, DisabledInputUtils.getNativeProps(inst, props), {
defaultValue: undefined,
value: undefined,
children: inst._wrapperState.initialValue,
@@ -0,0 +1,108 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
describe('DisabledInputUtils', function() {
var React;
var ReactDOM;
var ReactTestUtils;
var elements = ['button', 'input', 'select', 'textarea'];
function expectClickThru(element) {
onClick.mockClear();
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
expect(onClick.mock.calls.length).toBe(1);
}
function expectNoClickThru(element) {
onClick.mockClear();
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(element));
expect(onClick.mock.calls.length).toBe(0);
}
function mounted(element) {
element = ReactTestUtils.renderIntoDocument(element);
return element;
}
var onClick = jest.genMockFn();
elements.forEach(function(tagName) {
describe(tagName, function() {
beforeEach(function() {
React = require('React');
ReactDOM = require('ReactDOM');
ReactTestUtils = require('ReactTestUtils');
});
it('should forward clicks when it starts out not disabled', function() {
var element = React.createElement(tagName, {
onClick: onClick,
});
expectClickThru(mounted(element));
});
it('should not forward clicks when it starts out disabled', function() {
var element = React.createElement(tagName, {
onClick: onClick,
disabled: true,
});
expectNoClickThru(mounted(element));
});
it('should forward clicks when it becomes not disabled', function() {
var container = document.createElement('div');
var element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick, disabled: true }),
container
);
element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick }),
container
);
expectClickThru(element);
});
it('should not forward clicks when it becomes disabled', function() {
var container = document.createElement('div');
var element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick }),
container
);
element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick, disabled: true }),
container
);
expectNoClickThru(element);
});
it('should work correctly if the listener is changed', function() {
var container = document.createElement('div');
var element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick, disabled: true }),
container
);
element = ReactDOM.render(
React.createElement(tagName, { onClick: onClick, disabled: false }),
container
);
expectClickThru(element);
});
});
});
});
@@ -1,93 +0,0 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
describe('ReactDOMButton', function() {
var React;
var ReactDOM;
var ReactTestUtils;
var onClick = jest.genMockFn();
function expectClickThru(button) {
onClick.mockClear();
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(button));
expect(onClick.mock.calls.length).toBe(1);
}
function expectNoClickThru(button) {
onClick.mockClear();
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(button));
expect(onClick.mock.calls.length).toBe(0);
}
function mounted(button) {
button = ReactTestUtils.renderIntoDocument(button);
return button;
}
beforeEach(function() {
React = require('React');
ReactDOM = require('ReactDOM');
ReactTestUtils = require('ReactTestUtils');
});
it('should forward clicks when it starts out not disabled', function() {
expectClickThru(mounted(<button onClick={onClick} />));
});
it('should not forward clicks when it starts out disabled', function() {
expectNoClickThru(
mounted(<button disabled={true} onClick={onClick} />)
);
});
it('should forward clicks when it becomes not disabled', function() {
var container = document.createElement('div');
var btn = ReactDOM.render(
<button disabled={true} onClick={onClick} />,
container
);
btn = ReactDOM.render(
<button onClick={onClick} />,
container
);
expectClickThru(btn);
});
it('should not forward clicks when it becomes disabled', function() {
var container = document.createElement('div');
var btn = ReactDOM.render(
<button onClick={onClick} />,
container
);
btn = ReactDOM.render(
<button disabled={true} onClick={onClick} />,
container
);
expectNoClickThru(btn);
});
it('should work correctly if the listener is changed', function() {
var container = document.createElement('div');
var btn = ReactDOM.render(
<button disabled={true} onClick={function() {}} />,
container
);
btn = ReactDOM.render(
<button disabled={false} onClick={onClick} />,
container
);
expectClickThru(btn);
});
});
@@ -537,4 +537,21 @@ describe('ReactDOMSelect', function() {
"Cannot set property 'pendingUpdate' of null"
);
});
it('should select grandchild options nested inside an optgroup', function() {
var stub =
<select value="b" onChange={noop}>
<optgroup label="group">
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</optgroup>
</select>;
var container = document.createElement('div');
var node = ReactDOM.render(stub, container);
expect(node.options[0].selected).toBe(false); // a
expect(node.options[1].selected).toBe(true); // b
expect(node.options[2].selected).toBe(false); // c
});
});
@@ -60,6 +60,9 @@ var RESERVED_PROPS = {
suppressContentEditableWarning: null,
};
// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
var DOC_FRAGMENT_TYPE = 11;
function getDeclarationErrorAddendum(internalInstance) {
if (internalInstance) {
@@ -213,7 +216,8 @@ function enqueuePutListener(inst, registrationName, listener, transaction) {
);
}
var containerInfo = inst._nativeContainerInfo;
var doc = containerInfo._ownerDocument;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
if (!doc) {
// Server rendering.
return;
@@ -22,6 +22,7 @@ function ReactDOMContainerInfo(topLevelWrapper, node) {
_ownerDocument: node ?
node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument :
null,
_node: node,
_tag: node ? node.nodeName.toLowerCase() : null,
_namespaceURI: node ? node.namespaceURI : null,
};
@@ -290,7 +290,7 @@ var SVGDOMPropertyConfig = {
DOMAttributeNames: {},
};
Object.keys(ATTRS).map((key) => {
Object.keys(ATTRS).forEach((key) => {
SVGDOMPropertyConfig.Properties[key] = 0;
if (ATTRS[key]) {
SVGDOMPropertyConfig.DOMAttributeNames[key] = ATTRS[key];
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule IOSDefaultEventPluginOrder
* @flow
*/
'use strict';
var IOSDefaultEventPluginOrder = [
'ResponderEventPlugin',
'IOSNativeBridgeEventPlugin',
];
module.exports = IOSDefaultEventPluginOrder;
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule IOSNativeBridgeEventPlugin
* @flow
*/
'use strict';
var EventPropagators = require('EventPropagators');
var SyntheticEvent = require('SyntheticEvent');
var UIManager = require('UIManager');
var merge = require('merge');
var warning = require('warning');
var customBubblingEventTypes = UIManager.customBubblingEventTypes;
var customDirectEventTypes = UIManager.customDirectEventTypes;
var allTypesByEventName = {};
for (var bubblingTypeName in customBubblingEventTypes) {
allTypesByEventName[bubblingTypeName] = customBubblingEventTypes[bubblingTypeName];
}
for (var directTypeName in customDirectEventTypes) {
warning(
!customBubblingEventTypes[directTypeName],
'Event cannot be both direct and bubbling: %s',
directTypeName
);
allTypesByEventName[directTypeName] = customDirectEventTypes[directTypeName];
}
var IOSNativeBridgeEventPlugin = {
eventTypes: merge(customBubblingEventTypes, customDirectEventTypes),
/**
* @see {EventPluginHub.extractEvents}
*/
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget
): ?Object {
var bubbleDispatchConfig = customBubblingEventTypes[topLevelType];
var directDispatchConfig = customDirectEventTypes[topLevelType];
var event = SyntheticEvent.getPooled(
bubbleDispatchConfig || directDispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget
);
if (bubbleDispatchConfig) {
EventPropagators.accumulateTwoPhaseDispatches(event);
} else if (directDispatchConfig) {
EventPropagators.accumulateDirectDispatches(event);
} else {
return null;
}
return event;
},
};
module.exports = IOSNativeBridgeEventPlugin;
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NativeMethodsMixin
* @flow
*/
'use strict';
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
var TextInputState = require('TextInputState');
var UIManager = require('UIManager');
var findNodeHandle = require('findNodeHandle');
var invariant = require('invariant');
type MeasureOnSuccessCallback = (
x: number,
y: number,
width: number,
height: number,
pageX: number,
pageY: number
) => void
type MeasureInWindowOnSuccessCallback = (
x: number,
y: number,
width: number,
height: number,
) => void
type MeasureLayoutOnSuccessCallback = (
left: number,
top: number,
width: number,
height: number
) => void
function warnForStyleProps(props, validAttributes) {
for (var key in validAttributes.style) {
if (!(validAttributes[key] || props[key] === undefined)) {
console.error(
'You are setting the style `{ ' + key + ': ... }` as a prop. You ' +
'should nest it in a style object. ' +
'E.g. `{ style: { ' + key + ': ... } }`'
);
}
}
}
/**
* `NativeMethodsMixin` provides methods to access the underlying native
* component directly. This can be useful in cases when you want to focus
* a view or measure its on-screen dimensions, for example.
*
* The methods described here are available on most of the default components
* provided by React Native. Note, however, that they are *not* available on
* composite components that aren't directly backed by a native view. This will
* generally include most components that you define in your own app. For more
* information, see [Direct
* Manipulation](docs/direct-manipulation.html).
*/
var NativeMethodsMixin = {
/**
* Determines the location on screen, width, and height of the given view and
* returns the values via an async callback. If successful, the callback will
* be called with the following arguments:
*
* - x
* - y
* - width
* - height
* - pageX
* - pageY
*
* Note that these measurements are not available until after the rendering
* has been completed in native. If you need the measurements as soon as
* possible, consider using the [`onLayout`
* prop](docs/view.html#onlayout) instead.
*/
measure: function(callback: MeasureOnSuccessCallback) {
UIManager.measure(
findNodeHandle(this),
mountSafeCallback(this, callback)
);
},
/**
* Determines the location of the given view in the window and returns the
* values via an async callback. If the React root view is embedded in
* another native view, this will give you the absolute coordinates. If
* successful, the callback will be called with the following
* arguments:
*
* - x
* - y
* - width
* - height
*
* Note that these measurements are not available until after the rendering
* has been completed in native.
*/
measureInWindow: function(callback: MeasureInWindowOnSuccessCallback) {
UIManager.measureInWindow(
findNodeHandle(this),
mountSafeCallback(this, callback)
);
},
/**
* Like [`measure()`](#measure), but measures the view relative an ancestor,
* specified as `relativeToNativeNode`. This means that the returned x, y
* are relative to the origin x, y of the ancestor view.
*
* As always, to obtain a native node handle for a component, you can use
* `React.findNodeHandle(component)`.
*/
measureLayout: function(
relativeToNativeNode: number,
onSuccess: MeasureLayoutOnSuccessCallback,
onFail: () => void /* currently unused */
) {
UIManager.measureLayout(
findNodeHandle(this),
relativeToNativeNode,
mountSafeCallback(this, onFail),
mountSafeCallback(this, onSuccess)
);
},
/**
* This function sends props straight to native. They will not participate in
* future diff process - this means that if you do not include them in the
* next render, they will remain active (see [Direct
* Manipulation](docs/direct-manipulation.html)).
*/
setNativeProps: function(nativeProps: Object) {
if (__DEV__) {
warnForStyleProps(nativeProps, this.viewConfig.validAttributes);
}
var updatePayload = ReactNativeAttributePayload.create(
nativeProps,
this.viewConfig.validAttributes
);
UIManager.updateView(
findNodeHandle(this),
this.viewConfig.uiViewClassName,
updatePayload
);
},
/**
* Requests focus for the given input or view. The exact behavior triggered
* will depend on the platform and type of view.
*/
focus: function() {
TextInputState.focusTextInput(findNodeHandle(this));
},
/**
* Removes focus from an input or view. This is the opposite of `focus()`.
*/
blur: function() {
TextInputState.blurTextInput(findNodeHandle(this));
},
};
function throwOnStylesProp(component, props) {
if (props.styles !== undefined) {
var owner = component._owner || null;
var name = component.constructor.displayName;
var msg = '`styles` is not a supported property of `' + name + '`, did ' +
'you mean `style` (singular)?';
if (owner && owner.constructor && owner.constructor.displayName) {
msg += '\n\nCheck the `' + owner.constructor.displayName + '` parent ' +
' component.';
}
throw new Error(msg);
}
}
if (__DEV__) {
// hide this from Flow since we can't define these properties outside of
// __DEV__ without actually implementing them (setting them to undefined
// isn't allowed by ReactClass)
var NativeMethodsMixin_DEV = (NativeMethodsMixin: any);
invariant(
!NativeMethodsMixin_DEV.componentWillMount &&
!NativeMethodsMixin_DEV.componentWillReceiveProps,
'Do not override existing functions.'
);
NativeMethodsMixin_DEV.componentWillMount = function() {
throwOnStylesProp(this, this.props);
};
NativeMethodsMixin_DEV.componentWillReceiveProps = function(newProps) {
throwOnStylesProp(this, newProps);
};
}
/**
* In the future, we should cleanup callbacks by cancelling them instead of
* using this.
*/
var mountSafeCallback = function(context: ReactComponent, callback: ?Function): any {
return function() {
if (!callback || (context.isMounted && !context.isMounted())) {
return undefined;
}
return callback.apply(context, arguments);
};
};
module.exports = NativeMethodsMixin;
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNative
* @flow
*/
'use strict';
// Require ReactNativeDefaultInjection first for its side effects of setting up
// the JS environment
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection');
var ReactElement = require('ReactElement');
var ReactNativeMount = require('ReactNativeMount');
var ReactUpdates = require('ReactUpdates');
var findNodeHandle = require('findNodeHandle');
ReactNativeDefaultInjection.inject();
var render = function(
element: ReactElement,
mountInto: number,
callback?: ?(() => void)
): ?ReactComponent {
return ReactNativeMount.renderComponent(element, mountInto, callback);
};
var ReactNative = {
hasReactNativeInitialized: false,
findNodeHandle: findNodeHandle,
render: render,
unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
/* eslint-enable camelcase */
unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer,
};
// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
ComponentTree: {
getClosestInstanceFromNode: function(node) {
return ReactNativeComponentTree.getClosestInstanceFromNode(node);
},
getNodeFromInstance: function(inst) {
// inst is an internal instance (but could be a composite)
while (inst._renderedComponent) {
inst = inst._renderedComponent;
}
if (inst) {
return ReactNativeComponentTree.getNodeFromInstance(inst);
} else {
return null;
}
},
},
Mount: ReactNativeMount,
Reconciler: require('ReactReconciler'),
});
}
module.exports = ReactNative;
@@ -0,0 +1,546 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeAttributePayload
* @flow
*/
'use strict';
var Platform = require('Platform');
var ReactNativePropRegistry = require('ReactNativePropRegistry');
var deepDiffer = require('deepDiffer');
var flattenStyle = require('flattenStyle');
var emptyObject = {};
/**
* Create a payload that contains all the updates between two sets of props.
*
* These helpers are all encapsulated into a single module, because they use
* mutation as a performance optimization which leads to subtle shared
* dependencies between the code paths. To avoid this mutable state leaking
* across modules, I've kept them isolated to this module.
*/
type AttributeDiffer = (prevProp: mixed, nextProp: mixed) => boolean;
type AttributePreprocessor = (nextProp: mixed) => mixed;
type CustomAttributeConfiguration =
{ diff: AttributeDiffer, process: AttributePreprocessor } |
{ diff: AttributeDiffer } |
{ process: AttributePreprocessor };
type AttributeConfiguration =
{ [key: string]: (
CustomAttributeConfiguration | AttributeConfiguration /*| boolean*/
) };
type NestedNode = Array<NestedNode> | Object | number;
// Tracks removed keys
var removedKeys = null;
var removedKeyCount = 0;
function translateKey(propKey: string) : string {
if (propKey === 'transform') {
// We currently special case the key for `transform`. iOS uses the
// transformMatrix name and Android uses the decomposedMatrix name.
// TODO: We could unify these names and just use the name `transform`
// all the time. Just need to update the native side.
if (Platform.OS === 'android') {
return 'decomposedMatrix';
} else {
return 'transformMatrix';
}
}
return propKey;
}
function defaultDiffer(prevProp: mixed, nextProp: mixed) : boolean {
if (typeof nextProp !== 'object' || nextProp === null) {
// Scalars have already been checked for equality
return true;
} else {
// For objects and arrays, the default diffing algorithm is a deep compare
return deepDiffer(prevProp, nextProp);
}
}
function resolveObject(idOrObject: number | Object) : Object {
if (typeof idOrObject === 'number') {
return ReactNativePropRegistry.getByID(idOrObject);
}
return idOrObject;
}
function restoreDeletedValuesInNestedArray(
updatePayload: Object,
node: NestedNode,
validAttributes: AttributeConfiguration
) {
if (Array.isArray(node)) {
var i = node.length;
while (i-- && removedKeyCount > 0) {
restoreDeletedValuesInNestedArray(
updatePayload,
node[i],
validAttributes
);
}
} else if (node && removedKeyCount > 0) {
var obj = resolveObject(node);
for (var propKey in removedKeys) {
if (!removedKeys[propKey]) {
continue;
}
var nextProp = obj[propKey];
if (nextProp === undefined) {
continue;
}
var attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
if (typeof nextProp === 'function') {
nextProp = true;
}
if (typeof nextProp === 'undefined') {
nextProp = null;
}
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
updatePayload[propKey] = nextProp;
} else if (typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function') {
// case: CustomAttributeConfiguration
var nextValue = typeof attributeConfig.process === 'function' ?
attributeConfig.process(nextProp) :
nextProp;
updatePayload[propKey] = nextValue;
}
removedKeys[propKey] = false;
removedKeyCount--;
}
}
}
function diffNestedArrayProperty(
updatePayload:? Object,
prevArray: Array<NestedNode>,
nextArray: Array<NestedNode>,
validAttributes: AttributeConfiguration
) : ?Object {
var minLength = prevArray.length < nextArray.length ?
prevArray.length :
nextArray.length;
var i;
for (i = 0; i < minLength; i++) {
// Diff any items in the array in the forward direction. Repeated keys
// will be overwritten by later values.
updatePayload = diffNestedProperty(
updatePayload,
prevArray[i],
nextArray[i],
validAttributes
);
}
for (; i < prevArray.length; i++) {
// Clear out all remaining properties.
updatePayload = clearNestedProperty(
updatePayload,
prevArray[i],
validAttributes
);
}
for (; i < nextArray.length; i++) {
// Add all remaining properties.
updatePayload = addNestedProperty(
updatePayload,
nextArray[i],
validAttributes
);
}
return updatePayload;
}
function diffNestedProperty(
updatePayload:? Object,
prevProp: NestedNode,
nextProp: NestedNode,
validAttributes: AttributeConfiguration
) : ?Object {
if (!updatePayload && prevProp === nextProp) {
// If no properties have been added, then we can bail out quickly on object
// equality.
return updatePayload;
}
if (!prevProp || !nextProp) {
if (nextProp) {
return addNestedProperty(
updatePayload,
nextProp,
validAttributes
);
}
if (prevProp) {
return clearNestedProperty(
updatePayload,
prevProp,
validAttributes
);
}
return updatePayload;
}
if (!Array.isArray(prevProp) && !Array.isArray(nextProp)) {
// Both are leaves, we can diff the leaves.
return diffProperties(
updatePayload,
resolveObject(prevProp),
resolveObject(nextProp),
validAttributes
);
}
if (Array.isArray(prevProp) && Array.isArray(nextProp)) {
// Both are arrays, we can diff the arrays.
return diffNestedArrayProperty(
updatePayload,
prevProp,
nextProp,
validAttributes
);
}
if (Array.isArray(prevProp)) {
return diffProperties(
updatePayload,
// $FlowFixMe - We know that this is always an object when the input is.
flattenStyle(prevProp),
// $FlowFixMe - We know that this isn't an array because of above flow.
resolveObject(nextProp),
validAttributes
);
}
return diffProperties(
updatePayload,
resolveObject(prevProp),
// $FlowFixMe - We know that this is always an object when the input is.
flattenStyle(nextProp),
validAttributes
);
}
/**
* addNestedProperty takes a single set of props and valid attribute
* attribute configurations. It processes each prop and adds it to the
* updatePayload.
*/
function addNestedProperty(
updatePayload:? Object,
nextProp: NestedNode,
validAttributes: AttributeConfiguration
) {
if (!nextProp) {
return updatePayload;
}
if (!Array.isArray(nextProp)) {
// Add each property of the leaf.
return addProperties(
updatePayload,
resolveObject(nextProp),
validAttributes
);
}
for (var i = 0; i < nextProp.length; i++) {
// Add all the properties of the array.
updatePayload = addNestedProperty(
updatePayload,
nextProp[i],
validAttributes
);
}
return updatePayload;
}
/**
* clearNestedProperty takes a single set of props and valid attributes. It
* adds a null sentinel to the updatePayload, for each prop key.
*/
function clearNestedProperty(
updatePayload:? Object,
prevProp: NestedNode,
validAttributes: AttributeConfiguration
) : ?Object {
if (!prevProp) {
return updatePayload;
}
if (!Array.isArray(prevProp)) {
// Add each property of the leaf.
return clearProperties(
updatePayload,
resolveObject(prevProp),
validAttributes
);
}
for (var i = 0; i < prevProp.length; i++) {
// Add all the properties of the array.
updatePayload = clearNestedProperty(
updatePayload,
prevProp[i],
validAttributes
);
}
return updatePayload;
}
/**
* diffProperties takes two sets of props and a set of valid attributes
* and write to updatePayload the values that changed or were deleted.
* If no updatePayload is provided, a new one is created and returned if
* anything changed.
*/
function diffProperties(
updatePayload: ?Object,
prevProps: Object,
nextProps: Object,
validAttributes: AttributeConfiguration
): ?Object {
var attributeConfig : ?(CustomAttributeConfiguration | AttributeConfiguration);
var nextProp;
var prevProp;
var altKey;
for (var propKey in nextProps) {
attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
altKey = translateKey(propKey);
if (!validAttributes[altKey]) {
// If there is no config for the alternative, bail out. Helps ART.
altKey = propKey;
}
prevProp = prevProps[propKey];
nextProp = nextProps[propKey];
// functions are converted to booleans as markers that the associated
// events should be sent from native.
if (typeof nextProp === 'function') {
nextProp = (true : any);
// If nextProp is not a function, then don't bother changing prevProp
// since nextProp will win and go into the updatePayload regardless.
if (typeof prevProp === 'function') {
prevProp = (true : any);
}
}
// An explicit value of undefined is treated as a null because it overrides
// any other preceeding value.
if (typeof nextProp === 'undefined') {
nextProp = (null : any);
if (typeof prevProp === 'undefined') {
prevProp = (null : any);
}
}
if (removedKeys) {
removedKeys[propKey] = false;
}
if (updatePayload && updatePayload[altKey] !== undefined) {
// Something else already triggered an update to this key because another
// value diffed. Since we're now later in the nested arrays our value is
// more important so we need to calculate it and override the existing
// value. It doesn't matter if nothing changed, we'll set it anyway.
// Pattern match on: attributeConfig
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
updatePayload[altKey] = nextProp;
} else if (typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function') {
// case: CustomAttributeConfiguration
var nextValue = typeof attributeConfig.process === 'function' ?
attributeConfig.process(nextProp) :
nextProp;
updatePayload[altKey] = nextValue;
}
continue;
}
if (prevProp === nextProp) {
continue; // nothing changed
}
// Pattern match on: attributeConfig
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
if (defaultDiffer(prevProp, nextProp)) {
// a normal leaf has changed
(updatePayload || (updatePayload = {}))[altKey] = nextProp;
}
} else if (typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function') {
// case: CustomAttributeConfiguration
var shouldUpdate = prevProp === undefined || (
typeof attributeConfig.diff === 'function' ?
attributeConfig.diff(prevProp, nextProp) :
defaultDiffer(prevProp, nextProp)
);
if (shouldUpdate) {
nextValue = typeof attributeConfig.process === 'function' ?
attributeConfig.process(nextProp) :
nextProp;
(updatePayload || (updatePayload = {}))[altKey] = nextValue;
}
} else {
// default: fallthrough case when nested properties are defined
removedKeys = null;
removedKeyCount = 0;
updatePayload = diffNestedProperty(
updatePayload,
prevProp,
nextProp,
attributeConfig
);
if (removedKeyCount > 0 && updatePayload) {
restoreDeletedValuesInNestedArray(
updatePayload,
nextProp,
attributeConfig
);
removedKeys = null;
}
}
}
// Also iterate through all the previous props to catch any that have been
// removed and make sure native gets the signal so it can reset them to the
// default.
for (propKey in prevProps) {
if (nextProps[propKey] !== undefined) {
continue; // we've already covered this key in the previous pass
}
attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
altKey = translateKey(propKey);
if (!attributeConfig[altKey]) {
// If there is no config for the alternative, bail out. Helps ART.
altKey = propKey;
}
if (updatePayload && updatePayload[altKey] !== undefined) {
// This was already updated to a diff result earlier.
continue;
}
prevProp = prevProps[propKey];
if (prevProp === undefined) {
continue; // was already empty anyway
}
// Pattern match on: attributeConfig
if (typeof attributeConfig !== 'object' ||
typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function') {
// case: CustomAttributeConfiguration | !Object
// Flag the leaf property for removal by sending a sentinel.
(updatePayload || (updatePayload = {}))[altKey] = null;
if (!removedKeys) {
removedKeys = {};
}
if (!removedKeys[propKey]) {
removedKeys[propKey] = true;
removedKeyCount++;
}
} else {
// default:
// This is a nested attribute configuration where all the properties
// were removed so we need to go through and clear out all of them.
updatePayload = clearNestedProperty(
updatePayload,
prevProp,
attributeConfig
);
}
}
return updatePayload;
}
/**
* addProperties adds all the valid props to the payload after being processed.
*/
function addProperties(
updatePayload: ?Object,
props: Object,
validAttributes: AttributeConfiguration
) : ?Object {
// TODO: Fast path
return diffProperties(updatePayload, emptyObject, props, validAttributes);
}
/**
* clearProperties clears all the previous props by adding a null sentinel
* to the payload for each valid key.
*/
function clearProperties(
updatePayload: ?Object,
prevProps: Object,
validAttributes: AttributeConfiguration
) :? Object {
// TODO: Fast path
return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
}
var ReactNativeAttributePayload = {
create: function(
props: Object,
validAttributes: AttributeConfiguration
) : ?Object {
return addProperties(
null, // updatePayload
props,
validAttributes
);
},
diff: function(
prevProps: Object,
nextProps: Object,
validAttributes: AttributeConfiguration
) : ?Object {
return diffProperties(
null, // updatePayload
prevProps,
nextProps,
validAttributes
);
},
};
module.exports = ReactNativeAttributePayload;
@@ -0,0 +1,230 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeBaseComponent
* @flow
*/
'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin');
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeEventEmitter = require('ReactNativeEventEmitter');
var ReactNativeTagHandles = require('ReactNativeTagHandles');
var ReactMultiChild = require('ReactMultiChild');
var UIManager = require('UIManager');
var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
var registrationNames = ReactNativeEventEmitter.registrationNames;
var putListener = ReactNativeEventEmitter.putListener;
var deleteListener = ReactNativeEventEmitter.deleteListener;
var deleteAllListeners = ReactNativeEventEmitter.deleteAllListeners;
type ReactNativeBaseComponentViewConfig = {
validAttributes: Object;
uiViewClassName: string;
}
// require('UIManagerStatTracker').install(); // uncomment to enable
/**
* @constructor ReactNativeBaseComponent
* @extends ReactComponent
* @extends ReactMultiChild
* @param {!object} UIKit View Configuration.
*/
var ReactNativeBaseComponent = function(
viewConfig: ReactNativeBaseComponentViewConfig
) {
this.viewConfig = viewConfig;
};
/**
* Mixin for containers that contain UIViews. NOTE: markup is rendered markup
* which is a `viewID` ... see the return value for `mountComponent` !
*/
ReactNativeBaseComponent.Mixin = {
getPublicInstance: function() {
// TODO: This should probably use a composite wrapper
return this;
},
unmountComponent: function() {
ReactNativeComponentTree.uncacheNode(this);
deleteAllListeners(this);
this.unmountChildren();
this._rootNodeID = null;
},
/**
* Every native component is responsible for allocating its own `tag`, and
* issuing the native `createView` command. But it is not responsible for
* recording the fact that its own `rootNodeID` is associated with a
* `nodeHandle`. Only the code that actually adds its `nodeHandle` (`tag`) as
* a child of a container can confidently record that in
* `ReactNativeTagHandles`.
*/
initializeChildren: function(children, containerTag, transaction, context) {
var mountImages = this.mountChildren(children, transaction, context);
// In a well balanced tree, half of the nodes are in the bottom row and have
// no children - let's avoid calling out to the native bridge for a large
// portion of the children.
if (mountImages.length) {
// TODO: Pool these per platform view class. Reusing the `mountImages`
// array would likely be a jit deopt.
var createdTags = [];
for (var i = 0, l = mountImages.length; i < l; i++) {
var mountImage = mountImages[i];
var childTag = mountImage;
createdTags[i] = childTag;
}
UIManager.setChildren(containerTag, createdTags);
}
},
/**
* Updates the component's currently mounted representation.
*
* @param {object} nextElement
* @param {ReactReconcileTransaction} transaction
* @param {object} context
* @internal
*/
receiveComponent: function(nextElement, transaction, context) {
var prevElement = this._currentElement;
this._currentElement = nextElement;
if (__DEV__) {
for (var key in this.viewConfig.validAttributes) {
if (nextElement.props.hasOwnProperty(key)) {
deepFreezeAndThrowOnMutationInDev(nextElement.props[key]);
}
}
}
var updatePayload = ReactNativeAttributePayload.diff(
prevElement.props,
nextElement.props,
this.viewConfig.validAttributes
);
if (updatePayload) {
UIManager.updateView(
this._rootNodeID,
this.viewConfig.uiViewClassName,
updatePayload
);
}
this._reconcileListenersUponUpdate(
prevElement.props,
nextElement.props
);
this.updateChildren(nextElement.props.children, transaction, context);
},
/**
* @param {object} initialProps Native component props.
*/
_registerListenersUponCreation: function(initialProps) {
for (var key in initialProps) {
// NOTE: The check for `!props[key]`, is only possible because this method
// registers listeners the *first* time a component is created.
if (registrationNames[key] && initialProps[key]) {
var listener = initialProps[key];
putListener(this, key, listener);
}
}
},
/**
* Reconciles event listeners, adding or removing if necessary.
* @param {object} prevProps Native component props including events.
* @param {object} nextProps Next native component props including events.
*/
_reconcileListenersUponUpdate: function(prevProps, nextProps) {
for (var key in nextProps) {
if (registrationNames[key] && (nextProps[key] !== prevProps[key])) {
if (nextProps[key]) {
putListener(this, key, nextProps[key]);
} else {
deleteListener(this, key);
}
}
}
},
/**
* Currently this still uses IDs for reconciliation so this can return null.
*
* @return {null} Null.
*/
getNativeNode: function() {
return this._rootNodeID;
},
/**
* @param {string} rootID Root ID of this subtree.
* @param {Transaction} transaction For creating/updating.
* @return {string} Unique iOS view tag.
*/
mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) {
var tag = ReactNativeTagHandles.allocateTag();
this._rootNodeID = tag;
this._nativeParent = nativeParent;
this._nativeContainerInfo = nativeContainerInfo;
if (__DEV__) {
for (var key in this.viewConfig.validAttributes) {
if (this._currentElement.props.hasOwnProperty(key)) {
deepFreezeAndThrowOnMutationInDev(this._currentElement.props[key]);
}
}
}
var updatePayload = ReactNativeAttributePayload.create(
this._currentElement.props,
this.viewConfig.validAttributes
);
var nativeTopRootTag = nativeContainerInfo._tag;
UIManager.createView(
tag,
this.viewConfig.uiViewClassName,
nativeTopRootTag,
updatePayload
);
ReactNativeComponentTree.precacheNode(this, tag);
this._registerListenersUponCreation(this._currentElement.props);
this.initializeChildren(
this._currentElement.props.children,
tag,
transaction,
context
);
return tag;
},
};
/**
* Order of mixins is important. ReactNativeBaseComponent overrides methods in
* ReactMultiChild.
*/
Object.assign(
ReactNativeBaseComponent.prototype,
ReactMultiChild.Mixin,
ReactNativeBaseComponent.Mixin,
NativeMethodsMixin
);
module.exports = ReactNativeBaseComponent;
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeComponentEnvironment
* @flow
*/
'use strict';
var ReactNativeDOMIDOperations = require('ReactNativeDOMIDOperations');
var ReactNativeReconcileTransaction = require('ReactNativeReconcileTransaction');
var ReactNativeComponentEnvironment = {
processChildrenUpdates: ReactNativeDOMIDOperations.dangerouslyProcessChildrenUpdates,
replaceNodeWithMarkup: ReactNativeDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID,
/**
* Nothing to do for UIKit bridge.
*
* @private
*/
unmountIDFromEnvironment: function(/*rootNodeID*/) {
},
/**
* @param {DOMElement} Element to clear.
*/
clearNode: function(/*containerView*/) {
},
ReactReconcileTransaction: ReactNativeReconcileTransaction,
};
module.exports = ReactNativeComponentEnvironment;
@@ -0,0 +1,66 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeComponentTree
*/
'use strict';
var invariant = require('invariant');
var instanceCache = {};
/**
* Drill down (through composites and empty components) until we get a native or
* native text component.
*
* This is pretty polymorphic but unavoidable with the current structure we have
* for `_renderedChildren`.
*/
function getRenderedNativeOrTextFromComponent(component) {
var rendered;
while ((rendered = component._renderedComponent)) {
component = rendered;
}
return component;
}
/**
* Populate `_nativeNode` on the rendered native/text component with the given
* DOM node. The passed `inst` can be a composite.
*/
function precacheNode(inst, tag) {
var nativeInst = getRenderedNativeOrTextFromComponent(inst);
instanceCache[tag] = nativeInst;
}
function uncacheNode(inst) {
var tag = inst._rootNodeID;
if (tag) {
delete instanceCache[tag];
}
}
function getInstanceFromTag(tag) {
return instanceCache[tag] || null;
}
function getTagFromInstance(inst) {
invariant(inst._rootNodeID, 'All native instances should have a tag.');
return inst._rootNodeID;
}
var ReactNativeComponentTree = {
getClosestInstanceFromNode: getInstanceFromTag,
getInstanceFromNode: getInstanceFromTag,
getNodeFromInstance: getTagFromInstance,
precacheNode: precacheNode,
uncacheNode: uncacheNode,
};
module.exports = ReactNativeComponentTree;
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeContainerInfo
* @flow
*/
'use strict';
function ReactNativeContainerInfo(tag) {
var info = {
_tag: tag,
};
return info;
}
module.exports = ReactNativeContainerInfo;
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeDOMIDOperations
* @flow
*/
'use strict';
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
var ReactPerf = require('ReactPerf');
var UIManager = require('UIManager');
/**
* Updates a component's children by processing a series of updates.
* For each of the update/create commands, the `fromIndex` refers to the index
* that the item existed at *before* any of the updates are applied, and the
* `toIndex` refers to the index after *all* of the updates are applied
* (including deletes/moves). TODO: refactor so this can be shared with
* DOMChildrenOperations.
*
* @param {ReactNativeBaseComponent} updates List of update configurations.
* @param {array<string>} markup List of markup strings - in the case of React
* IOS, the ids of new components assumed to be already created.
*/
var dangerouslyProcessChildrenUpdates = function(inst, childrenUpdates) {
if (!childrenUpdates.length) {
return;
}
var containerTag = ReactNativeComponentTree.getNodeFromInstance(inst);
var moveFromIndices;
var moveToIndices;
var addChildTags;
var addAtIndices;
var removeAtIndices;
for (var i = 0; i < childrenUpdates.length; i++) {
var update = childrenUpdates[i];
if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING) {
(moveFromIndices || (moveFromIndices = [])).push(update.fromIndex);
(moveToIndices || (moveToIndices = [])).push(update.toIndex);
} else if (update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
(removeAtIndices || (removeAtIndices = [])).push(update.fromIndex);
} else if (update.type === ReactMultiChildUpdateTypes.INSERT_MARKUP) {
var mountImage = update.content;
var tag = mountImage;
(addAtIndices || (addAtIndices = [])).push(update.toIndex);
(addChildTags || (addChildTags = [])).push(tag);
}
}
UIManager.manageChildren(
containerTag,
moveFromIndices,
moveToIndices,
addChildTags,
addAtIndices,
removeAtIndices
);
};
/**
* Operations used to process updates to DOM nodes. This is made injectable via
* `ReactComponent.DOMIDOperations`.
*/
var ReactNativeDOMIDOperations = {
dangerouslyProcessChildrenUpdates: ReactPerf.measure(
// FIXME(frantic): #4441289 Hack to avoid modifying react-tools
'ReactDOMIDOperations',
'dangerouslyProcessChildrenUpdates',
dangerouslyProcessChildrenUpdates
),
/**
* Replaces a view that exists in the document with markup.
*
* @param {string} id ID of child to be replaced.
* @param {string} markup Mount image to replace child with id.
*/
dangerouslyReplaceNodeWithMarkupByID: ReactPerf.measure(
'ReactDOMIDOperations',
'dangerouslyReplaceNodeWithMarkupByID',
function(id, mountImage) {
var oldTag = id;
UIManager.replaceExistingNonRootView(oldTag, mountImage);
}
),
};
module.exports = ReactNativeDOMIDOperations;
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeDefaultInjection
* @flow
*/
'use strict';
/**
* Make sure essential globals are available and are patched correctly. Please don't remove this
* line. Bundles created by react-packager `require` it before executing any application code. This
* ensures it exists in the dependency graph and can be `require`d.
*/
require('InitializeJavaScriptAppEngine');
var EventPluginHub = require('EventPluginHub');
var EventPluginUtils = require('EventPluginUtils');
var IOSDefaultEventPluginOrder = require('IOSDefaultEventPluginOrder');
var IOSNativeBridgeEventPlugin = require('IOSNativeBridgeEventPlugin');
var ReactElement = require('ReactElement');
var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment');
var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler');
var ReactNativeTextComponent = require('ReactNativeTextComponent');
var ReactNativeTreeTraversal = require('ReactNativeTreeTraversal');
var ReactNativeComponent = require('ReactNativeComponent');
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactSimpleEmptyComponent = require('ReactSimpleEmptyComponent');
var ReactUpdates = require('ReactUpdates');
var ResponderEventPlugin = require('ResponderEventPlugin');
var invariant = require('invariant');
// Just to ensure this gets packaged, since its only caller is from Native.
require('RCTEventEmitter');
require('RCTLog');
require('JSTimersExecution');
function inject() {
/**
* Inject module for resolving DOM hierarchy and plugin ordering.
*/
EventPluginHub.injection.injectEventPluginOrder(IOSDefaultEventPluginOrder);
EventPluginUtils.injection.injectComponentTree(ReactNativeComponentTree);
EventPluginUtils.injection.injectTreeTraversal(ReactNativeTreeTraversal);
ResponderEventPlugin.injection.injectGlobalResponderHandler(
ReactNativeGlobalResponderHandler
);
/**
* Some important event plugins included by default (without having to require
* them).
*/
EventPluginHub.injection.injectEventPluginsByName({
'ResponderEventPlugin': ResponderEventPlugin,
'IOSNativeBridgeEventPlugin': IOSNativeBridgeEventPlugin,
});
ReactUpdates.injection.injectReconcileTransaction(
ReactNativeComponentEnvironment.ReactReconcileTransaction
);
ReactUpdates.injection.injectBatchingStrategy(
ReactDefaultBatchingStrategy
);
ReactComponentEnvironment.injection.injectEnvironment(
ReactNativeComponentEnvironment
);
var EmptyComponent = (instantiate) => {
// Can't import View at the top because it depends on React to make its composite
var View = require('View');
return new ReactSimpleEmptyComponent(
ReactElement.createElement(View, {
collapsable: true,
style: { position: 'absolute' },
}),
instantiate
);
};
ReactEmptyComponent.injection.injectEmptyComponentFactory(EmptyComponent);
ReactNativeComponent.injection.injectTextComponentClass(
ReactNativeTextComponent
);
ReactNativeComponent.injection.injectGenericComponentClass(function(tag) {
// Show a nicer error message for non-function tags
var info = '';
if (typeof tag === 'string' && /^[a-z]/.test(tag)) {
info += ' Each component name should start with an uppercase letter.';
}
invariant(false, 'Expected a component class, got %s.%s', tag, info);
});
}
module.exports = {
inject: inject,
};
@@ -0,0 +1,223 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeEventEmitter
* @flow
*/
'use strict';
var EventPluginHub = require('EventPluginHub');
var EventPluginRegistry = require('EventPluginRegistry');
var ReactEventEmitterMixin = require('ReactEventEmitterMixin');
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeTagHandles = require('ReactNativeTagHandles');
var ReactUpdates = require('ReactUpdates');
var EventConstants = require('EventConstants');
var merge = require('merge');
var warning = require('warning');
var topLevelTypes = EventConstants.topLevelTypes;
/**
* Version of `ReactBrowserEventEmitter` that works on the receiving side of a
* serialized worker boundary.
*/
// Shared default empty native event - conserve memory.
var EMPTY_NATIVE_EVENT = {};
/**
* Selects a subsequence of `Touch`es, without destroying `touches`.
*
* @param {Array<Touch>} touches Deserialized touch objects.
* @param {Array<number>} indices Indices by which to pull subsequence.
* @return {Array<Touch>} Subsequence of touch objects.
*/
var touchSubsequence = function(touches, indices) {
var ret = [];
for (var i = 0; i < indices.length; i++) {
ret.push(touches[indices[i]]);
}
return ret;
};
/**
* TODO: Pool all of this.
*
* Destroys `touches` by removing touch objects at indices `indices`. This is
* to maintain compatibility with W3C touch "end" events, where the active
* touches don't include the set that has just been "ended".
*
* @param {Array<Touch>} touches Deserialized touch objects.
* @param {Array<number>} indices Indices to remove from `touches`.
* @return {Array<Touch>} Subsequence of removed touch objects.
*/
var removeTouchesAtIndices = function(
touches: Array<Object>,
indices: Array<number>
): Array<Object> {
var rippedOut = [];
// use an unsafe downcast to alias to nullable elements,
// so we can delete and then compact.
var temp: Array<?Object> = (touches: Array<any>);
for (var i = 0; i < indices.length; i++) {
var index = indices[i];
rippedOut.push(touches[index]);
temp[index] = null;
}
var fillAt = 0;
for (var j = 0; j < temp.length; j++) {
var cur = temp[j];
if (cur !== null) {
temp[fillAt++] = cur;
}
}
temp.length = fillAt;
return rippedOut;
};
/**
* `ReactNativeEventEmitter` is used to attach top-level event listeners. For example:
*
* ReactNativeEventEmitter.putListener('myID', 'onClick', myFunction);
*
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
*
* @internal
*/
var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, {
registrationNames: EventPluginRegistry.registrationNameModules,
putListener: EventPluginHub.putListener,
getListener: EventPluginHub.getListener,
deleteListener: EventPluginHub.deleteListener,
deleteAllListeners: EventPluginHub.deleteAllListeners,
/**
* Internal version of `receiveEvent` in terms of normalized (non-tag)
* `rootNodeID`.
*
* @see receiveEvent.
*
* @param {rootNodeID} rootNodeID React root node ID that event occurred on.
* @param {TopLevelType} topLevelType Top level type of event.
* @param {object} nativeEventParam Object passed from native.
*/
_receiveRootNodeIDEvent: function(
rootNodeID: number,
topLevelType: string,
nativeEventParam: Object
) {
var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID);
if (!inst) {
// If the original instance is already gone, we don't have to dispatch
// any events.
return;
}
ReactUpdates.batchedUpdates(function() {
ReactNativeEventEmitter.handleTopLevel(
topLevelType,
inst,
nativeEvent,
nativeEvent.target
);
});
},
/**
* Publicly exposed method on module for native objc to invoke when a top
* level event is extracted.
* @param {rootNodeID} rootNodeID React root node ID that event occurred on.
* @param {TopLevelType} topLevelType Top level type of event.
* @param {object} nativeEventParam Object passed from native.
*/
receiveEvent: function(
tag: number,
topLevelType: string,
nativeEventParam: Object
) {
var rootNodeID = tag;
ReactNativeEventEmitter._receiveRootNodeIDEvent(
rootNodeID,
topLevelType,
nativeEventParam
);
},
/**
* Simple multi-wrapper around `receiveEvent` that is intended to receive an
* efficient representation of `Touch` objects, and other information that
* can be used to construct W3C compliant `Event` and `Touch` lists.
*
* This may create dispatch behavior that differs than web touch handling. We
* loop through each of the changed touches and receive it as a single event.
* So two `touchStart`/`touchMove`s that occur simultaneously are received as
* two separate touch event dispatches - when they arguably should be one.
*
* This implementation reuses the `Touch` objects themselves as the `Event`s
* since we dispatch an event for each touch (though that might not be spec
* compliant). The main purpose of reusing them is to save allocations.
*
* TODO: Dispatch multiple changed touches in one event. The bubble path
* could be the first common ancestor of all the `changedTouches`.
*
* One difference between this behavior and W3C spec: cancelled touches will
* not appear in `.touches`, or in any future `.touches`, though they may
* still be "actively touching the surface".
*
* Web desktop polyfills only need to construct a fake touch event with
* identifier 0, also abandoning traditional click handlers.
*/
receiveTouches: function(
eventTopLevelType: string,
touches: Array<Object>,
changedIndices: Array<number>
) {
var changedTouches =
eventTopLevelType === topLevelTypes.topTouchEnd ||
eventTopLevelType === topLevelTypes.topTouchCancel ?
removeTouchesAtIndices(touches, changedIndices) :
touchSubsequence(touches, changedIndices);
for (var jj = 0; jj < changedTouches.length; jj++) {
var touch = changedTouches[jj];
// Touch objects can fulfill the role of `DOM` `Event` objects if we set
// the `changedTouches`/`touches`. This saves allocations.
touch.changedTouches = changedTouches;
touch.touches = touches;
var nativeEvent = touch;
var rootNodeID = null;
var target = nativeEvent.target;
if (target !== null && target !== undefined) {
if (target < ReactNativeTagHandles.tagsStartAt) {
if (__DEV__) {
warning(
false,
'A view is reporting that a touch occured on tag zero.'
);
}
} else {
rootNodeID = target;
}
}
ReactNativeEventEmitter._receiveRootNodeIDEvent(
rootNodeID,
eventTopLevelType,
nativeEvent
);
}
},
});
module.exports = ReactNativeEventEmitter;
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeGlobalResponderHandler
*/
'use strict';
var UIManager = require('UIManager');
var ReactNativeGlobalResponderHandler = {
onChange: function(from, to, blockNativeResponder) {
if (to !== null) {
UIManager.setJSResponder(
to._rootNodeID,
blockNativeResponder
);
} else {
UIManager.clearJSResponder();
}
},
};
module.exports = ReactNativeGlobalResponderHandler;
@@ -0,0 +1,238 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeMount
* @flow
*/
'use strict';
var ReactElement = require('ReactElement');
var ReactNativeContainerInfo = require('ReactNativeContainerInfo');
var ReactNativeTagHandles = require('ReactNativeTagHandles');
var ReactPerf = require('ReactPerf');
var ReactReconciler = require('ReactReconciler');
var ReactUpdateQueue = require('ReactUpdateQueue');
var ReactUpdates = require('ReactUpdates');
var UIManager = require('UIManager');
var emptyObject = require('emptyObject');
var instantiateReactComponent = require('instantiateReactComponent');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
/**
* Temporary (?) hack so that we can store all top-level pending updates on
* composites instead of having to worry about different types of components
* here.
*/
var TopLevelWrapper = function() {};
TopLevelWrapper.prototype.isReactComponent = {};
if (__DEV__) {
TopLevelWrapper.displayName = 'TopLevelWrapper';
}
TopLevelWrapper.prototype.render = function() {
// this.props is actually a ReactElement
return this.props;
};
/**
* Mounts this component and inserts it into the DOM.
*
* @param {ReactComponent} componentInstance The instance to mount.
* @param {number} rootID ID of the root node.
* @param {number} containerTag container element to mount into.
* @param {ReactReconcileTransaction} transaction
*/
function mountComponentIntoNode(
componentInstance,
containerTag,
transaction) {
var markup = ReactReconciler.mountComponent(
componentInstance,
transaction,
null,
ReactNativeContainerInfo(containerTag),
emptyObject
);
componentInstance._renderedComponent._topLevelWrapper = componentInstance;
ReactNativeMount._mountImageIntoNode(markup, containerTag);
}
/**
* Batched mount.
*
* @param {ReactComponent} componentInstance The instance to mount.
* @param {number} rootID ID of the root node.
* @param {number} containerTag container element to mount into.
*/
function batchedMountComponentIntoNode(
componentInstance,
containerTag) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(
mountComponentIntoNode,
null,
componentInstance,
containerTag,
transaction
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
}
/**
* As soon as `ReactMount` is refactored to not rely on the DOM, we can share
* code between the two. For now, we'll hard code the ID logic.
*/
var ReactNativeMount = {
_instancesByContainerID: {},
// these two functions are needed by React Devtools
findNodeHandle: require('findNodeHandle'),
/**
* @param {ReactComponent} instance Instance to render.
* @param {containerTag} containerView Handle to native view tag
*/
renderComponent: function(
nextElement: ReactElement,
containerTag: number,
callback?: ?(() => void)
): ?ReactComponent {
var nextWrappedElement = new ReactElement(
TopLevelWrapper,
null,
null,
null,
null,
null,
nextElement
);
var topRootNodeID = containerTag;
var prevComponent = ReactNativeMount._instancesByContainerID[topRootNodeID];
if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props;
if (shouldUpdateReactComponent(prevElement, nextElement)) {
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
}
return prevComponent;
} else {
ReactNativeMount.unmountComponentAtNode(containerTag);
}
}
if (!ReactNativeTagHandles.reactTagIsNativeTopRootID(containerTag)) {
console.error('You cannot render into anything but a top root');
return null;
}
ReactNativeTagHandles.assertRootTag(containerTag);
var instance = instantiateReactComponent(nextWrappedElement);
ReactNativeMount._instancesByContainerID[containerTag] = instance;
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
// according to the current batching strategy.
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
instance,
containerTag
);
var component = instance.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
},
/**
* @param {View} view View tree image.
* @param {number} containerViewID View to insert sub-view into.
*/
_mountImageIntoNode: ReactPerf.measure(
// FIXME(frantic): #4441289 Hack to avoid modifying react-tools
'ReactComponentBrowserEnvironment',
'mountImageIntoNode',
function(mountImage, containerID) {
// Since we now know that the `mountImage` has been mounted, we can
// mark it as such.
var childTag = mountImage;
UIManager.setChildren(
containerID,
[childTag]
);
}
),
/**
* Standard unmounting of the component that is rendered into `containerID`,
* but will also execute a command to remove the actual container view
* itself. This is useful when a client is cleaning up a React tree, and also
* knows that the container will no longer be needed. When executing
* asynchronously, it's easier to just have this method be the one that calls
* for removal of the view.
*/
unmountComponentAtNodeAndRemoveContainer: function(
containerTag: number
) {
ReactNativeMount.unmountComponentAtNode(containerTag);
// call back into native to remove all of the subviews from this container
UIManager.removeRootView(containerTag);
},
/**
* Unmount component at container ID by iterating through each child component
* that has been rendered and unmounting it. There should just be one child
* component at this time.
*/
unmountComponentAtNode: function(containerTag: number): boolean {
if (!ReactNativeTagHandles.reactTagIsNativeTopRootID(containerTag)) {
console.error('You cannot render into anything but a top root');
return false;
}
var instance = ReactNativeMount._instancesByContainerID[containerTag];
if (!instance) {
return false;
}
ReactNativeMount.unmountComponentFromNode(instance, containerTag);
delete ReactNativeMount._instancesByContainerID[containerTag];
return true;
},
/**
* Unmounts a component and sends messages back to iOS to remove its subviews.
*
* @param {ReactComponent} instance React component instance.
* @param {string} containerID ID of container we're removing from.
* @final
* @internal
* @see {ReactNativeMount.unmountComponentAtNode}
*/
unmountComponentFromNode: function(
instance: ReactComponent,
containerID: string
) {
// Call back into native to remove all of the subviews from this container
ReactReconciler.unmountComponent(instance);
UIManager.removeSubviewsFromContainerWithID(containerID);
},
};
ReactNativeMount.renderComponent = ReactPerf.measure(
'ReactMount',
'_renderNewRootComponent',
ReactNativeMount.renderComponent
);
module.exports = ReactNativeMount;
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativePropRegistry
* @flow
*/
'use strict';
var objects = {};
var uniqueID = 1;
var emptyObject = {};
class ReactNativePropRegistry {
static register(object: Object): number {
var id = ++uniqueID;
if (__DEV__) {
Object.freeze(object);
}
objects[id] = object;
return id;
}
static getByID(id: number): Object {
if (!id) {
// Used in the style={[condition && id]} pattern,
// we want it to be a no-op when the value is false or null
return emptyObject;
}
var object = objects[id];
if (!object) {
console.warn('Invalid style with id `' + id + '`. Skipping ...');
return emptyObject;
}
return object;
}
}
module.exports = ReactNativePropRegistry;
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeReconcileTransaction
* @flow
*/
'use strict';
var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var Transaction = require('Transaction');
/**
* Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during
* the performing of the transaction.
*/
var ON_DOM_READY_QUEUEING = {
/**
* Initializes the internal `onDOMReady` queue.
*/
initialize: function() {
this.reactMountReady.reset();
},
/**
* After DOM is flushed, invoke all registered `onDOMReady` callbacks.
*/
close: function() {
this.reactMountReady.notifyAll();
},
};
/**
* Executed within the scope of the `Transaction` instance. Consider these as
* being member methods, but with an implied ordering while being isolated from
* each other.
*/
var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
/**
* Currently:
* - The order that these are listed in the transaction is critical:
* - Suppresses events.
* - Restores selection range.
*
* Future:
* - Restore document/overflow scroll positions that were unintentionally
* modified via DOM insertions above the top viewport boundary.
* - Implement/integrate with customized constraint based layout system and keep
* track of which dimensions must be remeasured.
*
* @class ReactNativeReconcileTransaction
*/
function ReactNativeReconcileTransaction() {
this.reinitializeTransaction();
this.reactMountReady = CallbackQueue.getPooled(null);
}
var Mixin = {
/**
* @see Transaction
* @abstract
* @final
* @return {array<object>} List of operation wrap procedures.
* TODO: convert to array<TransactionWrapper>
*/
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
/**
* @return {object} The queue to collect `onDOMReady` callbacks with.
* TODO: convert to ReactMountReady
*/
getReactMountReady: function() {
return this.reactMountReady;
},
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
*/
destructor: function() {
CallbackQueue.release(this.reactMountReady);
this.reactMountReady = null;
},
};
Object.assign(
ReactNativeReconcileTransaction.prototype,
Transaction.Mixin,
ReactNativeReconcileTransaction,
Mixin
);
PooledClass.addPoolingTo(ReactNativeReconcileTransaction);
module.exports = ReactNativeReconcileTransaction;
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeTagHandles
* @flow
*/
'use strict';
var invariant = require('invariant');
/**
* Keeps track of allocating and associating native "tags" which are numeric,
* unique view IDs. All the native tags are negative numbers, to avoid
* collisions, but in the JS we keep track of them as positive integers to store
* them effectively in Arrays. So we must refer to them as "inverses" of the
* native tags (that are * normally negative).
*
* It *must* be the case that every `rootNodeID` always maps to the exact same
* `tag` forever. The easiest way to accomplish this is to never delete
* anything from this table.
* Why: Because `dangerouslyReplaceNodeWithMarkupByID` relies on being able to
* unmount a component with a `rootNodeID`, then mount a new one in its place,
*/
var INITIAL_TAG_COUNT = 1;
var ReactNativeTagHandles = {
tagsStartAt: INITIAL_TAG_COUNT,
tagCount: INITIAL_TAG_COUNT,
allocateTag: function(): number {
// Skip over root IDs as those are reserved for native
while (this.reactTagIsNativeTopRootID(ReactNativeTagHandles.tagCount)) {
ReactNativeTagHandles.tagCount++;
}
var tag = ReactNativeTagHandles.tagCount;
ReactNativeTagHandles.tagCount++;
return tag;
},
assertRootTag: function(tag: number): void {
invariant(
this.reactTagIsNativeTopRootID(tag),
'Expect a native root tag, instead got %s', tag
);
},
reactTagIsNativeTopRootID: function(reactTag: number): bool {
// We reserve all tags that are 1 mod 10 for native root views
return reactTag % 10 === 1;
},
};
module.exports = ReactNativeTagHandles;
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeTextComponent
*/
'use strict';
var ReactNativeComponentTree = require('ReactNativeComponentTree');
var ReactNativeTagHandles = require('ReactNativeTagHandles');
var UIManager = require('UIManager');
var invariant = require('invariant');
var ReactNativeTextComponent = function(text) {
// This is really a ReactText (ReactNode), not a ReactElement
this._currentElement = text;
this._stringText = '' + text;
this._nativeParent = null;
this._rootNodeID = null;
};
Object.assign(ReactNativeTextComponent.prototype, {
mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) {
// TODO: nativeParent should have this context already. Stop abusing context.
invariant(
context.isInAParentText,
'RawText "%s" must be wrapped in an explicit <Text> component.',
this._stringText
);
this._nativeParent = nativeParent;
var tag = ReactNativeTagHandles.allocateTag();
this._rootNodeID = tag;
var nativeTopRootTag = nativeContainerInfo._tag;
UIManager.createView(
tag,
'RCTRawText',
nativeTopRootTag,
{text: this._stringText}
);
ReactNativeComponentTree.precacheNode(this, tag);
return tag;
},
getNativeNode: function() {
return this._rootNodeID;
},
receiveComponent: function(nextText, transaction, context) {
if (nextText !== this._currentElement) {
this._currentElement = nextText;
var nextStringText = '' + nextText;
if (nextStringText !== this._stringText) {
this._stringText = nextStringText;
UIManager.updateView(
this._rootNodeID,
'RCTRawText',
{text: this._stringText}
);
}
}
},
unmountComponent: function() {
ReactNativeComponentTree.uncacheNode(this);
this._currentElement = null;
this._stringText = null;
this._rootNodeID = null;
},
});
module.exports = ReactNativeTextComponent;
@@ -0,0 +1,126 @@
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeTreeTraversal
*/
'use strict';
// Same as ReactDOMTreeTraversal without the invariants.
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
function getLowestCommonAncestor(instA, instB) {
var depthA = 0;
for (var tempA = instA; tempA; tempA = tempA._nativeParent) {
depthA++;
}
var depthB = 0;
for (var tempB = instB; tempB; tempB = tempB._nativeParent) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = instA._nativeParent;
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = instB._nativeParent;
depthB--;
}
// Walk in lockstep until we find a match.
var depth = depthA;
while (depth--) {
if (instA === instB) {
return instA;
}
instA = instA._nativeParent;
instB = instB._nativeParent;
}
return null;
}
/**
* Return if A is an ancestor of B.
*/
function isAncestor(instA, instB) {
while (instB) {
if (instB === instA) {
return true;
}
instB = instB._nativeParent;
}
return false;
}
/**
* Return the parent instance of the passed-in instance.
*/
function getParentInstance(inst) {
return inst._nativeParent;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = inst._nativeParent;
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], false, arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], true, arg);
}
}
/**
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
* should would receive a `mouseEnter` or `mouseLeave` event.
*
* Does not invoke the callback on the nearest common ancestor because nothing
* "entered" or "left" that element.
*/
function traverseEnterLeave(from, to, fn, argFrom, argTo) {
var common = from && to ? getLowestCommonAncestor(from, to) : null;
var pathFrom = [];
while (from && from !== common) {
pathFrom.push(from);
from = from._nativeParent;
}
var pathTo = [];
while (to && to !== common) {
pathTo.push(to);
to = to._nativeParent;
}
var i;
for (i = 0; i < pathFrom.length; i++) {
fn(pathFrom[i], true, argFrom);
}
for (i = pathTo.length; i-- > 0;) {
fn(pathTo[i], false, argTo);
}
}
module.exports = {
isAncestor: isAncestor,
getLowestCommonAncestor: getLowestCommonAncestor,
getParentInstance: getParentInstance,
traverseTwoPhase: traverseTwoPhase,
traverseEnterLeave: traverseEnterLeave,
};
@@ -0,0 +1,14 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Noop
// TODO: Move all initialization callers back into react-native
@@ -0,0 +1,16 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// TODO: Figure out a way to drop this dependency
var InteractionManager = {};
module.exports = InteractionManager;
@@ -0,0 +1,14 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Noop
// TODO: Move all initialization callers back into react-native
@@ -0,0 +1,16 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Mock of the Native Hooks
var Platform = {};
module.exports = Platform;
@@ -0,0 +1,14 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Noop
// TODO: Move all initialization callers back into react-native
@@ -0,0 +1,14 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Noop
// TODO: Move all initialization callers back into react-native
@@ -0,0 +1,17 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Mock of the Native Hooks
// TODO: Should this move into the components themselves? E.g. focusable
var TextInputState = {};
module.exports = TextInputState;
@@ -0,0 +1,21 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// Mock of the Native Hooks
var RCTUIManager = {
createView: jest.genMockFunction(),
setChildren: jest.genMockFunction(),
manageChildren: jest.genMockFunction(),
updateView: jest.genMockFunction(),
};
module.exports = RCTUIManager;
@@ -0,0 +1,63 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// TODO: Move deepDiffer into react
var deepDiffer = function(one: any, two: any): bool {
if (one === two) {
// Short circuit on identical object references instead of traversing them.
return false;
}
if ((typeof one === 'function') && (typeof two === 'function')) {
// We consider all functions equal
return false;
}
if ((typeof one !== 'object') || (one === null)) {
// Primitives can be directly compared
return one !== two;
}
if ((typeof two !== 'object') || (two === null)) {
// We know they are different because the previous case would have triggered
// otherwise.
return true;
}
if (one.constructor !== two.constructor) {
return true;
}
if (Array.isArray(one)) {
// We know two is also an array because the constructors are equal
var len = one.length;
if (two.length !== len) {
return true;
}
for (var ii = 0; ii < len; ii++) {
if (deepDiffer(one[ii], two[ii])) {
return true;
}
}
} else {
for (var key in one) {
if (deepDiffer(one[key], two[key])) {
return true;
}
}
for (var twoKey in two) {
// The only case we haven't checked yet is keys that are in two but aren't
// in one, which means they are different.
if (one[twoKey] === undefined && two[twoKey] !== undefined) {
return true;
}
}
}
return false;
};
module.exports = deepDiffer;
@@ -0,0 +1,16 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// TODO: move into react or fbjs
var deepFreezeAndThrowOnMutationInDev = function() { };
module.exports = deepFreezeAndThrowOnMutationInDev;
@@ -0,0 +1,16 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// TODO: Move flattenStyle into react
var flattenStyle = function() { };
module.exports = flattenStyle;
@@ -0,0 +1,18 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
// TODO: Replace all callers with spread
var merge = function(a, b) {
return {...a, ...b};
};
module.exports = merge;
@@ -0,0 +1,246 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
'use strict';
jest.dontMock('ReactNativeAttributePayload');
jest.dontMock('ReactNativePropRegistry');
// jest.dontMock('deepDiffer');
// jest.dontMock('flattenStyle');
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
var ReactNativePropRegistry = require('ReactNativePropRegistry');
var diff = ReactNativeAttributePayload.diff;
describe('ReactNativeAttributePayload', function() {
it('should work with simple example', () => {
expect(diff(
{a: 1, c: 3},
{b: 2, c: 3},
{a: true, b: true}
)).toEqual({a: null, b: 2});
});
it('should skip fields that are equal', () => {
expect(diff(
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
{a: true, b: true, c: true, d: true, e: true, f: true}
)).toEqual(null);
});
it('should remove fields', () => {
expect(diff(
{a: 1},
{},
{a: true}
)).toEqual({a: null});
});
it('should remove fields that are set to undefined', () => {
expect(diff(
{a: 1},
{a: undefined},
{a: true}
)).toEqual({a: null});
});
it('should ignore invalid fields', () => {
expect(diff(
{a: 1},
{b: 2},
{}
)).toEqual(null);
});
it('should use the diff attribute', () => {
var diffA = jest.genMockFunction().mockImpl((a, b) => true);
var diffB = jest.genMockFunction().mockImpl((a, b) => false);
expect(diff(
{a: [1], b: [3]},
{a: [2], b: [4]},
{a: {diff: diffA}, b: {diff: diffB}}
)).toEqual({a: [2]});
expect(diffA).toBeCalledWith([1], [2]);
expect(diffB).toBeCalledWith([3], [4]);
});
it('should not use the diff attribute on addition/removal', () => {
var diffA = jest.genMockFunction();
var diffB = jest.genMockFunction();
expect(diff(
{a: [1]},
{b: [2]},
{a: {diff: diffA}, b: {diff: diffB}}
)).toEqual({a: null, b: [2]});
expect(diffA).not.toBeCalled();
expect(diffB).not.toBeCalled();
});
it('should do deep diffs of Objects by default', () => {
expect(diff(
{a: [1], b: {k: [3, 4]}, c: {k: [4, 4]} },
{a: [2], b: {k: [3, 4]}, c: {k: [4, 5]} },
{a: true, b: true, c: true}
)).toEqual({a: [2], c: {k: [4, 5]}});
});
it('should work with undefined styles', () => {
expect(diff(
{ style: { a: '#ffffff', b: 1 } },
{ style: undefined },
{ style: { b: true } }
)).toEqual({ b: null });
expect(diff(
{ style: undefined },
{ style: { a: '#ffffff', b: 1 } },
{ style: { b: true } }
)).toEqual({ b: 1 });
expect(diff(
{ style: undefined },
{ style: undefined },
{ style: { b: true } }
)).toEqual(null);
});
it('should work with empty styles', () => {
expect(diff(
{a: 1, c: 3},
{},
{a: true, b: true}
)).toEqual({a: null});
expect(diff(
{},
{a: 1, c: 3},
{a: true, b: true}
)).toEqual({a: 1});
expect(diff(
{},
{},
{a: true, b: true}
)).toEqual(null);
});
it('should flatten nested styles and predefined styles', () => {
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
expect(diff(
{},
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
validStyleAttribute
)).toEqual({ foo: 1, bar: 2 });
expect(diff(
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
{},
validStyleAttribute
)).toEqual({ foo: null, bar: null });
var barStyle = ReactNativePropRegistry.register({
bar: 3,
});
expect(diff(
{},
{ someStyle: [[{ foo: 1 }, { foo: 2 }], barStyle]},
validStyleAttribute
)).toEqual({ foo: 2, bar: 3 });
});
it('should reset a value to a previous if it is removed', () => {
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
expect(diff(
{ someStyle: [{ foo: 1 }, { foo: 3 }]},
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
validStyleAttribute
)).toEqual({ foo: 1, bar: 2 });
});
it('should not clear removed props if they are still in another slot', () => {
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
expect(diff(
{ someStyle: [{}, { foo: 3, bar: 2 }]},
{ someStyle: [{ foo: 3 }, { bar: 2 }]},
validStyleAttribute
)).toEqual({ foo: 3 }); // this should ideally be null. heuristic tradeoff.
expect(diff(
{ someStyle: [{}, { foo: 3, bar: 2 }]},
{ someStyle: [{ foo: 1, bar: 1 }, { bar: 2 }]},
validStyleAttribute
)).toEqual({ bar: 2, foo: 1 });
});
it('should clear a prop if a later style is explicit null/undefined', () => {
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
expect(diff(
{ someStyle: [{}, { foo: 3, bar: 2 }]},
{ someStyle: [{ foo: 1 }, { bar: 2, foo: null }]},
validStyleAttribute
)).toEqual({ foo: null });
expect(diff(
{ someStyle: [{ foo: 3 }, { foo: null, bar: 2 }]},
{ someStyle: [{ foo: null }, { bar: 2 }]},
validStyleAttribute
)).toEqual({ foo: null });
expect(diff(
{ someStyle: [{ foo: 1 }, { foo: null }]},
{ someStyle: [{ foo: 2 }, { foo: null }]},
validStyleAttribute
)).toEqual({ foo: null }); // this should ideally be null. heuristic.
// Test the same case with object equality because an early bailout doesn't
// work in this case.
var fooObj = { foo: 3 };
expect(diff(
{ someStyle: [{ foo: 1 }, fooObj]},
{ someStyle: [{ foo: 2 }, fooObj]},
validStyleAttribute
)).toEqual({ foo: 3 }); // this should ideally be null. heuristic.
expect(diff(
{ someStyle: [{ foo: 1 }, { foo: 3 }]},
{ someStyle: [{ foo: 2 }, { foo: undefined }]},
validStyleAttribute
)).toEqual({ foo: null }); // this should ideally be null. heuristic.
});
// Function properties are just markers to native that events should be sent.
it('should convert functions to booleans', () => {
// Note that if the property changes from one function to another, we don't
// need to send an update.
expect(diff(
{
a: function() {
return 1;
},
b: function() {
return 2;
},
c: 3,
},
{
b: function() {
return 9;
},
c: function() {
return 3;
},
},
{a: true, b: true, c: true}
)).toEqual({a: null, c: true});
});
});
@@ -0,0 +1,61 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var React;
var ReactNative;
var createReactNativeComponentClass;
var UIManager;
describe('ReactNative', function() {
beforeEach(function() {
React = require('React');
ReactNative = require('ReactNative');
UIManager = require('UIManager');
createReactNativeComponentClass = require('createReactNativeComponentClass');
});
it('should be able to create and render a native component', function() {
var View = createReactNativeComponentClass({
validAttributes: { foo: true },
uiViewClassName: 'View',
});
ReactNative.render(<View foo="test" />, 1);
expect(UIManager.createView).toBeCalled();
expect(UIManager.setChildren).toBeCalled();
expect(UIManager.manageChildren).not.toBeCalled();
expect(UIManager.updateView).not.toBeCalled();
});
it('should be able to create and update a native component', function() {
var View = createReactNativeComponentClass({
validAttributes: { foo: true },
uiViewClassName: 'View',
});
ReactNative.render(<View foo="foo" />, 11);
expect(UIManager.createView.mock.calls.length).toBe(2);
expect(UIManager.setChildren.mock.calls.length).toBe(2);
expect(UIManager.manageChildren).not.toBeCalled();
expect(UIManager.updateView).not.toBeCalled();
ReactNative.render(<View foo="bar" />, 11);
expect(UIManager.createView.mock.calls.length).toBe(2);
expect(UIManager.setChildren.mock.calls.length).toBe(2);
expect(UIManager.manageChildren).not.toBeCalled();
expect(UIManager.updateView).toBeCalledWith(3, 'View', { foo: 'bar' });
});
});
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule createReactNativeComponentClass
* @flow
*/
'use strict';
var ReactNativeBaseComponent = require('ReactNativeBaseComponent');
// See also ReactNativeBaseComponent
type ReactNativeBaseComponentViewConfig = {
validAttributes: Object;
uiViewClassName: string;
propTypes?: Object,
}
/**
* @param {string} config iOS View configuration.
* @private
*/
var createReactNativeComponentClass = function(
viewConfig: ReactNativeBaseComponentViewConfig
): ReactClass<any> {
var Constructor = function(element) {
this._currentElement = element;
this._topLevelWrapper = null;
this._nativeParent = null;
this._nativeContainerInfo = null;
this._rootNodeID = null;
this._renderedChildren = null;
};
Constructor.displayName = viewConfig.uiViewClassName;
Constructor.viewConfig = viewConfig;
Constructor.propTypes = viewConfig.propTypes;
Constructor.prototype = new ReactNativeBaseComponent(viewConfig);
Constructor.prototype.constructor = Constructor;
return ((Constructor: any): ReactClass);
};
module.exports = createReactNativeComponentClass;
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule findNodeHandle
* @flow
*/
'use strict';
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactInstanceMap = require('ReactInstanceMap');
var invariant = require('invariant');
var warning = require('warning');
/**
* ReactNative vs ReactWeb
* -----------------------
* React treats some pieces of data opaquely. This means that the information
* is first class (it can be passed around), but cannot be inspected. This
* allows us to build infrastructure that reasons about resources, without
* making assumptions about the nature of those resources, and this allows that
* infra to be shared across multiple platforms, where the resources are very
* different. General infra (such as `ReactMultiChild`) reasons opaquely about
* the data, but platform specific code (such as `ReactNativeBaseComponent`) can
* make assumptions about the data.
*
*
* `rootNodeID`, uniquely identifies a position in the generated native view
* tree. Many layers of composite components (created with `React.createClass`)
* can all share the same `rootNodeID`.
*
* `nodeHandle`: A sufficiently unambiguous way to refer to a lower level
* resource (dom node, native view etc). The `rootNodeID` is sufficient for web
* `nodeHandle`s, because the position in a tree is always enough to uniquely
* identify a DOM node (we never have nodes in some bank outside of the
* document). The same would be true for `ReactNative`, but we must maintain a
* mapping that we can send efficiently serializable
* strings across native boundaries.
*
* Opaque name TodaysWebReact FutureWebWorkerReact ReactNative
* ----------------------------------------------------------------------------
* nodeHandle N/A rootNodeID tag
*/
function findNodeHandle(componentOrHandle: any): ?number {
if (__DEV__) {
var owner = ReactCurrentOwner.current;
if (owner !== null) {
warning(
owner._warnedAboutRefsInRender,
'%s is accessing findNodeHandle inside its render(). ' +
'render() should be a pure function of props and state. It should ' +
'never access something that requires stale data from the previous ' +
'render, such as refs. Move this logic to componentDidMount and ' +
'componentDidUpdate instead.',
owner.getName() || 'A component'
);
owner._warnedAboutRefsInRender = true;
}
}
if (componentOrHandle == null) {
return null;
}
if (typeof componentOrHandle === 'number') {
// Already a node handle
return componentOrHandle;
}
var component = componentOrHandle;
// TODO (balpert): Wrap iOS native components in a composite wrapper, then
// ReactInstanceMap.get here will always succeed for mounted components
var internalInstance = ReactInstanceMap.get(component);
if (internalInstance) {
return internalInstance.getNativeNode();
} else {
var rootNodeID = component._rootNodeID;
if (rootNodeID) {
return rootNodeID;
} else {
invariant(
(
// Native
typeof component === 'object' &&
'_rootNodeID' in component
) || (
// Composite
component.render != null &&
typeof component.render === 'function'
),
'findNodeHandle(...): Argument is not a component ' +
'(type: %s, keys: %s)',
typeof component,
Object.keys(component)
);
invariant(
false,
'findNodeHandle(...): Unable to find node handle for unmounted ' +
'component.'
);
}
}
}
module.exports = findNodeHandle;
@@ -204,7 +204,7 @@ function executeDirectDispatch(event) {
!Array.isArray(dispatchListener),
'executeDirectDispatch(...): Invalid `event`.'
);
event.currentTarget = EventPluginUtils.getNodeFromInstance(dispatchInstance);
event.currentTarget = dispatchListener ? EventPluginUtils.getNodeFromInstance(dispatchInstance) : null;
var res = dispatchListener ? dispatchListener(event) : null;
event.currentTarget = null;
event._dispatchListeners = null;
@@ -0,0 +1,122 @@
/**
* @providesModule TouchHistoryMath
*/
'use strict';
var TouchHistoryMath = {
/**
* This code is optimized and not intended to look beautiful. This allows
* computing of touch centroids that have moved after `touchesChangedAfter`
* timeStamp. You can compute the current centroid involving all touches
* moves after `touchesChangedAfter`, or you can compute the previous
* centroid of all touches that were moved after `touchesChangedAfter`.
*
* @param {TouchHistoryMath} touchHistory Standard Responder touch track
* data.
* @param {number} touchesChangedAfter timeStamp after which moved touches
* are considered "actively moving" - not just "active".
* @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension.
* @param {boolean} ofCurrent Compute current centroid for actively moving
* touches vs. previous centroid of now actively moving touches.
* @return {number} value of centroid in specified dimension.
*/
centroidDimension: function(touchHistory, touchesChangedAfter, isXAxis, ofCurrent) {
var touchBank = touchHistory.touchBank;
var total = 0;
var count = 0;
var oneTouchData = touchHistory.numberActiveTouches === 1 ?
touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null;
if (oneTouchData !== null) {
if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) {
total += ofCurrent && isXAxis ? oneTouchData.currentPageX :
ofCurrent && !isXAxis ? oneTouchData.currentPageY :
!ofCurrent && isXAxis ? oneTouchData.previousPageX :
oneTouchData.previousPageY;
count = 1;
}
} else {
for (var i = 0; i < touchBank.length; i++) {
var touchTrack = touchBank[i];
if (touchTrack !== null &&
touchTrack !== undefined &&
touchTrack.touchActive &&
touchTrack.currentTimeStamp >= touchesChangedAfter) {
var toAdd; // Yuck, program temporarily in invalid state.
if (ofCurrent && isXAxis) {
toAdd = touchTrack.currentPageX;
} else if (ofCurrent && !isXAxis) {
toAdd = touchTrack.currentPageY;
} else if (!ofCurrent && isXAxis) {
toAdd = touchTrack.previousPageX;
} else {
toAdd = touchTrack.previousPageY;
}
total += toAdd;
count++;
}
}
}
return count > 0 ? total / count : TouchHistoryMath.noCentroid;
},
currentCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
true, // isXAxis
true // ofCurrent
);
},
currentCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
false, // isXAxis
true // ofCurrent
);
},
previousCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
true, // isXAxis
false // ofCurrent
);
},
previousCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
false, // isXAxis
false // ofCurrent
);
},
currentCentroidX: function(touchHistory) {
return TouchHistoryMath.centroidDimension(
touchHistory,
0, // touchesChangedAfter
true, // isXAxis
true // ofCurrent
);
},
currentCentroidY: function(touchHistory) {
return TouchHistoryMath.centroidDimension(
touchHistory,
0, // touchesChangedAfter
false, // isXAxis
true // ofCurrent
);
},
noCentroid: -1,
};
module.exports = TouchHistoryMath;
@@ -14,6 +14,7 @@
var ReactReconciler = require('ReactReconciler');
var instantiateReactComponent = require('instantiateReactComponent');
var KeyEscapeUtils = require('KeyEscapeUtils');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var traverseAllChildren = require('traverseAllChildren');
var warning = require('warning');
@@ -27,7 +28,7 @@ function instantiateChild(childInstances, child, name) {
'flattenChildren(...): Encountered two children with the same key, ' +
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.',
name
KeyEscapeUtils.unescape(name)
);
}
if (child != null && keyUnique) {
@@ -60,6 +60,10 @@ function warnIfInvalidElement(Component, element) {
}
}
function shouldConstruct(Component) {
return Component.prototype && Component.prototype.isReactComponent;
}
/**
* ------------------ The Life-Cycle of a Composite Component ------------------
*
@@ -128,6 +132,9 @@ var ReactCompositeComponentMixin = {
// See ReactUpdates and ReactUpdateQueue.
this._pendingCallbacks = null;
// ComponentWillUnmount shall only be called once
this._calledComponentWillUnmount = false;
},
/**
@@ -158,44 +165,22 @@ var ReactCompositeComponentMixin = {
var Component = this._currentElement.type;
// Initialize the public class
var inst;
var inst = this._constructComponent(publicProps, publicContext);
var renderedElement;
if (Component.prototype && Component.prototype.isReactComponent) {
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
} finally {
ReactCurrentOwner.current = null;
}
} else {
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
}
} else {
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
inst = Component(publicProps, publicContext, ReactUpdateQueue);
} finally {
ReactCurrentOwner.current = null;
}
} else {
inst = Component(publicProps, publicContext, ReactUpdateQueue);
}
if (inst == null || inst.render == null) {
renderedElement = inst;
warnIfInvalidElement(Component, renderedElement);
invariant(
inst === null ||
inst === false ||
ReactElement.isValidElement(inst),
'%s(...): A valid React element (or null) must be returned. You may have ' +
'returned undefined, an array or some other invalid object.',
Component.displayName || Component.name || 'Component'
);
inst = new StatelessComponent(Component);
}
// Support functional components
if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
renderedElement = inst;
warnIfInvalidElement(Component, renderedElement);
invariant(
inst === null ||
inst === false ||
ReactElement.isValidElement(inst),
'%s(...): A valid React element (or null) must be returned. You may have ' +
'returned undefined, an array or some other invalid object.',
Component.displayName || Component.name || 'Component'
);
inst = new StatelessComponent(Component);
}
if (__DEV__) {
@@ -323,6 +308,28 @@ var ReactCompositeComponentMixin = {
return markup;
},
_constructComponent: function(publicProps, publicContext) {
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
return this._constructComponentWithoutOwner(publicProps, publicContext);
} finally {
ReactCurrentOwner.current = null;
}
} else {
return this._constructComponentWithoutOwner(publicProps, publicContext);
}
},
_constructComponentWithoutOwner: function(publicProps, publicContext) {
var Component = this._currentElement.type;
if (shouldConstruct(Component)) {
return new Component(publicProps, publicContext, ReactUpdateQueue);
} else {
return Component(publicProps, publicContext, ReactUpdateQueue);
}
},
performInitialMountWithErrorHandling: function(
renderedElement,
nativeParent,
@@ -401,7 +408,8 @@ var ReactCompositeComponentMixin = {
}
var inst = this._instance;
if (inst.componentWillUnmount) {
if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) {
inst._calledComponentWillUnmount = true;
if (safely) {
var name = this.getName() + '.componentWillUnmount()';
ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
@@ -1303,4 +1303,45 @@ describe('ReactCompositeComponent', function() {
});
it('should only call componentWillUnmount once', function() {
var app;
var count = 0;
class App extends React.Component {
render() {
if (this.props.stage === 1) {
return <UnunmountableComponent />;
} else {
return null;
}
}
}
class UnunmountableComponent extends React.Component {
componentWillUnmount() {
app.setState({});
count++;
throw Error('always fails');
}
render() {
return <div>Hello {this.props.name}</div>;
}
}
var container = document.createElement('div');
var setRef = (ref) => {
if (ref) {
app = ref;
}
};
expect(function() {
ReactDOM.render(<App ref={setRef} stage={1} />, container);
ReactDOM.render(<App ref={setRef} stage={2} />, container);
}).toThrow();
expect(count).toBe(1);
});
});
+64
View File
@@ -0,0 +1,64 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule KeyEscapeUtils
*/
'use strict';
/**
* Escape and wrap key so it is safe to use as a reactid
*
* @param {*} key to be escaped.
* @return {string} the escaped key.
*/
function escape(key) {
var escapeRegex = /[=:]/g;
var escaperLookup = {
'=': '=0',
':': '=2',
};
var escapedString = ('' + key).replace(
escapeRegex,
function(match) {
return escaperLookup[match];
}
);
return '$' + escapedString;
}
/**
* Unescape and unwrap key for human-readable display
*
* @param {string} key to unescape.
* @return {string} the unescaped key.
*/
function unescape(key) {
var unescapeRegex = /(=0|=2)/g;
var unescaperLookup = {
'=0': '=',
'=2': ':',
};
var keySubstring = (key[0] === '.' && key[1] === '$')
? key.substring(2) : key.substring(1);
return ('' + keySubstring).replace(
unescapeRegex,
function(match) {
return unescaperLookup[match];
}
);
}
var KeyEscapeUtils = {
escape: escape,
unescape: unescape,
};
module.exports = KeyEscapeUtils;
@@ -0,0 +1,38 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var KeyEscapeUtils;
describe('KeyEscapeUtils', () => {
beforeEach(() => {
jest.resetModuleRegistry();
KeyEscapeUtils = require('KeyEscapeUtils');
});
describe('escape', () => {
it('should properly escape and wrap user defined keys', () => {
expect(KeyEscapeUtils.escape('1')).toBe('$1');
expect(KeyEscapeUtils.escape('1=::=2')).toBe('$1=0=2=2=02');
});
});
describe('unescape', () => {
it('should properly unescape and unwrap user defined keys', () => {
expect(KeyEscapeUtils.unescape('.1')).toBe('1');
expect(KeyEscapeUtils.unescape('$1')).toBe('1');
expect(KeyEscapeUtils.unescape('.$1')).toBe('1');
expect(KeyEscapeUtils.unescape('$1=0=2=2=02')).toBe('1=::=2');
});
});
});
@@ -1,941 +0,0 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var OrderedMap;
/**
* Shared, reusable objects.
*/
var hasEmptyStringKey = {
'thisKeyIsFine': {data: []},
'': {thisShouldCauseAFailure: []},
'thisKeyIsAlsoFine': {data: []},
};
/**
* Used as map/forEach callback.
*/
var duplicate = function(itm, key, count) {
return {
uniqueID: itm.uniqueID,
val: itm.val + key + count + this.justToTestScope,
};
};
// Should not be allowed - because then null/'null' become impossible to
// distinguish. Every key MUST be a string period!
var hasNullAndUndefStringKey = [
{uniqueID: 'undefined', val: 'thisIsUndefined'},
{uniqueID: 'null', val: 'thisIsNull'},
];
var hasNullKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: null, data: []},
];
var hasObjectKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: {}, data: []},
];
var hasArrayKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: [], data: []},
];
// This should be allowed
var hasNullStringKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: 'null', data: []},
];
var hasUndefinedKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: undefined, data: []},
];
var hasUndefinedStringKey = [
{uniqueID: 'thisKeyIsFine', data: []},
{uniqueID: 'thisKeyIsAlsoFine', data: []},
{uniqueID: 'undefined', data: []},
];
var hasPositiveNumericKey = [
{uniqueID: 'notANumber', data: []},
{uniqueID: '5', data: []},
{uniqueID: 'notAnotherNumber', data: []},
];
var hasZeroStringKey = [
{uniqueID: 'greg', data: 'grego'},
{uniqueID: '0', data: '0o'},
{uniqueID: 'tom', data: 'tomo'},
];
var hasZeroNumberKey = [
{uniqueID: 'greg', data: 'grego'},
{uniqueID: 0, data: '0o'},
{uniqueID: 'tom', data: 'tomo'},
];
var hasAllNumericStringKeys = [
{uniqueID: '0', name: 'Gregory'},
{uniqueID: '2', name: 'James'},
{uniqueID: '1', name: 'Tom'},
];
var hasAllNumericKeys = [
{uniqueID: 0, name: 'Gregory'},
{uniqueID: 2, name: 'James'},
{uniqueID: 1, name: 'Tom'},
];
var hasAllValidKeys = [
{uniqueID: 'keyOne', value: 'valueOne'},
{uniqueID: 'keyTwo', value: 'valueTwo'},
];
var hasDuplicateKeys = [
{uniqueID: 'keyOne', value: 'valueOne'},
{uniqueID: 'keyTwo', value: 'valueTwo'},
{uniqueID: 'keyOne', value: 'valueThree'},
];
var idEntities = [
{uniqueID: 'greg', name: 'Gregory'},
{uniqueID: 'james', name: 'James'},
{uniqueID: 'tom', name: 'Tom'},
];
var hasEmptyKey = [
{uniqueID: 'greg', name: 'Gregory'},
{uniqueID: '', name: 'James'},
{uniqueID: 'tom', name: 'Tom'},
];
var extractUniqueID = function(entity) {
return entity.uniqueID;
};
describe('OrderedMap', function() {
beforeEach(function() {
jest.resetModuleRegistry();
OrderedMap = require('OrderedMap');
});
it('should create according to simple object with keys', function() {
OrderedMap.fromArray(hasAllValidKeys, extractUniqueID);
// Iterate over and ensure key order.
});
it('should create from array when providing an identity CB', function() {
expect(function() {
OrderedMap.fromArray(idEntities, extractUniqueID);
}).not.toThrow();
});
it('should throw if constructing from Array without identity CB', function() {
OrderedMap.fromArray(idEntities, extractUniqueID);
// Iterate and ensure key order
});
it('should not throw when fromArray extracts a numeric key', function() {
expect(function() {
OrderedMap.fromArray(hasPositiveNumericKey, extractUniqueID);
}).not.toThrow();
});
it('should throw when any key is the empty string', function() {
expect(function() {
OrderedMap.fromArray(hasEmptyKey, extractUniqueID);
}).toThrow();
});
it('should not throw when a key is the string "undefined" or "null"',
function() {
var om = OrderedMap.fromArray(hasNullAndUndefStringKey, extractUniqueID);
expect(om.length).toBe(2);
expect(om.indexOfKey('undefined')).toBe(0);
expect(om.indexOfKey('null')).toBe(1);
expect(om.keyAfter('undefined')).toBe('null');
expect(om.keyAfter('null')).toBe(undefined);
expect(om.keyBefore('undefined')).toBe(undefined);
expect(om.has('undefined')).toBe(true);
expect(om.has('null')).toBe(true);
expect(om.get('undefined').val).toBe('thisIsUndefined');
expect(om.get('null').val).toBe('thisIsNull');
});
/**
* Numeric keys are cast to strings.
*/
it('should not throw when a key is the number zero', function() {
var om = OrderedMap.fromArray(hasZeroNumberKey, extractUniqueID);
expect(om.length).toBe(3);
expect(om.indexOfKey('0')).toBe(1);
expect(om.indexOfKey(0)).toBe(1);
});
it('should throw when any key is falsey', function() {
expect(function() {
OrderedMap.fromArray(hasEmptyStringKey, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(hasNullKey, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(hasUndefinedKey, extractUniqueID);
}).toThrow();
});
it('should not throw on string keys "undefined/null"', function() {
expect(function() {
OrderedMap.fromArray(hasNullStringKey, extractUniqueID);
}).not.toThrow();
expect(function() {
OrderedMap.fromArray(hasUndefinedStringKey, extractUniqueID);
}).not.toThrow();
});
it('should throw on extracting keys that are not strings/nums', function() {
expect(function() {
OrderedMap.fromArray(hasObjectKey, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(hasArrayKey, extractUniqueID);
}).toThrow();
});
it('should throw if instantiating with duplicate key', function() {
expect(function() {
OrderedMap.fromArray(hasDuplicateKeys, extractUniqueID);
}).toThrow();
});
it('should not throw when a key is the string "0"', function() {
var verifyOM = function(om) {
expect(om.length).toBe(3);
expect(om.indexOfKey('greg')).toBe(0);
expect(om.indexOfKey('0')).toBe(1);
expect(om.indexOfKey(0)).toBe(1); // Casts on writes and reads.
expect(om.indexOfKey('tom')).toBe(2);
expect(om.keyAfter('greg')).toBe('0');
expect(om.keyAfter('0')).toBe('tom');
expect(om.keyAfter(0)).toBe('tom');
expect(om.keyAfter('tom')).toBe(undefined);
expect(om.keyBefore('greg')).toBe(undefined);
expect(om.keyBefore(0)).toBe('greg');
expect(om.keyBefore('0')).toBe('greg');
expect(om.keyBefore('tom')).toBe('0');
expect(om.has('undefined')).toBe(false);
expect(om.has(0)).toBe(true);
expect(om.has('0')).toBe(true);
};
verifyOM(OrderedMap.fromArray(hasZeroStringKey, extractUniqueID));
verifyOM(OrderedMap.fromArray(hasZeroNumberKey, extractUniqueID));
});
it('should throw when getting invalid public key', function() {
var om = OrderedMap.fromArray(hasAllValidKeys, extractUniqueID);
expect(function() {
om.has(undefined);
}).toThrow();
expect(function() {
om.get(undefined);
}).toThrow();
expect(function() {
om.has(null);
}).toThrow();
expect(function() {
om.get(null);
}).toThrow();
expect(function() {
om.has('');
}).toThrow();
expect(function() {
om.get('');
}).toThrow();
});
it('should throw when any key is falsey', function() {
expect(function() {
OrderedMap.fromArray(hasEmptyStringKey, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(hasNullKey, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(hasUndefinedKey, extractUniqueID);
}).toThrow();
});
it('should throw when fromArray is passed crazy args', function() {
// Test passing another OrderedMap (when it expects a plain object.)
// This is probably not what you meant to do! We should error.
var validOM = OrderedMap.fromArray(hasAllValidKeys, extractUniqueID);
expect(function() {
OrderedMap.fromArray({uniqueID: 'asdf'}, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(validOM, extractUniqueID);
}).toThrow();
});
it('should throw when fromArray is passed crazy things', function() {
expect(function() {
OrderedMap.fromArray(null, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray('stringgg', extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(undefined, extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray(new Date(), extractUniqueID);
}).toThrow();
expect(function() {
OrderedMap.fromArray({}, extractUniqueID);
}).toThrow();
// Test failure without extractor
expect(function() {
OrderedMap.fromArray(idEntities);
}).toThrow();
expect(function() {
OrderedMap.fromArray(idEntities, extractUniqueID);
}).not.toThrow();
});
// Testing methods that accept other `OrderedMap`s.
it('should throw when from/merge is passed an non-OrderedMap.', function() {
// Test passing an array to construction.
expect(function() {
OrderedMap.from(idEntities, extractUniqueID);
}).toThrow();
// Test passing an array to merge.
expect(function() {
OrderedMap.fromArray(idEntities, extractUniqueID)
.merge(idEntities, extractUniqueID);
}).toThrow();
// Test passing a plain object to merge.
expect(function() {
OrderedMap.fromArray(
idEntities,
extractUniqueID
).merge({blah: 'willFail'});
}).toThrow();
});
it('should throw when accessing key before/after of non-key', function() {
var om = OrderedMap.fromArray(
[
{uniqueID: 'first'},
{uniqueID: 'two'},
], extractUniqueID
);
expect(function() {
om.keyBefore('dog');
}).toThrow();
expect(function() {
om.keyAfter('cat');
}).toThrow();
expect(function() {
om.keyAfter(null);
}).toThrow();
expect(function() {
om.keyAfter(undefined);
}).toThrow();
});
it('should throw passing invalid/not-present-keys to before/after',
function() {
var om = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'second'},
{uniqueID: 'three', val: 'third'},
{uniqueID: 'four', val: 'fourth'},
], extractUniqueID);
expect(function() {
om.keyBefore('');
}).toThrow();
expect(function() {
om.keyBefore(null);
}).toThrow();
expect(function() {
om.keyBefore(undefined);
}).toThrow();
expect(function() {
om.keyBefore('notInTheOrderedMap!');
}).toThrow();
expect(function() {
om.keyAfter('');
}).toThrow();
expect(function() {
om.keyAfter(null);
}).toThrow();
expect(function() {
om.keyAfter(undefined);
}).toThrow();
expect(function() {
om.keyAfter('notInTheOrderedMap!');
}).toThrow();
expect(function() {
om.nthKeyAfter('', 1);
}).toThrow();
expect(function() {
om.nthKeyAfter(null, 1);
}).toThrow();
expect(function() {
om.nthKeyAfter(undefined, 1);
}).toThrow();
expect(function() {
om.nthKeyAfter('notInTheOrderedMap!', 1);
}).toThrow();
expect(function() {
om.nthKeyBefore('', 1);
}).toThrow();
expect(function() {
om.nthKeyBefore(null, 1);
}).toThrow();
expect(function() {
om.nthKeyBefore(undefined, 1);
}).toThrow();
expect(function() {
om.nthKeyBefore('notInTheOrderedMap!', 1);
}).toThrow();
});
it('should correctly determine the nth key after before', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'second'},
{uniqueID: 'three', val: 'third'},
{uniqueID: 'four', val: 'fourth'},
], extractUniqueID);
expect(om.keyBefore('one')).toBe(undefined); // first key
expect(om.keyBefore('two')).toBe('one');
expect(om.keyBefore('three')).toBe('two');
expect(om.keyBefore('four')).toBe('three');
expect(om.keyAfter('one')).toBe('two'); // first key
expect(om.keyAfter('two')).toBe('three');
expect(om.keyAfter('three')).toBe('four');
expect(om.keyAfter('four')).toBe(undefined);
expect(om.nthKeyBefore('one', 0)).toBe('one'); // first key
expect(om.nthKeyBefore('one', 1)).toBe(undefined);
expect(om.nthKeyBefore('one', 2)).toBe(undefined);
expect(om.nthKeyBefore('two', 0)).toBe('two');
expect(om.nthKeyBefore('two', 1)).toBe('one');
expect(om.nthKeyBefore('four', 0)).toBe('four');
expect(om.nthKeyBefore('four', 1)).toBe('three');
expect(om.nthKeyAfter('one', 0)).toBe('one');
expect(om.nthKeyAfter('one', 1)).toBe('two');
expect(om.nthKeyAfter('one', 2)).toBe('three');
expect(om.nthKeyAfter('two', 0)).toBe('two');
expect(om.nthKeyAfter('two', 1)).toBe('three');
expect(om.nthKeyAfter('four', 0)).toBe('four');
expect(om.nthKeyAfter('four', 1)).toBe(undefined);
});
it('should compute key indices correctly', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'second'},
], extractUniqueID);
expect(om.keyAtIndex(0)).toBe('one');
expect(om.keyAtIndex(1)).toBe('two');
expect(om.keyAtIndex(2)).toBe(undefined);
expect(om.indexOfKey('one')).toBe(0);
expect(om.indexOfKey('two')).toBe(1);
expect(om.indexOfKey('nope')).toBe(undefined);
expect(function() {
om.indexOfKey(null);
}).toThrow();
expect(function() {
om.indexOfKey(undefined);
}).toThrow();
expect(function() {
om.indexOfKey(''); // Empty key is not allowed
}).toThrow();
});
it('should compute indices on array that extracted numeric ids', function() {
var som = OrderedMap.fromArray(hasZeroStringKey, extractUniqueID);
expect(som.keyAtIndex(0)).toBe('greg');
expect(som.keyAtIndex(1)).toBe('0');
expect(som.keyAtIndex(2)).toBe('tom');
expect(som.indexOfKey('greg')).toBe(0);
expect(som.indexOfKey('0')).toBe(1);
expect(som.indexOfKey('tom')).toBe(2);
var verifyNumericKeys = function(nom) {
expect(nom.keyAtIndex(0)).toBe('0');
expect(nom.keyAtIndex(1)).toBe('2');
expect(nom.keyAtIndex(2)).toBe('1');
expect(nom.indexOfKey('0')).toBe(0);
expect(nom.indexOfKey('2')).toBe(1); // Prove these are not ordered by
expect(nom.indexOfKey('1')).toBe(2); // their keys
};
var omStringNumberKeys =
OrderedMap.fromArray(hasAllNumericStringKeys, extractUniqueID);
verifyNumericKeys(omStringNumberKeys);
var omNumericKeys =
OrderedMap.fromArray(hasAllNumericKeys, extractUniqueID);
verifyNumericKeys(omNumericKeys);
});
it('should compute indices on mutually exclusive merge', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'second'},
], extractUniqueID);
var om2 = OrderedMap.fromArray([
{uniqueID: 'three', val: 'third'},
], extractUniqueID);
var res = om.merge(om2);
expect(res.length).toBe(3);
expect(res.keyAtIndex(0)).toBe('one');
expect(res.keyAtIndex(1)).toBe('two');
expect(res.keyAtIndex(2)).toBe('three');
expect(res.keyAtIndex(3)).toBe(undefined);
expect(res.indexOfKey('one')).toBe(0);
expect(res.indexOfKey('two')).toBe(1);
expect(res.indexOfKey('three')).toBe(2);
expect(res.indexOfKey('dog')).toBe(undefined);
expect(res.has('one')).toBe(true);
expect(res.has('two')).toBe(true);
expect(res.has('three')).toBe(true);
expect(res.has('dog')).toBe(false);
expect(res.get('one').val).toBe('first');
expect(res.get('two').val).toBe('second');
expect(res.get('three').val).toBe('third');
expect(res.get('dog')).toBe(undefined);
});
it('should compute indices on intersected merge', function() {
var oneTwo = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'secondOM1'},
], extractUniqueID);
var testOneTwoMergedWithTwoThree = function(res) {
expect(res.length).toBe(3);
expect(res.keyAtIndex(0)).toBe('one');
expect(res.keyAtIndex(1)).toBe('two');
expect(res.keyAtIndex(2)).toBe('three');
expect(res.keyAtIndex(3)).toBe(undefined);
expect(res.indexOfKey('one')).toBe(0);
expect(res.indexOfKey('two')).toBe(1);
expect(res.indexOfKey('three')).toBe(2);
expect(res.indexOfKey('dog')).toBe(undefined);
expect(res.has('one')).toBe(true);
expect(res.has('two')).toBe(true);
expect(res.has('three')).toBe(true);
expect(res.has('dog')).toBe(false);
expect(res.get('one').val).toBe('first');
expect(res.get('two').val).toBe('secondOM2');
expect(res.get('three').val).toBe('third');
expect(res.get('dog')).toBe(undefined);
};
var result =
oneTwo.merge(OrderedMap.fromArray([
{uniqueID: 'two', val: 'secondOM2'},
{uniqueID: 'three', val: 'third'},
], extractUniqueID));
testOneTwoMergedWithTwoThree(result);
// Everything should be exactly as before, since the ordering of `two` was
// already determined by `om`.
result = oneTwo.merge(
OrderedMap.fromArray([
{uniqueID: 'three', val: 'third'},
{uniqueID: 'two', val:'secondOM2'},
], extractUniqueID)
);
testOneTwoMergedWithTwoThree(result);
var testTwoThreeMergedWithOneTwo = function(res) {
expect(res.length).toBe(3);
expect(res.keyAtIndex(0)).toBe('two');
expect(res.keyAtIndex(1)).toBe('three');
expect(res.keyAtIndex(2)).toBe('one');
expect(res.keyAtIndex(3)).toBe(undefined);
expect(res.indexOfKey('two')).toBe(0);
expect(res.indexOfKey('three')).toBe(1);
expect(res.indexOfKey('one')).toBe(2);
expect(res.indexOfKey('cat')).toBe(undefined);
expect(res.has('two')).toBe(true);
expect(res.has('three')).toBe(true);
expect(res.has('one')).toBe(true);
expect(res.has('dog')).toBe(false);
expect(res.get('one').val).toBe('first');
expect(res.get('two').val).toBe('secondOM1');
expect(res.get('three').val).toBe('third');
expect(res.get('dog')).toBe(undefined);
};
result = OrderedMap.fromArray([
{uniqueID: 'two', val: 'secondOM2'},
{uniqueID: 'three', val: 'third'},
], extractUniqueID).merge(oneTwo);
testTwoThreeMergedWithOneTwo(result);
});
it('should merge mutually exclusive keys to the end.', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'one', val: 'first'},
{uniqueID: 'two', val: 'second'},
], extractUniqueID);
var om2 = OrderedMap.fromArray([
{uniqueID: 'three', val: 'first'},
{uniqueID: 'four', val: 'second'},
], extractUniqueID);
var res = om.merge(om2);
expect(res.length).toBe(4);
});
it('should map correctly', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'x', val: 'xx'},
{uniqueID: 'y', val: 'yy'},
{uniqueID: 'z', val: 'zz'},
], extractUniqueID);
var scope = {justToTestScope: 'justTestingScope'};
var verifyResult = function(omResult) {
expect(omResult.length).toBe(3);
expect(omResult.keyAtIndex(0)).toBe('x');
expect(omResult.keyAtIndex(1)).toBe('y');
expect(omResult.keyAtIndex(2)).toBe('z');
expect(omResult.get('x').val).toBe('xxx0justTestingScope');
expect(omResult.get('y').val).toBe('yyy1justTestingScope');
expect(omResult.get('z').val).toBe('zzz2justTestingScope');
};
var resultOM = om.map(function(itm, key, count) {
return {
uniqueID: itm.uniqueID,
val: itm.val + key + count + this.justToTestScope,
};
}, scope);
verifyResult(resultOM);
var resArray = [];
om.forEach(function(itm, key, count) {
resArray.push({
uniqueID: itm.uniqueID,
val: itm.val + key + count + this.justToTestScope,
});
}, scope);
resultOM = OrderedMap.fromArray(resArray, extractUniqueID);
verifyResult(resultOM);
});
it('should filter correctly', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'x', val: 'xx'},
{uniqueID: 'y', val: 'yy'},
{uniqueID: 'z', val: 'zz'},
], extractUniqueID);
var scope = {justToTestScope: 'justTestingScope'};
var filteringCallback = function(item, key, indexInOriginal) {
expect(this).toBe(scope);
expect(key === 'x' || key === 'y' || key === 'z').toBe(true);
if (key === 'x') {
expect(item.val).toBe('xx');
expect(indexInOriginal).toBe(0);
return false;
} else if (key === 'y') {
expect(item.val).toBe('yy');
expect(indexInOriginal).toBe(1);
return true;
} else {
expect(item.val).toBe('zz');
expect(indexInOriginal).toBe(2);
return true;
}
};
var verifyResult = function(omResult) {
expect(omResult.length).toBe(2);
expect(omResult.keyAtIndex(0)).toBe('y');
expect(omResult.keyAtIndex(1)).toBe('z');
expect(omResult.has('x')).toBe(false);
expect(omResult.has('z')).toBe(true);
expect(omResult.get('z').val).toBe('zz');
expect(omResult.has('y')).toBe(true);
expect(omResult.get('y').val).toBe('yy');
};
var resultOM = om.filter(filteringCallback, scope);
verifyResult(resultOM);
});
it('should throw when providing invalid ranges to ranging', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'x', val: 'xx'},
{uniqueID: 'y', val: 'yy'},
{uniqueID: 'z', val: 'zz'},
], extractUniqueID);
var scope = {justToTestScope: 'justTestingScope'};
expect(function() {
om.mapRange(duplicate, 0, 3, scope);
}).not.toThrow();
expect(function() {
om.filterRange(duplicate, 0, 3, scope);
}).not.toThrow();
expect(function() {
om.forEachRange(duplicate, 0, 3, scope);
}).not.toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'x', 3, scope);
}).toThrow(
'mapKeyRange must be given keys that are present.'
);
expect(function() {
om.forEachKeyRange(duplicate, 'x', 3, scope);
}).toThrow(
'forEachKeyRange must be given keys that are present.'
);
expect(function() {
om.mapRange(duplicate, 0, 4, scope);
}).toThrow();
expect(function() {
om.filterRange(duplicate, 0, 4, scope);
}).toThrow();
expect(function() {
om.forEachRange(duplicate, 0, 4, scope);
}).toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'x', null, scope);
}).toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'x', null, scope);
}).toThrow();
expect(function() {
om.mapRange(duplicate, -1, 1, scope);
}).toThrow();
expect(function() {
om.filterRange(duplicate, -1, 1, scope);
}).toThrow();
expect(function() {
om.forEachRange(duplicate, -1, 1, scope);
}).toThrow();
expect(function() {
om.mapKeyRange(duplicate, null, 'y', scope);
}).toThrow();
expect(function() {
om.forEachKeyRange(duplicate, null, 'y', scope);
}).toThrow();
expect(function() {
om.mapRange(duplicate, 0, 0, scope);
}).not.toThrow();
expect(function() {
om.filterRange(duplicate, 0, 0, scope);
}).not.toThrow();
expect(function() {
om.forEachRange(duplicate, 0, 0, scope);
}).not.toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'x', 'x', scope);
}).not.toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'x', 'x', scope);
}).not.toThrow();
expect(function() {
om.mapRange(duplicate, 0, -1, scope);
}).toThrow();
expect(function() {
om.filterRange(duplicate, 0, -1, scope);
}).toThrow();
expect(function() {
om.forEachRange(duplicate, 0, -1, scope);
}).toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'x', null, scope);
}).toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'x', null, scope);
}).toThrow();
expect(function() {
om.mapRange(duplicate, 2, 1, scope);
}).not.toThrow();
expect(function() {
om.filterRange(duplicate, 2, 1, scope);
}).not.toThrow();
expect(function() {
om.forEachRange(duplicate, 2, 1, scope);
}).not.toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'z', 'z', scope);
}).not.toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'z', 'z', scope);
}).not.toThrow();
expect(function() {
om.mapRange(duplicate, 2, 2, scope);
}).toThrow();
expect(function() {
om.filterRange(duplicate, 2, 2, scope);
}).toThrow();
expect(function() {
om.forEachRange(duplicate, 2, 2, scope);
}).toThrow();
expect(function() {
om.mapKeyRange(duplicate, 'z', null, scope);
}).toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'z', null, scope);
}).toThrow();
// Provide keys in reverse order - should throw.
expect(function() {
om.mapKeyRange(duplicate, 'y', 'x', scope);
}).toThrow();
expect(function() {
om.forEachKeyRange(duplicate, 'y', 'x', scope);
}).toThrow();
});
// TEST length zero map, or keyrange start===end
it('should map range correctly', function() {
var om = OrderedMap.fromArray([
{uniqueID: 'x', val: 'xx'},
{uniqueID: 'y', val: 'yy'},
{uniqueID: 'z', val: 'zz'},
], extractUniqueID);
var scope = {justToTestScope: 'justTestingScope'};
var verifyThreeItems = function(omResult) {
expect(omResult.length).toBe(3);
expect(omResult.keyAtIndex(0)).toBe('x');
expect(omResult.keyAtIndex(1)).toBe('y');
expect(omResult.keyAtIndex(2)).toBe('z');
expect(omResult.get('x').val).toBe('xxx0justTestingScope');
expect(omResult.get('y').val).toBe('yyy1justTestingScope');
expect(omResult.get('z').val).toBe('zzz2justTestingScope');
};
var verifyFirstTwoItems = function(omResult) {
expect(omResult.length).toBe(2);
expect(omResult.keyAtIndex(0)).toBe('x');
expect(omResult.keyAtIndex(1)).toBe('y');
expect(omResult.get('x').val).toBe('xxx0justTestingScope');
expect(omResult.get('y').val).toBe('yyy1justTestingScope');
};
var verifyLastTwoItems = function(omResult) {
expect(omResult.length).toBe(2);
expect(omResult.keyAtIndex(0)).toBe('y');
expect(omResult.keyAtIndex(1)).toBe('z');
expect(omResult.get('y').val).toBe('yyy1justTestingScope');
expect(omResult.get('z').val).toBe('zzz2justTestingScope');
};
var verifyMiddleItem = function(omResult) {
expect(omResult.length).toBe(1);
expect(omResult.keyAtIndex(0)).toBe('y');
expect(omResult.get('y').val).toBe('yyy1justTestingScope');
};
var verifyEmpty = function(omResult) {
expect(omResult.length).toBe(0);
};
var omResultThree = om.mapRange(duplicate, 0, 3, scope);
verifyThreeItems(omResultThree);
var resArray = [];
var pushToResArray = function(itm, key, count) {
resArray.push({
uniqueID: itm.uniqueID,
val: itm.val + key + count + this.justToTestScope,
});
};
om.forEachRange(pushToResArray, 0, 3, scope);
omResultThree = OrderedMap.fromArray(resArray, extractUniqueID);
verifyThreeItems(omResultThree);
var omResultFirstTwo = om.mapRange(duplicate, 0, 2, scope);
verifyFirstTwoItems(omResultFirstTwo);
resArray = [];
om.forEachRange(pushToResArray, 0, 2, scope);
omResultFirstTwo = OrderedMap.fromArray(resArray, extractUniqueID);
verifyFirstTwoItems(omResultFirstTwo);
var omResultLastTwo = om.mapRange(duplicate, 1, 2, scope);
verifyLastTwoItems(omResultLastTwo);
resArray = [];
om.forEachRange(pushToResArray, 1, 2, scope);
omResultLastTwo = OrderedMap.fromArray(resArray, extractUniqueID);
verifyLastTwoItems(omResultLastTwo);
var omResultMiddle = om.mapRange(duplicate, 1, 1, scope);
verifyMiddleItem(omResultMiddle);
resArray = [];
om.forEachRange(pushToResArray, 1, 1, scope);
omResultMiddle = OrderedMap.fromArray(resArray, extractUniqueID);
verifyMiddleItem(omResultMiddle);
var omResultNone = om.mapRange(duplicate, 1, 0, scope);
verifyEmpty(omResultNone);
});
it('should extract the original array correctly', function() {
var sourceArray = [
{uniqueID: 'x', val: 'xx'},
{uniqueID: 'y', val: 'yy'},
{uniqueID: 'z', val: 'zz'},
];
var om = OrderedMap.fromArray(sourceArray, extractUniqueID);
expect(om.toArray()).toEqual(sourceArray);
});
});
+2 -1
View File
@@ -11,6 +11,7 @@
'use strict';
var KeyEscapeUtils = require('KeyEscapeUtils');
var traverseAllChildren = require('traverseAllChildren');
var warning = require('warning');
@@ -29,7 +30,7 @@ function flattenSingleChildIntoContext(traverseContext, child, name) {
'flattenChildren(...): Encountered two children with the same key, ' +
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.',
name
KeyEscapeUtils.unescape(name)
);
}
if (keyUnique && child != null) {
+3 -37
View File
@@ -16,6 +16,7 @@ var ReactElement = require('ReactElement');
var getIteratorFn = require('getIteratorFn');
var invariant = require('invariant');
var KeyEscapeUtils = require('KeyEscapeUtils');
var warning = require('warning');
var SEPARATOR = '.';
@@ -26,19 +27,8 @@ var SUBSEPARATOR = ':';
* pattern.
*/
var userProvidedKeyEscaperLookup = {
'=': '=0',
':': '=2',
};
var userProvidedKeyEscapeRegex = /[=:]/g;
var didWarnAboutMaps = false;
function userProvidedKeyEscaper(match) {
return userProvidedKeyEscaperLookup[match];
}
/**
* Generate a key string that identifies a component within a set.
*
@@ -51,36 +41,12 @@ function getComponentKey(component, index) {
// that we don't block potential future ES APIs.
if (component && typeof component === 'object' && component.key != null) {
// Explicit key
return wrapUserProvidedKey(component.key);
return KeyEscapeUtils.escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}
/**
* Escape a component key so that it is safe to use in a reactid.
*
* @param {*} text Component key to be escaped.
* @return {string} An escaped string.
*/
function escapeUserProvidedKey(text) {
return ('' + text).replace(
userProvidedKeyEscapeRegex,
userProvidedKeyEscaper
);
}
/**
* Wrap a `key` value explicitly provided by the user to distinguish it from
* implicitly-generated keys generated by a component's index in its parent.
*
* @param {string} key Value of a user-provided `key` attribute
* @return {string}
*/
function wrapUserProvidedKey(key) {
return '$' + escapeUserProvidedKey(key);
}
/**
* @param {?*} children Children tree container.
* @param {!string} nameSoFar Name of the key path so far.
@@ -166,7 +132,7 @@ function traverseAllChildrenImpl(
child = entry[1];
nextName = (
nextNamePrefix +
wrapUserProvidedKey(entry[0]) + SUBSEPARATOR +
KeyEscapeUtils.escape(entry[0]) + SUBSEPARATOR +
getComponentKey(child, 0)
);
subtreeCount += traverseAllChildrenImpl(
+3 -2
View File
@@ -175,12 +175,13 @@ function getUnchangedComponents(measurement) {
// the amount of time it took to render the entire subtree.
var cleanComponents = {};
var writes = measurement.writes;
var hierarchy = measurement.hierarchy;
var dirtyComposites = {};
Object.keys(writes).forEach(function(id) {
writes[id].forEach(function(write) {
// Root mounting (innerHTML set) is recorded with an ID of ''
if (id !== '') {
measurement.hierarchy[id].forEach((c) => dirtyComposites[c] = true);
if (id !== '' && hierarchy.hasOwnProperty(id)) {
hierarchy[id].forEach((c) => dirtyComposites[c] = true);
}
});
});
+11 -3
View File
@@ -178,9 +178,6 @@ var ReactTestUtils = {
* @return {array} an array of all the matches.
*/
scryRenderedDOMComponentsWithClass: function(root, classNames) {
if (!Array.isArray(classNames)) {
classNames = classNames.split(/\s+/);
}
return ReactTestUtils.findAllInRenderedTree(root, function(inst) {
if (ReactTestUtils.isDOMComponent(inst)) {
var className = inst.className;
@@ -189,6 +186,15 @@ var ReactTestUtils = {
className = inst.getAttribute('class') || '';
}
var classList = className.split(/\s+/);
if (!Array.isArray(classNames)) {
invariant(
classNames !== undefined,
'TestUtils.scryRenderedDOMComponentsWithClass expects a ' +
'className as a second argument.'
);
classNames = classNames.split(/\s+/);
}
return classNames.every(function(name) {
return classList.indexOf(name) !== -1;
});
@@ -399,6 +405,8 @@ var ShallowComponentWrapper = function(element) {
Object.assign(
ShallowComponentWrapper.prototype,
ReactCompositeComponent.Mixin, {
_constructComponent:
ReactCompositeComponent.Mixin._constructComponentWithoutOwner,
_instantiateReactComponent: function(element) {
return new NoopInternalComponent(element);
},
@@ -239,6 +239,18 @@ describe('ReactDefaultPerf', function() {
expect(summary).toEqual([]);
});
it('should not fail on input change events', function() {
var container = document.createElement('div');
var onChange = () => {};
var input = ReactDOM.render(
<input checked={true} onChange={onChange} />,
container
);
expectNoWaste(() => {
ReactTestUtils.Simulate.change(input);
});
});
it('should print a table after calling printOperations', function() {
var container = document.createElement('div');
var measurements = measure(() => {
+20
View File
@@ -47,6 +47,26 @@ describe('ReactTestUtils', function() {
]);
});
it('should shallow render a functional component', function() {
function SomeComponent() {
return (
<div>
<span className="child1" />
<span className="child2" />
</div>
);
}
var shallowRenderer = ReactTestUtils.createRenderer();
var result = shallowRenderer.render(<SomeComponent />);
expect(result.type).toBe('div');
expect(result.props.children).toEqual([
<span className="child1" />,
<span className="child2" />,
]);
});
it('should throw for invalid elements', function() {
var SomeComponent = React.createClass({
render: function() {