You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
14 KiB
461 lines
14 KiB
/** |
|
* Copyright 2014-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. |
|
* |
|
* @providesModule ReactElementValidator |
|
*/ |
|
|
|
/** |
|
* ReactElementValidator provides a wrapper around a element factory |
|
* which validates the props passed to the element. This is intended to be |
|
* used only in DEV and could be replaced by a static type checker for languages |
|
* that support it. |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var ReactElement = require("./ReactElement"); |
|
var ReactFragment = require("./ReactFragment"); |
|
var ReactPropTypeLocations = require("./ReactPropTypeLocations"); |
|
var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); |
|
var ReactCurrentOwner = require("./ReactCurrentOwner"); |
|
var ReactNativeComponent = require("./ReactNativeComponent"); |
|
|
|
var getIteratorFn = require("./getIteratorFn"); |
|
var invariant = require("./invariant"); |
|
var warning = require("./warning"); |
|
|
|
function getDeclarationErrorAddendum() { |
|
if (ReactCurrentOwner.current) { |
|
var name = ReactCurrentOwner.current.getName(); |
|
if (name) { |
|
return ' Check the render method of `' + name + '`.'; |
|
} |
|
} |
|
return ''; |
|
} |
|
|
|
/** |
|
* Warn if there's no key explicitly set on dynamic arrays of children or |
|
* object keys are not valid. This allows us to keep track of children between |
|
* updates. |
|
*/ |
|
var ownerHasKeyUseWarning = {}; |
|
|
|
var loggedTypeFailures = {}; |
|
|
|
var NUMERIC_PROPERTY_REGEX = /^\d+$/; |
|
|
|
/** |
|
* Gets the instance's name for use in warnings. |
|
* |
|
* @internal |
|
* @return {?string} Display name or undefined |
|
*/ |
|
function getName(instance) { |
|
var publicInstance = instance && instance.getPublicInstance(); |
|
if (!publicInstance) { |
|
return undefined; |
|
} |
|
var constructor = publicInstance.constructor; |
|
if (!constructor) { |
|
return undefined; |
|
} |
|
return constructor.displayName || constructor.name || undefined; |
|
} |
|
|
|
/** |
|
* Gets the current owner's displayName for use in warnings. |
|
* |
|
* @internal |
|
* @return {?string} Display name or undefined |
|
*/ |
|
function getCurrentOwnerDisplayName() { |
|
var current = ReactCurrentOwner.current; |
|
return ( |
|
current && getName(current) || undefined |
|
); |
|
} |
|
|
|
/** |
|
* Warn if the element doesn't have an explicit key assigned to it. |
|
* This element is in an array. The array could grow and shrink or be |
|
* reordered. All children that haven't already been validated are required to |
|
* have a "key" property assigned to it. |
|
* |
|
* @internal |
|
* @param {ReactElement} element Element that requires a key. |
|
* @param {*} parentType element's parent's type. |
|
*/ |
|
function validateExplicitKey(element, parentType) { |
|
if (element._store.validated || element.key != null) { |
|
return; |
|
} |
|
element._store.validated = true; |
|
|
|
warnAndMonitorForKeyUse( |
|
'Each child in an array or iterator should have a unique "key" prop.', |
|
element, |
|
parentType |
|
); |
|
} |
|
|
|
/** |
|
* Warn if the key is being defined as an object property but has an incorrect |
|
* value. |
|
* |
|
* @internal |
|
* @param {string} name Property name of the key. |
|
* @param {ReactElement} element Component that requires a key. |
|
* @param {*} parentType element's parent's type. |
|
*/ |
|
function validatePropertyKey(name, element, parentType) { |
|
if (!NUMERIC_PROPERTY_REGEX.test(name)) { |
|
return; |
|
} |
|
warnAndMonitorForKeyUse( |
|
'Child objects should have non-numeric keys so ordering is preserved.', |
|
element, |
|
parentType |
|
); |
|
} |
|
|
|
/** |
|
* Shared warning and monitoring code for the key warnings. |
|
* |
|
* @internal |
|
* @param {string} message The base warning that gets output. |
|
* @param {ReactElement} element Component that requires a key. |
|
* @param {*} parentType element's parent's type. |
|
*/ |
|
function warnAndMonitorForKeyUse(message, element, parentType) { |
|
var ownerName = getCurrentOwnerDisplayName(); |
|
var parentName = typeof parentType === 'string' ? |
|
parentType : parentType.displayName || parentType.name; |
|
|
|
var useName = ownerName || parentName; |
|
var memoizer = ownerHasKeyUseWarning[message] || ( |
|
(ownerHasKeyUseWarning[message] = {}) |
|
); |
|
if (memoizer.hasOwnProperty(useName)) { |
|
return; |
|
} |
|
memoizer[useName] = true; |
|
|
|
var parentOrOwnerAddendum = |
|
ownerName ? (" Check the render method of " + ownerName + ".") : |
|
parentName ? (" Check the React.render call using <" + parentName + ">.") : |
|
''; |
|
|
|
// Usually the current owner is the offender, but if it accepts children as a |
|
// property, it may be the creator of the child that's responsible for |
|
// assigning it a key. |
|
var childOwnerAddendum = ''; |
|
if (element && |
|
element._owner && |
|
element._owner !== ReactCurrentOwner.current) { |
|
// Name of the component that originally created this child. |
|
var childOwnerName = getName(element._owner); |
|
|
|
childOwnerAddendum = (" It was passed a child from " + childOwnerName + "."); |
|
} |
|
|
|
("production" !== process.env.NODE_ENV ? warning( |
|
false, |
|
message + '%s%s See http://fb.me/react-warning-keys for more information.', |
|
parentOrOwnerAddendum, |
|
childOwnerAddendum |
|
) : null); |
|
} |
|
|
|
/** |
|
* Ensure that every element either is passed in a static location, in an |
|
* array with an explicit keys property defined, or in an object literal |
|
* with valid key property. |
|
* |
|
* @internal |
|
* @param {ReactNode} node Statically passed child of any type. |
|
* @param {*} parentType node's parent's type. |
|
*/ |
|
function validateChildKeys(node, parentType) { |
|
if (Array.isArray(node)) { |
|
for (var i = 0; i < node.length; i++) { |
|
var child = node[i]; |
|
if (ReactElement.isValidElement(child)) { |
|
validateExplicitKey(child, parentType); |
|
} |
|
} |
|
} else if (ReactElement.isValidElement(node)) { |
|
// This element was passed in a valid location. |
|
node._store.validated = true; |
|
} else if (node) { |
|
var iteratorFn = getIteratorFn(node); |
|
// Entry iterators provide implicit keys. |
|
if (iteratorFn) { |
|
if (iteratorFn !== node.entries) { |
|
var iterator = iteratorFn.call(node); |
|
var step; |
|
while (!(step = iterator.next()).done) { |
|
if (ReactElement.isValidElement(step.value)) { |
|
validateExplicitKey(step.value, parentType); |
|
} |
|
} |
|
} |
|
} else if (typeof node === 'object') { |
|
var fragment = ReactFragment.extractIfFragment(node); |
|
for (var key in fragment) { |
|
if (fragment.hasOwnProperty(key)) { |
|
validatePropertyKey(key, fragment[key], parentType); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Assert that the props are valid |
|
* |
|
* @param {string} componentName Name of the component for error messages. |
|
* @param {object} propTypes Map of prop name to a ReactPropType |
|
* @param {object} props |
|
* @param {string} location e.g. "prop", "context", "child context" |
|
* @private |
|
*/ |
|
function checkPropTypes(componentName, propTypes, props, location) { |
|
for (var propName in propTypes) { |
|
if (propTypes.hasOwnProperty(propName)) { |
|
var error; |
|
// Prop type validation may throw. In case they do, we don't want to |
|
// fail the render phase where it didn't fail before. So we log it. |
|
// After these have been cleaned up, we'll let them throw. |
|
try { |
|
// This is intentionally an invariant that gets caught. It's the same |
|
// behavior as without this statement except with a better message. |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
typeof propTypes[propName] === 'function', |
|
'%s: %s type `%s` is invalid; it must be a function, usually from ' + |
|
'React.PropTypes.', |
|
componentName || 'React class', |
|
ReactPropTypeLocationNames[location], |
|
propName |
|
) : invariant(typeof propTypes[propName] === 'function')); |
|
error = propTypes[propName](props, propName, componentName, location); |
|
} catch (ex) { |
|
error = ex; |
|
} |
|
if (error instanceof Error && !(error.message in loggedTypeFailures)) { |
|
// Only monitor this failure once because there tends to be a lot of the |
|
// same error. |
|
loggedTypeFailures[error.message] = true; |
|
|
|
var addendum = getDeclarationErrorAddendum(this); |
|
("production" !== process.env.NODE_ENV ? warning(false, 'Failed propType: %s%s', error.message, addendum) : null); |
|
} |
|
} |
|
} |
|
} |
|
|
|
var warnedPropsMutations = {}; |
|
|
|
/** |
|
* Warn about mutating props when setting `propName` on `element`. |
|
* |
|
* @param {string} propName The string key within props that was set |
|
* @param {ReactElement} element |
|
*/ |
|
function warnForPropsMutation(propName, element) { |
|
var type = element.type; |
|
var elementName = typeof type === 'string' ? type : type.displayName; |
|
var ownerName = element._owner ? |
|
element._owner.getPublicInstance().constructor.displayName : null; |
|
|
|
var warningKey = propName + '|' + elementName + '|' + ownerName; |
|
if (warnedPropsMutations.hasOwnProperty(warningKey)) { |
|
return; |
|
} |
|
warnedPropsMutations[warningKey] = true; |
|
|
|
var elementInfo = ''; |
|
if (elementName) { |
|
elementInfo = ' <' + elementName + ' />'; |
|
} |
|
var ownerInfo = ''; |
|
if (ownerName) { |
|
ownerInfo = ' The element was created by ' + ownerName + '.'; |
|
} |
|
|
|
("production" !== process.env.NODE_ENV ? warning( |
|
false, |
|
'Don\'t set .props.%s of the React component%s. Instead, specify the ' + |
|
'correct value when initially creating the element or use ' + |
|
'React.cloneElement to make a new element with updated props.%s', |
|
propName, |
|
elementInfo, |
|
ownerInfo |
|
) : null); |
|
} |
|
|
|
// Inline Object.is polyfill |
|
function is(a, b) { |
|
if (a !== a) { |
|
// NaN |
|
return b !== b; |
|
} |
|
if (a === 0 && b === 0) { |
|
// +-0 |
|
return 1 / a === 1 / b; |
|
} |
|
return a === b; |
|
} |
|
|
|
/** |
|
* Given an element, check if its props have been mutated since element |
|
* creation (or the last call to this function). In particular, check if any |
|
* new props have been added, which we can't directly catch by defining warning |
|
* properties on the props object. |
|
* |
|
* @param {ReactElement} element |
|
*/ |
|
function checkAndWarnForMutatedProps(element) { |
|
if (!element._store) { |
|
// Element was created using `new ReactElement` directly or with |
|
// `ReactElement.createElement`; skip mutation checking |
|
return; |
|
} |
|
|
|
var originalProps = element._store.originalProps; |
|
var props = element.props; |
|
|
|
for (var propName in props) { |
|
if (props.hasOwnProperty(propName)) { |
|
if (!originalProps.hasOwnProperty(propName) || |
|
!is(originalProps[propName], props[propName])) { |
|
warnForPropsMutation(propName, element); |
|
|
|
// Copy over the new value so that the two props objects match again |
|
originalProps[propName] = props[propName]; |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Given an element, validate that its props follow the propTypes definition, |
|
* provided by the type. |
|
* |
|
* @param {ReactElement} element |
|
*/ |
|
function validatePropTypes(element) { |
|
if (element.type == null) { |
|
// This has already warned. Don't throw. |
|
return; |
|
} |
|
// Extract the component class from the element. Converts string types |
|
// to a composite class which may have propTypes. |
|
// TODO: Validating a string's propTypes is not decoupled from the |
|
// rendering target which is problematic. |
|
var componentClass = ReactNativeComponent.getComponentClassForElement( |
|
element |
|
); |
|
var name = componentClass.displayName || componentClass.name; |
|
if (componentClass.propTypes) { |
|
checkPropTypes( |
|
name, |
|
componentClass.propTypes, |
|
element.props, |
|
ReactPropTypeLocations.prop |
|
); |
|
} |
|
if (typeof componentClass.getDefaultProps === 'function') { |
|
("production" !== process.env.NODE_ENV ? warning( |
|
componentClass.getDefaultProps.isReactClassApproved, |
|
'getDefaultProps is only used on classic React.createClass ' + |
|
'definitions. Use a static property named `defaultProps` instead.' |
|
) : null); |
|
} |
|
} |
|
|
|
var ReactElementValidator = { |
|
|
|
checkAndWarnForMutatedProps: checkAndWarnForMutatedProps, |
|
|
|
createElement: function(type, props, children) { |
|
// We warn in this case but don't throw. We expect the element creation to |
|
// succeed and there will likely be errors in render. |
|
("production" !== process.env.NODE_ENV ? warning( |
|
type != null, |
|
'React.createElement: type should not be null or undefined. It should ' + |
|
'be a string (for DOM elements) or a ReactClass (for composite ' + |
|
'components).' |
|
) : null); |
|
|
|
var element = ReactElement.createElement.apply(this, arguments); |
|
|
|
// The result can be nullish if a mock or a custom function is used. |
|
// TODO: Drop this when these are no longer allowed as the type argument. |
|
if (element == null) { |
|
return element; |
|
} |
|
|
|
for (var i = 2; i < arguments.length; i++) { |
|
validateChildKeys(arguments[i], type); |
|
} |
|
|
|
validatePropTypes(element); |
|
|
|
return element; |
|
}, |
|
|
|
createFactory: function(type) { |
|
var validatedFactory = ReactElementValidator.createElement.bind( |
|
null, |
|
type |
|
); |
|
// Legacy hook TODO: Warn if this is accessed |
|
validatedFactory.type = type; |
|
|
|
if ("production" !== process.env.NODE_ENV) { |
|
try { |
|
Object.defineProperty( |
|
validatedFactory, |
|
'type', |
|
{ |
|
enumerable: false, |
|
get: function() { |
|
("production" !== process.env.NODE_ENV ? warning( |
|
false, |
|
'Factory.type is deprecated. Access the class directly ' + |
|
'before passing it to createFactory.' |
|
) : null); |
|
Object.defineProperty(this, 'type', { |
|
value: type |
|
}); |
|
return type; |
|
} |
|
} |
|
); |
|
} catch (x) { |
|
// IE will fail on defineProperty (es5-shim/sham too) |
|
} |
|
} |
|
|
|
|
|
return validatedFactory; |
|
}, |
|
|
|
cloneElement: function(element, props, children) { |
|
var newElement = ReactElement.cloneElement.apply(this, arguments); |
|
for (var i = 2; i < arguments.length; i++) { |
|
validateChildKeys(arguments[i], newElement.type); |
|
} |
|
validatePropTypes(newElement); |
|
return newElement; |
|
} |
|
|
|
}; |
|
|
|
module.exports = ReactElementValidator;
|
|
|