@samitouri / QOS-React-2 / commits / 71c10b9f45

Add PropTypes.ArrayOf and clean up ReactPropTypes.js

cpojer committed Feb 3, 2014 at 16:47 UTC 71c10b9f4509e275ac09240c957181a1122b961d
3 files changed +219 -109
docs/docs/05-reusable-components.md
+3
@@ -37,6 +37,9 @@ React.createClass({
37 // it as an enum.
38 optionalEnum: React.PropTypes.oneOf(['News','Photos']),
39
40 + // Expect an array of a certain propType
41 + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number)
42 +
43 // You can also declare that a prop is an instance of a class. This uses
44 // JS's instanceof operator.
45 someClass: React.PropTypes.instanceOf(SomeClass),
src/core/ReactPropTypes.js
+135 -109
@@ -84,6 +84,7 @@ var Props = {
84 shape: createShapeTypeChecker,
85 oneOf: createEnumTypeChecker,
86 oneOfType: createUnionTypeChecker,
87 + arrayOf: createArrayOfTypeChecker,
88
89 instanceOf: createInstanceTypeChecker,
90
@@ -96,16 +97,6 @@ var Props = {
97
98 var ANONYMOUS = '<<anonymous>>';
99
99 -function createAnyTypeChecker() {
100 - function validateAnyType(
101 - shouldThrow, propValue, propName, componentName, location
102 - ) {
103 - // always is valid
104 - return true;
105 - }
106 - return createChainableTypeChecker(validateAnyType);
107 -}
108 -
100 function isRenderable(propValue) {
101 switch(typeof propValue) {
102 case 'number':
@@ -138,24 +129,33 @@ function getPropType(propValue) {
129 return propType;
130 }
131
132 +function createAnyTypeChecker() {
133 + function validateAnyType(
134 + shouldWarn, propValue, propName, componentName, location
135 + ) {
136 + return true; // is always valid
137 + }
138 + return createChainableTypeChecker(validateAnyType);
139 +}
140 +
141 function createPrimitiveTypeChecker(expectedType) {
142 function validatePrimitiveType(
143 - shouldThrow, propValue, propName, componentName, location
143 + shouldWarn, propValue, propName, componentName, location
144 ) {
145 var propType = getPropType(propValue);
146 var isValid = propType === expectedType;
147 - if (!shouldThrow) {
148 - return isValid;
147 + if (shouldWarn) {
148 + warning(
149 + isValid,
150 + 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.',
151 + ReactPropTypeLocationNames[location],
152 + propName,
153 + propType,
154 + componentName,
155 + expectedType
156 + );
157 }
150 - warning(
151 - isValid,
152 - 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.',
153 - ReactPropTypeLocationNames[location],
154 - propName,
155 - propType,
156 - componentName,
157 - expectedType
158 - );
158 + return isValid;
159 }
160 return createChainableTypeChecker(validatePrimitiveType);
161 }
@@ -163,121 +163,170 @@ function createPrimitiveTypeChecker(expectedType) {
163 function createEnumTypeChecker(expectedValues) {
164 var expectedEnum = createObjectFrom(expectedValues);
165 function validateEnumType(
166 - shouldThrow, propValue, propName, componentName, location
166 + shouldWarn, propValue, propName, componentName, location
167 ) {
168 var isValid = expectedEnum[propValue];
169 - if (!shouldThrow) {
170 - return isValid;
169 + if (shouldWarn) {
170 + warning(
171 + isValid,
172 + 'Invalid %s `%s` supplied to `%s`, expected one of %s.',
173 + ReactPropTypeLocationNames[location],
174 + propName,
175 + componentName,
176 + JSON.stringify(Object.keys(expectedEnum))
177 + );
178 }
172 - warning(
173 - isValid,
174 - 'Invalid %s `%s` supplied to `%s`, expected one of %s.',
175 - ReactPropTypeLocationNames[location],
176 - propName,
177 - componentName,
178 - JSON.stringify(Object.keys(expectedEnum))
179 - );
179 + return isValid;
180 }
181 return createChainableTypeChecker(validateEnumType);
182 }
183
184 function createShapeTypeChecker(shapeTypes) {
185 function validateShapeType(
186 - shouldThrow, propValue, propName, componentName, location
186 + shouldWarn, propValue, propName, componentName, location
187 ) {
188 var propType = getPropType(propValue);
189 var isValid = propType === 'object';
190 if (isValid) {
191 for (var key in shapeTypes) {
192 var checker = shapeTypes[key];
193 - if (checker) {
194 - // It's going to throw an exception if it doesn't pass
195 - checker(propValue, key, componentName, location);
193 + if (checker && !checker(propValue, key, componentName, location)) {
194 + return false;
195 }
196 }
197 }
199 - if (!shouldThrow) {
200 - return isValid;
198 + if (shouldWarn) {
199 + warning(
200 + isValid,
201 + 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `object`.',
202 + ReactPropTypeLocationNames[location],
203 + propName,
204 + propType,
205 + componentName
206 + );
207 }
202 - warning(
203 - isValid,
204 - 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `object`.',
205 - ReactPropTypeLocationNames[location],
206 - propName,
207 - propType,
208 - componentName
209 - );
208 + return isValid;
209 }
210 return createChainableTypeChecker(validateShapeType);
211 }
212
213 function createInstanceTypeChecker(expectedClass) {
214 function validateInstanceType(
216 - shouldThrow, propValue, propName, componentName, location
215 + shouldWarn, propValue, propName, componentName, location
216 ) {
217 var isValid = propValue instanceof expectedClass;
219 - if (!shouldThrow) {
220 - return isValid;
218 + if (shouldWarn) {
219 + warning(
220 + isValid,
221 + 'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.',
222 + ReactPropTypeLocationNames[location],
223 + propName,
224 + componentName,
225 + expectedClass.name || ANONYMOUS
226 + );
227 }
222 - warning(
223 - isValid,
224 - 'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.',
225 - ReactPropTypeLocationNames[location],
226 - propName,
227 - componentName,
228 - expectedClass.name || ANONYMOUS
229 - );
228 + return isValid;
229 }
230 return createChainableTypeChecker(validateInstanceType);
231 }
232
233 +function createArrayOfTypeChecker(propTypeChecker) {
234 + function validateArrayType(
235 + shouldWarn, propValue, propName, componentName, location
236 + ) {
237 + var isValid = Array.isArray(propValue);
238 + if (isValid) {
239 + for (var i = 0; i < propValue.length; i++) {
240 + if (!propTypeChecker(propValue, i, componentName, location)) {
241 + return false;
242 + }
243 + }
244 + }
245 + if (shouldWarn) {
246 + warning(
247 + isValid,
248 + 'Invalid %s `%s` supplied to `%s`, expected an array.',
249 + ReactPropTypeLocationNames[location],
250 + propName,
251 + componentName
252 + );
253 + }
254 + return isValid;
255 + }
256 + return createChainableTypeChecker(validateArrayType);
257 +}
258 +
259 function createRenderableTypeChecker() {
260 function validateRenderableType(
236 - shouldThrow, propValue, propName, componentName, location
261 + shouldWarn, propValue, propName, componentName, location
262 ) {
263 var isValid = isRenderable(propValue);
239 - if (!shouldThrow) {
240 - return isValid;
264 + if (shouldWarn) {
265 + warning(
266 + isValid,
267 + 'Invalid %s `%s` supplied to `%s`, expected a renderable prop.',
268 + ReactPropTypeLocationNames[location],
269 + propName,
270 + componentName
271 + );
272 }
242 - warning(
243 - isValid,
244 - 'Invalid %s `%s` supplied to `%s`, expected a renderable prop.',
245 - ReactPropTypeLocationNames[location],
246 - propName,
247 - componentName
248 - );
273 + return isValid;
274 }
275 return createChainableTypeChecker(validateRenderableType);
276 }
277
278 function createComponentTypeChecker() {
279 function validateComponentType(
255 - shouldThrow, propValue, propName, componentName, location
280 + shouldWarn, propValue, propName, componentName, location
281 ) {
282 var isValid = ReactComponent.isValidComponent(propValue);
258 - if (!shouldThrow) {
259 - return isValid;
283 + if (shouldWarn) {
284 + warning(
285 + isValid,
286 + 'Invalid %s `%s` supplied to `%s`, expected a React component.',
287 + ReactPropTypeLocationNames[location],
288 + propName,
289 + componentName
290 + );
291 + }
292 + return isValid;
293 + }
294 + return createChainableTypeChecker(validateComponentType);
295 +}
296 +
297 +function createUnionTypeChecker(arrayOfValidators) {
298 + return function(props, propName, componentName, location) {
299 + var isValid = false;
300 + for (var ii = 0; ii < arrayOfValidators.length; ii++) {
301 + var validate = arrayOfValidators[ii];
302 + if (typeof validate.weak === 'function') {
303 + validate = validate.weak;
304 + }
305 + if (validate(props, propName, componentName, location)) {
306 + isValid = true;
307 + break;
308 + }
309 }
310 warning(
311 isValid,
263 - 'Invalid %s `%s` supplied to `%s`, expected a React component.',
312 + 'Invalid %s `%s` supplied to `%s`.',
313 ReactPropTypeLocationNames[location],
314 propName,
266 - componentName
315 + componentName || ANONYMOUS
316 );
268 - }
269 - return createChainableTypeChecker(validateComponentType);
317 + return isValid;
318 + };
319 }
320
321 function createChainableTypeChecker(validate) {
322 function checkType(
274 - isRequired, shouldThrow, props, propName, componentName, location
323 + isRequired, shouldWarn, props, propName, componentName, location
324 ) {
325 var propValue = props[propName];
326 if (propValue != null) {
327 // Only validate if there is a value to check.
328 return validate(
280 - shouldThrow,
329 + shouldWarn,
330 propValue,
331 propName,
332 componentName || ANONYMOUS,
@@ -285,16 +334,16 @@ function createChainableTypeChecker(validate) {
334 );
335 } else {
336 var isValid = !isRequired;
288 - if (!shouldThrow) {
289 - return isValid;
337 + if (shouldWarn) {
338 + warning(
339 + isValid,
340 + 'Required %s `%s` was not specified in `%s`.',
341 + ReactPropTypeLocationNames[location],
342 + propName,
343 + componentName || ANONYMOUS
344 + );
345 }
291 - warning(
292 - isValid,
293 - 'Required %s `%s` was not specified in `%s`.',
294 - ReactPropTypeLocationNames[location],
295 - propName,
296 - componentName || ANONYMOUS
297 - );
346 + return isValid;
347 }
348 }
349
@@ -307,27 +356,4 @@ function createChainableTypeChecker(validate) {
356 return checker;
357 }
358
310 -function createUnionTypeChecker(arrayOfValidators) {
311 - return function(props, propName, componentName, location) {
312 - var isValid = false;
313 - for (var ii = 0; ii < arrayOfValidators.length; ii++) {
314 - var validate = arrayOfValidators[ii];
315 - if (typeof validate.weak === 'function') {
316 - validate = validate.weak;
317 - }
318 - if (validate(props, propName, componentName, location)) {
319 - isValid = true;
320 - break;
321 - }
322 - }
323 - warning(
324 - isValid,
325 - 'Invalid %s `%s` supplied to `%s`.',
326 - ReactPropTypeLocationNames[location],
327 - propName,
328 - componentName || ANONYMOUS
329 - );
330 - };
331 -}
332 -
359 module.exports = Props;
src/core/__tests__/ReactPropTypes-test.js
+81
@@ -287,6 +287,87 @@ describe('Shape Types', function() {
287 });
288 });
289
290 +describe('ArrayOf Type', function() {
291 + beforeEach(function() {
292 + require('mock-modules').dumpCache();
293 + mocks = require('mocks');
294 + warn = console.warn;
295 + console.warn = mocks.getMockFunction();
296 + });
297 +
298 + afterEach(function() {
299 + console.warn = warn;
300 + });
301 +
302 + it('should support the arrayOf propType', function() {
303 + typeCheck(Props.arrayOf(Props.number), [1, 2, 3]);
304 + typeCheck(Props.arrayOf(Props.string), ['a', 'b', 'c']);
305 + typeCheck(Props.arrayOf(Props.oneOf(['a', 'b'])), ['a', 'b']);
306 +
307 + // No warnings should have been logged.
308 + expect(console.warn.mock.calls.length).toBe(0);
309 + });
310 +
311 + it('should support arrayOf with complex types', function() {
312 + typeCheck(Props.arrayOf(Props.shape({
313 + a: Props.number.isRequired
314 + })), [{a: 1}, {a: 2}]);
315 +
316 + function Thing() {}
317 + typeCheck(
318 + Props.arrayOf(Props.instanceOf(Thing)),
319 + [new Thing(), new Thing()]
320 + );
321 +
322 + // No warnings should have been logged.
323 + expect(console.warn.mock.calls.length).toBe(0);
324 + });
325 +
326 + it('should warn with invalid items in the array', function() {
327 + typeCheck(Props.arrayOf(Props.number), [1, 2, 'b']);
328 +
329 + expect(console.warn.mock.calls.length).toBe(1);
330 +
331 + expect(console.warn.mock.calls[0][0]).toBe(
332 + 'Warning: Invalid prop `2` of type `string` supplied to ' +
333 + '`testComponent`, expected `number`.'
334 + );
335 + });
336 +
337 + it('should warn with invalid complex types', function() {
338 + function Thing() {}
339 + var name = Thing.name || '<<anonymous>>';
340 +
341 + typeCheck(
342 + Props.arrayOf(Props.instanceOf(Thing)),
343 + [new Thing(), 'xyz']
344 + );
345 +
346 + expect(console.warn.mock.calls.length).toBe(1);
347 +
348 + expect(console.warn.mock.calls[0][0]).toBe(
349 + 'Warning: Invalid prop `1` supplied to ' +
350 + '`testComponent`, expected instance of `' + name + '`.'
351 + );
352 + });
353 +
354 + it('should warn when passed something other than an array', function() {
355 + typeCheck(Props.arrayOf(Props.number), {'0': 'maybe-array', length: 1});
356 + typeCheck(Props.arrayOf(Props.number), 123);
357 + typeCheck(Props.arrayOf(Props.number), 'string');
358 +
359 + expect(console.warn.mock.calls.length).toBe(3);
360 +
361 + var message = 'Warning: Invalid prop `testProp` supplied to ' +
362 + '`testComponent`, expected an array.';
363 +
364 + expect(console.warn.mock.calls[0][0]).toBe(message);
365 + expect(console.warn.mock.calls[1][0]).toBe(message);
366 + expect(console.warn.mock.calls[2][0]).toBe(message);
367 + });
368 +
369 +});
370 +
371 describe('Instance Types', function() {
372 beforeEach(function() {
373 require('mock-modules').dumpCache();