@samitouri / QOS-React-2 / commits / 79b9d5af62

Introduce 'statics' for class specification

This moves the static properties off of the root object and into a 'statics' property. Then they are made available on the constructor so they can be used directly and in mixins.

Marshall Roch committed Dec 2, 2013 at 17:20 UTC 79b9d5af627a6f0e5c6c3496d7e3161df871f899
2 files changed +309 -94
src/core/ReactCompositeComponent.js
+130 -57
@@ -85,38 +85,39 @@ var SpecPolicy = keyMirror({
85 */
86 var ReactCompositeComponentInterface = {
87
88 - /**
89 - * An array of Mixin objects to include when defining your component.
90 - *
91 - * @type {array}
92 - * @optional
93 - */
94 - mixins: SpecPolicy.DEFINE_MANY,
95 -
96 - /**
97 - * Definition of prop types for this component.
98 - *
99 - * @type {object}
100 - * @optional
101 - */
102 - propTypes: SpecPolicy.DEFINE_MANY_MERGED,
103 -
104 - /**
105 - * Definition of context types for this component.
106 - *
107 - * @type {object}
108 - * @optional
109 - */
110 - contextTypes: SpecPolicy.DEFINE_MANY_MERGED,
111 -
112 - /**
113 - * Definition of context types this component sets for its children.
114 - *
115 - * @type {object}
116 - * @optional
117 - */
118 - childContextTypes: SpecPolicy.DEFINE_MANY_MERGED,
119 -
88 + statics: {
89 + /**
90 + * An array of Mixin objects to include when defining your component.
91 + *
92 + * @type {array}
93 + * @optional
94 + */
95 + mixins: SpecPolicy.DEFINE_MANY,
96 +
97 + /**
98 + * Definition of prop types for this component.
99 + *
100 + * @type {object}
101 + * @optional
102 + */
103 + propTypes: SpecPolicy.DEFINE_MANY_MERGED,
104 +
105 + /**
106 + * Definition of context types for this component.
107 + *
108 + * @type {object}
109 + * @optional
110 + */
111 + contextTypes: SpecPolicy.DEFINE_MANY_MERGED,
112 +
113 + /**
114 + * Definition of context types this component sets for its children.
115 + *
116 + * @type {object}
117 + * @optional
118 + */
119 + childContextTypes: SpecPolicy.DEFINE_MANY_MERGED
120 + },
121
122 // ==== Definition methods ====
123
@@ -303,23 +304,30 @@ var ReactCompositeComponentInterface = {
304 };
305
306 /**
306 - * Mapping from class specification keys to special processing functions.
307 + * Mapping from class specification static keys to special processing functions.
308 + *
309 + * These are all defined inside the "statics" key in the class spec:
310 + *
311 + * React.createClass({
312 + * statics: {
313 + * mixins: [FooMixin]
314 + * },
315 + * render: function() {...}
316 + * })
317 *
318 * Although these are declared in the specification when defining classes
319 * using `React.createClass`, they will not be on the component's prototype.
320 */
311 -var RESERVED_SPEC_KEYS = {
312 - displayName: function(Constructor, displayName) {
313 - Constructor.displayName = displayName;
314 - },
315 - mixins: function(Constructor, mixins) {
321 +var RESERVED_STATIC_SPEC_KEYS = {
322 + mixins: function(ConvenienceConstructor, mixins) {
323 if (mixins) {
324 for (var i = 0; i < mixins.length; i++) {
318 - mixSpecIntoComponent(Constructor, mixins[i]);
325 + mixSpecIntoComponent(ConvenienceConstructor, mixins[i]);
326 }
327 }
328 },
322 - childContextTypes: function(Constructor, childContextTypes) {
329 + childContextTypes: function(ConvenienceConstructor, childContextTypes) {
330 + var Constructor = ConvenienceConstructor.componentConstructor;
331 validateTypeDef(
332 Constructor,
333 childContextTypes,
@@ -327,7 +335,8 @@ var RESERVED_SPEC_KEYS = {
335 );
336 Constructor.childContextTypes = childContextTypes;
337 },
330 - contextTypes: function(Constructor, contextTypes) {
338 + contextTypes: function(ConvenienceConstructor, contextTypes) {
339 + var Constructor = ConvenienceConstructor.componentConstructor;
340 validateTypeDef(
341 Constructor,
342 contextTypes,
@@ -335,7 +344,8 @@ var RESERVED_SPEC_KEYS = {
344 );
345 Constructor.contextTypes = contextTypes;
346 },
338 - propTypes: function(Constructor, propTypes) {
347 + propTypes: function(ConvenienceConstructor, propTypes) {
348 + var Constructor = ConvenienceConstructor.componentConstructor;
349 validateTypeDef(
350 Constructor,
351 propTypes,
@@ -345,6 +355,24 @@ var RESERVED_SPEC_KEYS = {
355 }
356 };
357
358 +/**
359 + * Mapping from class specification keys to special processing functions.
360 + *
361 + * Although these are declared in the specification when defining classes
362 + * using `React.createClass`, they will not be on the component's prototype.
363 + */
364 +var RESERVED_SPEC_KEYS = {
365 + // TODO: move this to RESERVED_STATIC_SPEC_KEYS, but this requires also
366 + // changing the JSX transform.
367 + displayName: function(ConvenienceConstructor, displayName) {
368 + ConvenienceConstructor.componentConstructor.displayName = displayName;
369 + },
370 +
371 + statics: function(ConvenienceConstructor, statics) {
372 + mixStaticSpecIntoComponent(ConvenienceConstructor, statics);
373 + }
374 +};
375 +
376 function validateTypeDef(Constructor, typeDef, location) {
377 for (var propName in typeDef) {
378 if (typeDef.hasOwnProperty(propName)) {
@@ -360,11 +388,13 @@ function validateTypeDef(Constructor, typeDef, location) {
388 }
389 }
390
363 -function validateMethodOverride(proto, name) {
364 - var specPolicy = ReactCompositeComponentInterface[name];
391 +function validateMethodOverride(proto, name, isStatic) {
392 + var specPolicy = isStatic ?
393 + ReactCompositeComponentInterface.statics[name] :
394 + ReactCompositeComponentInterface[name];
395
396 // Disallow overriding of base class methods unless explicitly allowed.
367 - if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
397 + if (!isStatic && ReactCompositeComponentMixin.hasOwnProperty(name)) {
398 invariant(
399 specPolicy === SpecPolicy.OVERRIDE_BASE,
400 'ReactCompositeComponentInterface: You are attempting to override ' +
@@ -387,7 +417,6 @@ function validateMethodOverride(proto, name) {
417 }
418 }
419
390 -
420 function validateLifeCycleOnReplaceState(instance) {
421 var compositeLifeCycleState = instance._compositeLifeCycleState;
422 invariant(
@@ -407,17 +436,27 @@ function validateLifeCycleOnReplaceState(instance) {
436 * Custom version of `mixInto` which handles policy validation and reserved
437 * specification keys when building `ReactCompositeComponent` classses.
438 */
410 -function mixSpecIntoComponent(Constructor, spec) {
439 +function mixSpecIntoComponent(ConvenienceConstructor, spec) {
440 + var Constructor = ConvenienceConstructor.componentConstructor;
441 var proto = Constructor.prototype;
442 for (var name in spec) {
443 var property = spec[name];
444 if (!spec.hasOwnProperty(name) || !property) {
445 continue;
446 }
417 - validateMethodOverride(proto, name);
447 +
448 + validateMethodOverride(proto, name, false);
449
450 if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
420 - RESERVED_SPEC_KEYS[name](Constructor, property);
451 + RESERVED_SPEC_KEYS[name](ConvenienceConstructor, property);
452 + } else if (RESERVED_STATIC_SPEC_KEYS.hasOwnProperty(name)) {
453 + if (__DEV__) {
454 + console.warn(
455 + 'createClass(...): `' + name + '` is now a static property and ' +
456 + 'should be defined inside "statics".'
457 + );
458 + }
459 + RESERVED_STATIC_SPEC_KEYS[name](ConvenienceConstructor, property);
460 } else {
461 // Setup methods on prototype:
462 // The following member methods should not be automatically bound:
@@ -457,6 +496,38 @@ function mixSpecIntoComponent(Constructor, spec) {
496 }
497 }
498
499 +function mixStaticSpecIntoComponent(ConvenienceConstructor, statics) {
500 + if (!statics) {
501 + return;
502 + }
503 + for (var name in statics) {
504 + var property = statics[name];
505 + if (!statics.hasOwnProperty(name) || !property) {
506 + return;
507 + }
508 +
509 + validateMethodOverride(ConvenienceConstructor, name, true);
510 +
511 + if (RESERVED_STATIC_SPEC_KEYS.hasOwnProperty(name)) {
512 + RESERVED_STATIC_SPEC_KEYS[name](ConvenienceConstructor, property);
513 + } else {
514 + var isInherited = name in ConvenienceConstructor;
515 + var result = property;
516 + if (isInherited) {
517 + var existingProperty = ConvenienceConstructor[name];
518 + if (ReactCompositeComponentInterface.statics[name] ===
519 + SpecPolicy.DEFINE_MANY_MERGED) {
520 + result = createMergedResultFunction(existingProperty, property);
521 + } else {
522 + result = createChainedFunction(existingProperty, property);
523 + }
524 + }
525 + ConvenienceConstructor[name] = result;
526 + ConvenienceConstructor.componentConstructor[name] = result;
527 + }
528 + }
529 +}
530 +
531 /**
532 * Merge two objects, but throw if both contain the same key.
533 *
@@ -1172,7 +1243,16 @@ var ReactCompositeComponent = {
1243 var Constructor = function() {};
1244 Constructor.prototype = new ReactCompositeComponentBase();
1245 Constructor.prototype.constructor = Constructor;
1175 - mixSpecIntoComponent(Constructor, spec);
1246 +
1247 + var ConvenienceConstructor = function(props, children) {
1248 + var instance = new Constructor();
1249 + instance.construct.apply(instance, arguments);
1250 + return instance;
1251 + };
1252 + ConvenienceConstructor.componentConstructor = Constructor;
1253 + ConvenienceConstructor.originalSpec = spec;
1254 +
1255 + mixSpecIntoComponent(ConvenienceConstructor, spec);
1256
1257 invariant(
1258 Constructor.prototype.render,
@@ -1197,13 +1277,6 @@ var ReactCompositeComponent = {
1277 }
1278 }
1279
1200 - var ConvenienceConstructor = function(props, children) {
1201 - var instance = new Constructor();
1202 - instance.construct.apply(instance, arguments);
1203 - return instance;
1204 - };
1205 - ConvenienceConstructor.componentConstructor = Constructor;
1206 - ConvenienceConstructor.originalSpec = spec;
1280 return ConvenienceConstructor;
1281 },
1282
src/core/__tests__/ReactCompositeComponent-test.js
+179 -37
@@ -235,7 +235,9 @@ describe('ReactCompositeComponent', function() {
235
236 it('should normalize props with default values', function() {
237 var Component = React.createClass({
238 - propTypes: {key: ReactPropTypes.string.isRequired},
238 + statics: {
239 + propTypes: {key: ReactPropTypes.string.isRequired}
240 + },
241 getDefaultProps: function() {
242 return {key: 'testKey'};
243 },
@@ -262,7 +264,9 @@ describe('ReactCompositeComponent', function() {
264
265 it('should check default prop values', function() {
266 var Component = React.createClass({
265 - propTypes: {key: ReactPropTypes.string.isRequired},
267 + statics: {
268 + propTypes: {key: ReactPropTypes.string.isRequired}
269 + },
270 getDefaultProps: function() {
271 return {key: null};
272 },
@@ -282,8 +286,10 @@ describe('ReactCompositeComponent', function() {
286
287 it('should check declared prop types', function() {
288 var Component = React.createClass({
285 - propTypes: {
286 - key: ReactPropTypes.string.isRequired
289 + statics: {
290 + propTypes: {
291 + key: ReactPropTypes.string.isRequired
292 + }
293 },
294 render: function() {
295 return <span>{this.props.key}</span>;
@@ -313,8 +319,10 @@ describe('ReactCompositeComponent', function() {
319 expect(function() {
320 React.createClass({
321 displayName: 'Component',
316 - propTypes: {
317 - key: null
322 + statics: {
323 + propTypes: {
324 + key: null
325 + }
326 },
327 render: function() {
328 return <span>{this.props.key}</span>;
@@ -330,8 +338,10 @@ describe('ReactCompositeComponent', function() {
338 expect(function() {
339 React.createClass({
340 displayName: 'Component',
333 - contextTypes: {
334 - key: null
341 + statics: {
342 + contextTypes: {
343 + key: null
344 + }
345 },
346 render: function() {
347 return <span>{this.props.key}</span>;
@@ -347,8 +357,10 @@ describe('ReactCompositeComponent', function() {
357 expect(function() {
358 React.createClass({
359 displayName: 'Component',
350 - childContextTypes: {
351 - key: null
360 + statics: {
361 + childContextTypes: {
362 + key: null
363 + }
364 },
365 render: function() {
366 return <span>{this.props.key}</span>;
@@ -416,7 +428,9 @@ describe('ReactCompositeComponent', function() {
428 }
429 };
430 var Component = React.createClass({
419 - mixins: [Mixin],
431 + statics: {
432 + mixins: [Mixin]
433 + },
434 getInitialState: function() {
435 return {component: true};
436 },
@@ -437,7 +451,9 @@ describe('ReactCompositeComponent', function() {
451 }
452 };
453 var Component = React.createClass({
440 - mixins: [Mixin],
454 + statics: {
455 + mixins: [Mixin]
456 + },
457 getInitialState: function() {
458 return {x: true};
459 },
@@ -461,7 +477,9 @@ describe('ReactCompositeComponent', function() {
477 }
478 };
479 var Component = React.createClass({
464 - mixins: [Mixin],
480 + statics: {
481 + mixins: [Mixin]
482 + },
483 getInitialState: function() {
484 return {x: true};
485 },
@@ -548,14 +566,59 @@ describe('ReactCompositeComponent', function() {
566 }
567 });
568
569 + it('should warn when using deprecated non-static spec keys', function() {
570 + var warn = console.warn;
571 + console.warn = mocks.getMockFunction();
572 + try {
573 + React.createClass({
574 + mixins: [{}],
575 + propTypes: {
576 + foo: ReactPropTypes.string
577 + },
578 + contextTypes: {
579 + foo: ReactPropTypes.string
580 + },
581 + childContextTypes: {
582 + foo: ReactPropTypes.string
583 + },
584 + render: function() {
585 + return <div />;
586 + }
587 + });
588 + expect(console.warn.mock.calls.length).toBe(4);
589 + expect(console.warn.mock.calls[0][0]).toBe(
590 + 'createClass(...): `mixins` is now a static property and should ' +
591 + 'be defined inside "statics".'
592 + );
593 + expect(console.warn.mock.calls[1][0]).toBe(
594 + 'createClass(...): `propTypes` is now a static property and should ' +
595 + 'be defined inside "statics".'
596 + );
597 + expect(console.warn.mock.calls[2][0]).toBe(
598 + 'createClass(...): `contextTypes` is now a static property and ' +
599 + 'should be defined inside "statics".'
600 + );
601 + expect(console.warn.mock.calls[3][0]).toBe(
602 + 'createClass(...): `childContextTypes` is now a static property and ' +
603 + 'should be defined inside "statics".'
604 + );
605 + } catch (e) {
606 + throw e;
607 + } finally {
608 + console.warn = warn;
609 + }
610 + });
611 +
612 it('should pass context', function() {
613 var childInstance = null;
614 var grandchildInstance = null;
615
616 var Parent = React.createClass({
556 - childContextTypes: {
557 - foo: ReactPropTypes.string,
558 - depth: ReactPropTypes.number
617 + statics: {
618 + childContextTypes: {
619 + foo: ReactPropTypes.string,
620 + depth: ReactPropTypes.number
621 + }
622 },
623
624 getChildContext: function() {
@@ -572,13 +635,15 @@ describe('ReactCompositeComponent', function() {
635 });
636
637 var Child = React.createClass({
575 - contextTypes: {
576 - foo: ReactPropTypes.string,
577 - depth: ReactPropTypes.number
578 - },
638 + statics: {
639 + contextTypes: {
640 + foo: ReactPropTypes.string,
641 + depth: ReactPropTypes.number
642 + },
643
580 - childContextTypes: {
581 - depth: ReactPropTypes.number
644 + childContextTypes: {
645 + depth: ReactPropTypes.number
646 + }
647 },
648
649 getChildContext: function() {
@@ -594,9 +659,11 @@ describe('ReactCompositeComponent', function() {
659 });
660
661 var Grandchild = React.createClass({
597 - contextTypes: {
598 - foo: ReactPropTypes.string,
599 - depth: ReactPropTypes.number
662 + statics: {
663 + contextTypes: {
664 + foo: ReactPropTypes.string,
665 + depth: ReactPropTypes.number
666 + }
667 },
668
669 render: function() {
@@ -612,8 +679,10 @@ describe('ReactCompositeComponent', function() {
679
680 it('should check context types', function() {
681 var Component = React.createClass({
615 - contextTypes: {
616 - foo: ReactPropTypes.string.isRequired
682 + statics: {
683 + contextTypes: {
684 + foo: ReactPropTypes.string.isRequired
685 + }
686 },
687
688 render: function() {
@@ -646,9 +715,11 @@ describe('ReactCompositeComponent', function() {
715
716 it('should check child context types', function() {
717 var Component = React.createClass({
649 - childContextTypes: {
650 - foo: ReactPropTypes.string.isRequired,
651 - bar: ReactPropTypes.number
718 + statics: {
719 + childContextTypes: {
720 + foo: ReactPropTypes.string.isRequired,
721 + bar: ReactPropTypes.number
722 + }
723 },
724
725 getChildContext: function() {
@@ -693,8 +764,10 @@ describe('ReactCompositeComponent', function() {
764
765 it('should filter out context not in contextTypes', function() {
766 var Component = React.createClass({
696 - contextTypes: {
697 - foo: ReactPropTypes.string
767 + statics: {
768 + contextTypes: {
769 + foo: ReactPropTypes.string
770 + }
771 },
772
773 render: function() {
@@ -716,9 +789,11 @@ describe('ReactCompositeComponent', function() {
789 var actualComponentDidUpdate;
790
791 var Parent = React.createClass({
719 - childContextTypes: {
720 - foo: ReactPropTypes.string.isRequired,
721 - bar: ReactPropTypes.string.isRequired
792 + statics: {
793 + childContextTypes: {
794 + foo: ReactPropTypes.string.isRequired,
795 + bar: ReactPropTypes.string.isRequired
796 + }
797 },
798
799 getChildContext: function() {
@@ -734,8 +809,10 @@ describe('ReactCompositeComponent', function() {
809 });
810
811 var Component = React.createClass({
737 - contextTypes: {
738 - foo: ReactPropTypes.string
812 + statics: {
813 + contextTypes: {
814 + foo: ReactPropTypes.string
815 + }
816 },
817
818 componentWillReceiveProps: function(nextProps, nextContext) {
@@ -769,4 +846,69 @@ describe('ReactCompositeComponent', function() {
846 expect(actualComponentWillUpdate).toEqual({foo: 'def'});
847 expect(actualComponentDidUpdate).toEqual({foo: 'abc'});
848 });
849 +
850 + it('should support statics', function() {
851 + var Component = React.createClass({
852 + statics: {
853 + abc: 'def'
854 + },
855 +
856 + render: function() {
857 + return <span />;
858 + }
859 + });
860 + var instance = <Component />;
861 + ReactTestUtils.renderIntoDocument(instance);
862 + expect(instance.constructor.abc).toBe('def');
863 + expect(Component.abc).toBe('def');
864 + });
865 +
866 + it('should support statics in mixins', function() {
867 + var Mixin = {
868 + statics: {
869 + foo: 'bar'
870 + }
871 + };
872 + var Component = React.createClass({
873 + mixins: [Mixin],
874 +
875 + statics: {
876 + abc: 'def'
877 + },
878 +
879 + render: function() {
880 + return <span />;
881 + }
882 + });
883 + var instance = <Component />;
884 + ReactTestUtils.renderIntoDocument(instance);
885 + expect(instance.constructor.foo).toBe('bar');
886 + expect(Component.foo).toBe('bar');
887 + expect(instance.constructor.abc).toBe('def');
888 + expect(Component.abc).toBe('def');
889 + });
890 +
891 + it("should throw if mixins override each others' statics", function() {
892 + expect(function() {
893 + var Mixin = {
894 + statics: {
895 + abc: 'foo'
896 + }
897 + };
898 + React.createClass({
899 + statics: {
900 + mixins: [Mixin],
901 + abc: 'bar'
902 + },
903 +
904 + render: function() {
905 + return <span />;
906 + }
907 + });
908 + }).toThrow(
909 + 'Invariant Violation: ReactCompositeComponentInterface: You are ' +
910 + 'attempting to define `abc` on your component more than once. This ' +
911 + 'conflict may be due to a mixin.'
912 + );
913 + });
914 });