@samitouri / QOS-React-2 / commits / d914522ae4

A new prop type for React Components

Since we can use props to pass React Components, we should have a better way of validating these props than just `object`. Having a prop type `component` will allow us to safely assume that the passed prop IS a component.

Mouad Debbar committed Dec 20, 2013 at 18:07 UTC d914522ae4111512c7e0a604a6c5027600f5e966
2 files changed +133 -1
src/core/ReactPropTypes.js
+46 -1
@@ -18,6 +18,7 @@
18
19 "use strict";
20
21 +var ReactComponent = require('ReactComponent');
22 var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
23
24 var createObjectFrom = require('createObjectFrom');
@@ -83,12 +84,37 @@ var Props = {
84 oneOf: createEnumTypeChecker,
85 oneOfType: createUnionTypeChecker,
86
86 - instanceOf: createInstanceTypeChecker
87 + instanceOf: createInstanceTypeChecker,
88 +
89 + renderable: createRenderableTypeChecker()
90
91 };
92
93 var ANONYMOUS = '<<anonymous>>';
94
95 +function isRenderable(propValue) {
96 + switch(typeof propValue) {
97 + case 'number':
98 + case 'string':
99 + return true;
100 + case 'object':
101 + if (Array.isArray(propValue)) {
102 + return propValue.every(isRenderable);
103 + }
104 + if (ReactComponent.isValidComponent(propValue)) {
105 + return true;
106 + }
107 + for (var k in propValue) {
108 + if (!isRenderable(propValue[k])) {
109 + return false;
110 + }
111 + }
112 + return true;
113 + default:
114 + return false;
115 + }
116 +}
117 +
118 function createPrimitiveTypeChecker(expectedType) {
119 function validatePrimitiveType(
120 shouldThrow, propValue, propName, componentName, location
@@ -155,6 +181,25 @@ function createInstanceTypeChecker(expectedClass) {
181 return createChainableTypeChecker(validateInstanceType);
182 }
183
184 +function createRenderableTypeChecker() {
185 + function validateRenderableType(
186 + shouldThrow, propValue, propName, componentName, location
187 + ) {
188 + var isValid = isRenderable(propValue);
189 + if (!shouldThrow) {
190 + return isValid;
191 + }
192 + invariant(
193 + isValid,
194 + 'Invalid %s `%s` supplied to `%s`, expected a renderable prop.',
195 + ReactPropTypeLocationNames[location],
196 + propName,
197 + componentName
198 + );
199 + }
200 + return createChainableTypeChecker(validateRenderableType);
201 +}
202 +
203 function createChainableTypeChecker(validate) {
204 function checkType(
205 isRequired, shouldThrow, props, propName, componentName, location
src/core/__tests__/ReactPropTypes-test.js
+87
@@ -20,6 +20,7 @@
20 "use strict";
21
22 var Props = require('ReactPropTypes');
23 +var React = require('React');
24 var ReactPropTypeLocations = require('ReactPropTypeLocations');
25
26 function typeCheck(declaration, value) {
@@ -36,6 +37,12 @@ function typeCheck(declaration, value) {
37 );
38 }
39
40 +var MyComponent = React.createClass({
41 + render: function() {
42 + return <div />;
43 + }
44 +});
45 +
46 describe('Primitive Types', function() {
47 beforeEach(function() {
48 require('mock-modules').dumpCache();
@@ -206,4 +213,84 @@ describe('Union Types', function() {
213 expect(typeCheck(checker, 123)).not.toThrow();
214 });
215
216 + describe('React Component Types', function() {
217 + beforeEach(function() {
218 + require('mock-modules').dumpCache();
219 + });
220 +
221 + var myFunc = function() {};
222 +
223 + it('should throw for invalid values', function() {
224 + expect(typeCheck(Props.renderable, false)).toThrow(
225 + 'Invariant Violation: Invalid prop `testProp` supplied to ' +
226 + '`testComponent`, expected a renderable prop.'
227 + );
228 + expect(typeCheck(Props.renderable, myFunc)).toThrow(
229 + 'Invariant Violation: Invalid prop `testProp` supplied to ' +
230 + '`testComponent`, expected a renderable prop.'
231 + );
232 + expect(typeCheck(Props.renderable, {key: myFunc})).toThrow(
233 + 'Invariant Violation: Invalid prop `testProp` supplied to ' +
234 + '`testComponent`, expected a renderable prop.'
235 + );
236 + });
237 +
238 + it('should not throw for valid values', function() {
239 + // DOM component
240 + expect(typeCheck(Props.renderable, <div />)).not.toThrow();
241 + // Custom component
242 + expect(typeCheck(Props.renderable, <MyComponent />)).not.toThrow();
243 + // String
244 + expect(typeCheck(Props.renderable, 'Some string')).not.toThrow();
245 + // Empty array
246 + expect(typeCheck(Props.renderable, [])).not.toThrow();
247 + // Empty object
248 + expect(typeCheck(Props.renderable, {})).not.toThrow();
249 + // Array of renderable things
250 + expect(
251 + typeCheck(Props.renderable, [
252 + 123,
253 + 'Some string',
254 + <div />,
255 + ['Another string', [456], <span />, <MyComponent />],
256 + <MyComponent />
257 + ])
258 + ).not.toThrow();
259 + // Object of rendereable things
260 + expect(
261 + typeCheck(Props.renderable, {
262 + k0: 123,
263 + k1: 'Some string',
264 + k2: <div />,
265 + k3: {
266 + k30: <MyComponent />,
267 + k31: {k310: <a />},
268 + k32: 'Another string'
269 + }
270 + })
271 + ).not.toThrow();
272 + });
273 +
274 + it('should not throw for null/undefined if not required', function() {
275 + expect(typeCheck(Props.renderable, null)).not.toThrow();
276 + expect(typeCheck(Props.renderable, undefined)).not.toThrow();
277 + });
278 +
279 + it('should throw for missing required values', function() {
280 + expect(typeCheck(Props.renderable.isRequired, null)).toThrow(
281 + 'Invariant Violation: Required prop `testProp` was not specified in ' +
282 + '`testComponent`.'
283 + );
284 + expect(typeCheck(Props.renderable.isRequired, undefined)).toThrow(
285 + 'Invariant Violation: Required prop `testProp` was not specified in ' +
286 + '`testComponent`.'
287 + );
288 + });
289 +
290 + it('should accept empty array & object for required props', function() {
291 + expect(typeCheck(Props.renderable.isRequired, [])).not.toThrow();
292 + expect(typeCheck(Props.renderable.isRequired, {})).not.toThrow();
293 + });
294 + });
295 +
296 });