Skip to content

Commit

Permalink
Update PropTypes for ReactElement & ReactNode
Browse files Browse the repository at this point in the history
  • Loading branch information
zpao committed Oct 15, 2014
1 parent 2b22544 commit 770b579
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 44 deletions.
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

0 comments on commit 770b579

Please sign in to comment.