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();