mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge branch '15.0.2-dev' into 15-stable
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 + '/**/*.*'];
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,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
@@ -11,4 +11,4 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = '15.0.1';
|
||||
module.exports = '15.0.2-alpha.4';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user