Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions src/core/ReactPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
var ReactElement = require('ReactElement');
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');

var deprecated = require('deprecated');
var emptyFunction = require('emptyFunction');

/**
Expand Down Expand Up @@ -65,6 +66,9 @@ var emptyFunction = require('emptyFunction');

var ANONYMOUS = '<<anonymous>>';

var elementTypeChecker = createElementTypeChecker();
var nodeTypeChecker = createNodeChecker();

var ReactPropTypes = {
array: createPrimitiveTypeChecker('array'),
bool: createPrimitiveTypeChecker('boolean'),
Expand All @@ -75,13 +79,28 @@ var ReactPropTypes = {

any: createAnyTypeChecker(),
arrayOf: createArrayOfTypeChecker,
component: createComponentTypeChecker(),
element: elementTypeChecker,
instanceOf: createInstanceTypeChecker,
node: nodeTypeChecker,
objectOf: createObjectOfTypeChecker,
oneOf: createEnumTypeChecker,
oneOfType: createUnionTypeChecker,
renderable: createRenderableTypeChecker(),
shape: createShapeTypeChecker
shape: createShapeTypeChecker,

component: deprecated(
'React.PropTypes',
'component',
'element',
this,
elementTypeChecker
),
renderable: deprecated(
'React.PropTypes',
'renderable',
'node',
this,
nodeTypeChecker
)
};

function createChainableTypeChecker(validate) {
Expand Down Expand Up @@ -151,13 +170,13 @@ function createArrayOfTypeChecker(typeChecker) {
return createChainableTypeChecker(validate);
}

function createComponentTypeChecker() {
function createElementTypeChecker() {
function validate(props, propName, componentName, location) {
if (!ReactElement.isValidElement(props[propName])) {
var locationName = ReactPropTypeLocationNames[location];
return new Error(
`Invalid ${locationName} \`${propName}\` supplied to ` +
`\`${componentName}\`, expected a React component.`
`\`${componentName}\`, expected a ReactElement.`
);
}
}
Expand Down Expand Up @@ -238,13 +257,13 @@ function createUnionTypeChecker(arrayOfTypeCheckers) {
return createChainableTypeChecker(validate);
}

function createRenderableTypeChecker() {
function createNodeChecker() {
function validate(props, propName, componentName, location) {
if (!isRenderable(props[propName])) {
if (!isNode(props[propName])) {
var locationName = ReactPropTypeLocationNames[location];
return new Error(
`Invalid ${locationName} \`${propName}\` supplied to ` +
`\`${componentName}\`, expected a renderable prop.`
`\`${componentName}\`, expected a ReactNode.`
);
}
}
Expand Down Expand Up @@ -276,25 +295,22 @@ function createShapeTypeChecker(shapeTypes) {
return createChainableTypeChecker(validate, 'expected `object`');
}

function isRenderable(propValue) {
function isNode(propValue) {
switch(typeof propValue) {
// TODO: this was probably written with the assumption that we're not
// returning `this.props.component` directly from `render`. This is
// currently not supported but we should, to make it consistent.
case 'number':
case 'string':
return true;
case 'boolean':
return !propValue;
case 'object':
if (Array.isArray(propValue)) {
return propValue.every(isRenderable);
return propValue.every(isNode);
}
if (ReactElement.isValidElement(propValue)) {
return true;
}
for (var k in propValue) {
if (!isRenderable(propValue[k])) {
if (!isNode(propValue[k])) {
return false;
}
}
Expand Down
63 changes: 34 additions & 29 deletions src/core/__tests__/ReactPropTypes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ describe('ReactPropTypes', function() {
beforeEach(function() {
Component = React.createClass({
propTypes: {
label: PropTypes.component.isRequired
label: PropTypes.element.isRequired
},

render: function() {
Expand All @@ -235,16 +235,16 @@ describe('ReactPropTypes', function() {
});

it('should support components', () => {
typeCheckPass(PropTypes.component, <div />);
typeCheckPass(PropTypes.element, <div />);
});

it('should not support multiple components or scalar values', () => {
var message = 'Invalid prop `testProp` supplied to `testComponent`, ' +
'expected a React component.';
typeCheckFail(PropTypes.component, [<div />, <div />], message);
typeCheckFail(PropTypes.component, 123, message);
typeCheckFail(PropTypes.component, 'foo', message);
typeCheckFail(PropTypes.component, false, message);
'expected a ReactElement.';
typeCheckFail(PropTypes.element, [<div />, <div />], message);
typeCheckFail(PropTypes.element, 123, message);
typeCheckFail(PropTypes.element, 'foo', message);
typeCheckFail(PropTypes.element, false, message);
});

it('should be able to define a single child as label', () => {
Expand All @@ -262,13 +262,13 @@ describe('ReactPropTypes', function() {
});

it("should be implicitly optional and not warn without values", function() {
typeCheckPass(PropTypes.component, null);
typeCheckPass(PropTypes.component, undefined);
typeCheckPass(PropTypes.element, null);
typeCheckPass(PropTypes.element, undefined);
});

it("should warn for missing required values", function() {
typeCheckFail(PropTypes.component.isRequired, null, requiredMessage);
typeCheckFail(PropTypes.component.isRequired, undefined, requiredMessage);
typeCheckFail(PropTypes.element.isRequired, null, requiredMessage);
typeCheckFail(PropTypes.element.isRequired, undefined, requiredMessage);
});
});

Expand Down Expand Up @@ -349,20 +349,21 @@ describe('ReactPropTypes', function() {

it('should warn for invalid values', function() {
var failMessage = 'Invalid prop `testProp` supplied to ' +
'`testComponent`, expected a renderable prop.';
typeCheckFail(PropTypes.renderable, true, failMessage);
typeCheckFail(PropTypes.renderable, function() {}, failMessage);
typeCheckFail(PropTypes.renderable, {key: function() {}}, failMessage);
'`testComponent`, expected a ReactNode.';
typeCheckFail(PropTypes.node, true, failMessage);
typeCheckFail(PropTypes.node, function() {}, failMessage);
typeCheckFail(PropTypes.node, {key: function() {}}, failMessage);
});

it('should not warn for valid values', function() {
typeCheckPass(PropTypes.renderable, <div />);
typeCheckPass(PropTypes.renderable, false);
typeCheckPass(PropTypes.renderable, <MyComponent />);
typeCheckPass(PropTypes.renderable, 'Some string');
typeCheckPass(PropTypes.renderable, []);
typeCheckPass(PropTypes.renderable, {});
typeCheckPass(PropTypes.renderable, [
typeCheckPass(PropTypes.node, <div />);
typeCheckPass(PropTypes.node, false);
typeCheckPass(PropTypes.node, <MyComponent />);
typeCheckPass(PropTypes.node, 'Some string');
typeCheckPass(PropTypes.node, []);
typeCheckPass(PropTypes.node, {});

typeCheckPass(PropTypes.node, [
123,
'Some string',
<div />,
Expand All @@ -371,7 +372,7 @@ describe('ReactPropTypes', function() {
]);

// Object of rendereable things
typeCheckPass(PropTypes.renderable, {
typeCheckPass(PropTypes.node, {
k0: 123,
k1: 'Some string',
k2: <div />,
Expand All @@ -384,26 +385,30 @@ describe('ReactPropTypes', function() {
});

it('should not warn for null/undefined if not required', function() {
typeCheckPass(PropTypes.renderable, null);
typeCheckPass(PropTypes.renderable, undefined);
typeCheckPass(PropTypes.node, null);
typeCheckPass(PropTypes.node, undefined);
});

it('should warn for missing required values', function() {
typeCheckFail(
PropTypes.renderable.isRequired,
PropTypes.node.isRequired,
null,
'Required prop `testProp` was not specified in `testComponent`.'
);
typeCheckFail(
PropTypes.renderable.isRequired,
PropTypes.node.isRequired,
undefined,
'Required prop `testProp` was not specified in `testComponent`.'
);
});

it('should accept empty array & object for required props', function() {
it('should accept empty array for required props', function() {
typeCheckPass(PropTypes.node.isRequired, []);
});

it('should still work for deprecated typechecks', function() {
typeCheckPass(PropTypes.renderable, []);
typeCheckPass(PropTypes.renderable.isRequired, []);
typeCheckPass(PropTypes.renderable.isRequired, {});
});
});

Expand Down
4 changes: 3 additions & 1 deletion src/utils/deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ function deprecated(namespace, oldName, newName, ctx, fn) {
return fn.apply(ctx, arguments);
};
newFn.displayName = `${namespace}_${oldName}`;
return newFn;
// We need to make sure all properties of the original fn are copied over.
// In particular, this is needed to support PropTypes
return Object.assign(newFn, fn);
}

return fn;
Expand Down