Make ReactTextComponent properly injectable
ReactTextComponent's implementation is DOM-specific; instead of flattenChildren creating the ReactTextComponent instances, ReactNativeComponent now takes care of having ReactTextComponent injected and creating the component instance. I also renamed ReactTextComponent to ReactDOMTextComponent and moved it to browser/ui/ where it belongs. ReactDOMTextComponent no longer inherits directly from ReactComponent and instead implements construct and {mount,receive,unmount}Component directly. This diff removes `ReactTestUtils.isTextComponent` which should have previously never returned true when using public APIs. Test Plan: jest, use ballmer-peak example.
Ben Alpert committed
Oct 19, 2014 at 15:51 UTC
230115da9210f389e60f7eb0e402de1a06b5f269
14 files changed
+140
-113
docs/docs/10.4-test-utils.md
-8
@@ -75,14 +75,6 @@ boolean isCompositeComponentWithType(ReactComponent instance, function component
75
76
Returns true if `instance` is a composite component (created with `React.createClass()`) whose type is of a React `componentClass`.
77
78
-### isTextComponent
79
-
80
-```javascript
81
-boolean isTextComponent(ReactComponent instance)
82
-```
83
-
84
-Returns true if `instance` is a plain text component.
85
-
78
### findAllInRenderedTree
79
80
```javascript
src/browser/ui/React.js
+2
-2
@@ -22,6 +22,7 @@ var ReactElement = require('ReactElement');
22
var ReactElementValidator = require('ReactElementValidator');
23
var ReactDOM = require('ReactDOM');
24
var ReactDOMComponent = require('ReactDOMComponent');
25
+var ReactDOMTextComponent = require('ReactDOMTextComponent');
26
var ReactDefaultInjection = require('ReactDefaultInjection');
27
var ReactInstanceHandles = require('ReactInstanceHandles');
28
var ReactMount = require('ReactMount');
@@ -30,7 +31,6 @@ var ReactPerf = require('ReactPerf');
31
var ReactPropTypes = require('ReactPropTypes');
32
var ReactRef = require('ReactRef');
33
var ReactServerRendering = require('ReactServerRendering');
33
-var ReactTextComponent = require('ReactTextComponent');
34
35
var assign = require('Object.assign');
36
var deprecated = require('deprecated');
@@ -122,7 +122,7 @@ if (
122
InstanceHandles: ReactInstanceHandles,
123
Mount: ReactMount,
124
MultiChild: ReactMultiChild,
125
- TextComponent: ReactTextComponent
125
+ TextComponent: ReactDOMTextComponent
126
});
127
}
128
src/browser/ui/ReactDOMTextComponent.js
renamed
+41
-35
@@ -6,7 +6,7 @@
6
* LICENSE file in the root directory of this source tree. An additional grant
7
* of patent rights can be found in the PATENTS file in the same directory.
8
*
9
- * @providesModule ReactTextComponent
9
+ * @providesModule ReactDOMTextComponent
10
* @typechecks static-only
11
*/
12
@@ -14,7 +14,8 @@
14
15
var DOMPropertyOperations = require('DOMPropertyOperations');
16
var ReactComponent = require('ReactComponent');
17
-var ReactElement = require('ReactElement');
17
+var ReactComponentBrowserEnvironment =
18
+ require('ReactComponentBrowserEnvironment');
19
20
var assign = require('Object.assign');
21
var escapeTextForBrowser = require('escapeTextForBrowser');
@@ -31,15 +32,25 @@ var invariant = require('invariant');
32
*
33
* TODO: Investigate representing React components in the DOM with text nodes.
34
*
34
- * @class ReactTextComponent
35
+ * @class ReactDOMTextComponent
36
* @extends ReactComponent
37
* @internal
38
*/
38
-var ReactTextComponent = function(props) {
39
- // This constructor and it's argument is currently used by mocks.
39
+var ReactDOMTextComponent = function(props) {
40
+ // This constructor and its argument is currently used by mocks.
41
};
42
42
-assign(ReactTextComponent.prototype, ReactComponent.Mixin, {
43
+assign(ReactDOMTextComponent.prototype, {
44
+
45
+ /**
46
+ * @param {ReactText} node
47
+ * @internal
48
+ */
49
+ construct: function(text) {
50
+ // TODO: This is really a ReactText (ReactNode), not a ReactElement
51
+ this._currentElement = text;
52
+ this._stringText = '' + text;
53
+ },
54
55
/**
56
* Creates the markup for this text node. This node is not intended to have
@@ -52,14 +63,8 @@ assign(ReactTextComponent.prototype, ReactComponent.Mixin, {
63
* @internal
64
*/
65
mountComponent: function(rootID, transaction, mountDepth) {
55
- ReactComponent.Mixin.mountComponent.call(
56
- this,
57
- rootID,
58
- transaction,
59
- mountDepth
60
- );
61
-
62
- var escapedText = escapeTextForBrowser(this.props);
66
+ this._rootNodeID = rootID;
67
+ var escapedText = escapeTextForBrowser(this._stringText);
68
69
if (transaction.renderToStaticMarkup) {
70
// Normally we'd wrap this in a `span` for the reasons stated above, but
@@ -78,38 +83,39 @@ assign(ReactTextComponent.prototype, ReactComponent.Mixin, {
83
/**
84
* Updates this component by updating the text content.
85
*
81
- * @param {object} nextComponent Contains the next text content.
86
+ * @param {ReactText} nextText The next text content
87
* @param {ReactReconcileTransaction} transaction
88
* @internal
89
*/
85
- receiveComponent: function(nextComponent, transaction) {
86
- var nextProps = nextComponent.props;
87
- if (nextProps !== this.props) {
88
- // TODO: Save this as pending props and use performUpdateIfNecessary
89
- // and/or updateComponent to do the actual update for consistency with
90
- // other component types?
91
- this.props = nextProps;
92
- ReactComponent.BackendIDOperations.updateTextContentByID(
93
- this._rootNodeID,
94
- nextProps
95
- );
90
+ receiveComponent: function(nextText, transaction) {
91
+ if (nextText !== this._currentElement) {
92
+ this._currentElement = nextText;
93
+ var nextStringText = '' + nextText;
94
+ if (nextStringText !== this._stringText) {
95
+ // TODO: Save this as pending props and use performUpdateIfNecessary
96
+ // and/or updateComponent to do the actual update for consistency with
97
+ // other component types?
98
+ this._stringText = nextStringText;
99
+ ReactComponent.BackendIDOperations.updateTextContentByID(
100
+ this._rootNodeID,
101
+ nextStringText
102
+ );
103
+ }
104
}
105
},
106
107
updateComponent: function() {
108
invariant(
109
false,
102
- 'ReactTextComponent: updateComponent() should never be called'
110
+ 'ReactDOMTextComponent: updateComponent() should never be called'
111
);
112
+ },
113
+
114
+ unmountComponent: function() {
115
+ // TODO: Is this necessary?
116
+ ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
117
}
118
119
});
120
108
-var ReactTextComponentFactory = function(text) {
109
- // Bypass validation and configuration
110
- return new ReactElement(ReactTextComponent, null, null, null, null, text);
111
-};
112
-
113
-ReactTextComponentFactory.type = ReactTextComponent;
114
-
115
-module.exports = ReactTextComponentFactory;
121
+module.exports = ReactDOMTextComponent;
src/browser/ui/ReactDefaultInjection.js
+5
@@ -32,6 +32,7 @@ var ReactDOMInput = require('ReactDOMInput');
32
var ReactDOMOption = require('ReactDOMOption');
33
var ReactDOMSelect = require('ReactDOMSelect');
34
var ReactDOMTextarea = require('ReactDOMTextarea');
35
+var ReactDOMTextComponent = require('ReactDOMTextComponent');
36
var ReactEventListener = require('ReactEventListener');
37
var ReactInjection = require('ReactInjection');
38
var ReactInstanceHandles = require('ReactInstanceHandles');
@@ -73,6 +74,10 @@ function inject() {
74
ReactDOMComponent
75
);
76
77
+ ReactInjection.NativeComponent.injectTextComponentClass(
78
+ ReactDOMTextComponent
79
+ );
80
+
81
ReactInjection.NativeComponent.injectComponentClasses({
82
'button': ReactDOMButton,
83
'form': ReactDOMForm,
src/browser/ui/__tests__/ReactDOMTextComponent-test.js
renamed
+1
-1
@@ -13,7 +13,7 @@
13
14
var React;
15
16
-describe('ReactTextComponent', function() {
16
+describe('ReactDOMTextComponent', function() {
17
beforeEach(function() {
18
React = require('React');
19
});
src/core/ReactMultiChild.js
+1
-1
@@ -306,7 +306,7 @@ var ReactMultiChild = {
306
// Remove children that are no longer present.
307
for (name in prevChildren) {
308
if (prevChildren.hasOwnProperty(name) &&
309
- !(nextChildren && nextChildren[name])) {
309
+ !(nextChildren && nextChildren.hasOwnProperty(name))) {
310
this._unmountChildByName(prevChildren[name], name);
311
}
312
}
src/core/ReactNativeComponent.js
+24
@@ -17,6 +17,7 @@ var invariant = require('invariant');
17
var genericComponentClass = null;
18
// This registry keeps track of wrapper classes around native tags
19
var tagToComponentClass = {};
20
+var textComponentClass = null;
21
22
var ReactNativeComponentInjection = {
23
// This accepts a class that receives the tag string. This is a catch all
@@ -24,6 +25,11 @@ var ReactNativeComponentInjection = {
25
injectGenericComponentClass: function(componentClass) {
26
genericComponentClass = componentClass;
27
},
28
+ // This accepts a text component class that takes the text string to be
29
+ // rendered as props.
30
+ injectTextComponentClass: function(componentClass) {
31
+ textComponentClass = componentClass;
32
+ },
33
// This accepts a keyed object with classes as values. Each key represents a
34
// tag. That particular tag will use this class instead of the generic one.
35
injectComponentClasses: function(componentClasses) {
@@ -60,8 +66,26 @@ function createInstanceForTag(tag, props, parentType) {
66
return new componentClass(props);
67
}
68
69
+/**
70
+ * @param {ReactText} text
71
+ * @return {ReactComponent}
72
+ */
73
+function createInstanceForText(text) {
74
+ return new textComponentClass(text);
75
+}
76
+
77
+/**
78
+ * @param {ReactComponent} component
79
+ * @return {boolean}
80
+ */
81
+function isTextComponent(component) {
82
+ return component instanceof textComponentClass;
83
+}
84
+
85
var ReactNativeComponent = {
86
createInstanceForTag: createInstanceForTag,
87
+ createInstanceForText: createInstanceForText,
88
+ isTextComponent: isTextComponent,
89
injection: ReactNativeComponentInjection
90
};
91
src/core/__tests__/ReactMultiChildText-test.js
+1
-2
@@ -70,8 +70,7 @@ var expectChildren = function(d, children) {
70
if (typeof child === 'string') {
71
reactComponentExpect(d)
72
.expectRenderedChildAt(i)
73
- .toBeTextComponent()
74
- .instance();
73
+ .toBeTextComponent();
74
75
textNode = d.getDOMNode().childNodes[i].firstChild;
76
src/core/instantiateReactComponent.js
+45
-33
@@ -16,6 +16,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent');
16
var ReactNativeComponent = require('ReactNativeComponent');
17
18
var assign = require('Object.assign');
19
+var invariant = require('invariant');
20
var warning = require('warning');
21
22
// To avoid a cyclic dependency, we create the final class in this module
@@ -46,60 +47,71 @@ function isInternalComponentType(type) {
47
}
48
49
/**
49
- * Given an `element` create an instance that will actually be mounted.
50
+ * Given a ReactNode, create an instance that will actually be mounted.
51
*
51
- * @param {object} element
52
+ * @param {ReactNode} node
53
* @param {*} parentCompositeType The composite type that resolved this.
54
* @return {object} A new instance of the element's constructor.
55
* @protected
56
*/
56
-function instantiateReactComponent(element, parentCompositeType) {
57
+function instantiateReactComponent(node, parentCompositeType) {
58
var instance;
59
59
- if (__DEV__) {
60
- warning(
61
- element && (typeof element.type === 'function' ||
62
- typeof element.type === 'string'),
63
- 'Only functions or strings can be mounted as React components.'
64
- );
65
- }
60
+ if (typeof node === 'object') {
61
+ var element = node;
62
+ if (__DEV__) {
63
+ warning(
64
+ element && (typeof element.type === 'function' ||
65
+ typeof element.type === 'string'),
66
+ 'Only functions or strings can be mounted as React components.'
67
+ );
68
+ }
69
67
- // Special case string values
68
- if (typeof element.type === 'string') {
69
- instance = ReactNativeComponent.createInstanceForTag(
70
- element.type,
71
- element.props,
72
- parentCompositeType
73
- );
74
- // If the injected special class is not an internal class, but another
75
- // composite, then we must wrap it.
76
- // TODO: Move this resolution around to something cleaner.
77
- if (typeof instance.mountComponent !== 'function') {
78
- instance = new ReactCompositeComponentWrapper(instance);
70
+ // Special case string values
71
+ if (typeof element.type === 'string') {
72
+ instance = ReactNativeComponent.createInstanceForTag(
73
+ element.type,
74
+ element.props,
75
+ parentCompositeType
76
+ );
77
+ // If the injected special class is not an internal class, but another
78
+ // composite, then we must wrap it.
79
+ // TODO: Move this resolution around to something cleaner.
80
+ if (typeof instance.mountComponent !== 'function') {
81
+ instance = new ReactCompositeComponentWrapper(instance);
82
+ }
83
+ } else if (isInternalComponentType(element.type)) {
84
+ // This is temporarily available for custom components that are not string
85
+ // represenations. I.e. ART. Once those are updated to use the string
86
+ // representation, we can drop this code path.
87
+ instance = new element.type(element);
88
+ } else {
89
+ // TODO: Update to follow new ES6 initialization. Ideally, we can use
90
+ // props in property initializers.
91
+ var inst = new element.type(element.props);
92
+ instance = new ReactCompositeComponentWrapper(inst);
93
}
80
- } else if (isInternalComponentType(element.type)) {
81
- // This is temporarily available for custom components that are not string
82
- // represenations. I.e. ART. Once those are updated to use the string
83
- // representation, we can drop this code path.
84
- instance = new element.type(element);
94
+ } else if (typeof node === 'string' || typeof node === 'number') {
95
+ instance = ReactNativeComponent.createInstanceForText(node);
96
} else {
86
- // TODO: Update to follow new ES6 initialization. Ideally, we can use props
87
- // in property initializers.
88
- var inst = new element.type(element.props);
89
- instance = new ReactCompositeComponentWrapper(inst);
97
+ invariant(
98
+ false,
99
+ 'Encountered invalid React node of type ' + typeof node
100
+ );
101
}
102
103
if (__DEV__) {
104
warning(
105
typeof instance.construct === 'function' &&
106
typeof instance.mountComponent === 'function' &&
96
- typeof instance.receiveComponent === 'function',
107
+ typeof instance.receiveComponent === 'function' &&
108
+ typeof instance.unmountComponent === 'function',
109
'Only React Components can be mounted.'
110
);
111
}
112
113
// Sets up the instance. This can probably just move into the constructor now.
102
- instance.construct(element);
114
+ instance.construct(node);
115
116
return instance;
117
}
src/core/shouldUpdateReactComponent.js
+15
-5
@@ -12,6 +12,8 @@
12
13
"use strict";
14
15
+var ReactNativeComponent = require('ReactNativeComponent');
16
+
17
/**
18
* Given a `prevElement` and `nextElement`, determines if the existing
19
* instance should be updated as opposed to being destroyed or replaced by a new
@@ -24,11 +26,19 @@
26
* @protected
27
*/
28
function shouldUpdateReactComponent(prevElement, nextElement) {
27
- if (prevElement && nextElement &&
28
- prevElement.type === nextElement.type &&
29
- prevElement.key === nextElement.key &&
30
- prevElement._owner === nextElement._owner) {
31
- return true;
29
+ if (prevElement != null && nextElement != null) {
30
+ var prevType = typeof prevElement;
31
+ var nextType = typeof nextElement;
32
+ if (prevType === 'string' || prevType === 'number') {
33
+ return (nextType === 'string' || nextType === 'number');
34
+ } else {
35
+ return (
36
+ nextType === 'object' &&
37
+ prevElement.type === nextElement.type &&
38
+ prevElement.key === nextElement.key &&
39
+ prevElement._owner === nextElement._owner
40
+ );
41
+ }
42
}
43
return false;
44
}
src/test/ReactTestUtils.js
-5
@@ -19,7 +19,6 @@ var ReactElement = require('ReactElement');
19
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
20
var ReactInstanceMap = require('ReactInstanceMap');
21
var ReactMount = require('ReactMount');
22
-var ReactTextComponent = require('ReactTextComponent');
22
var ReactUpdates = require('ReactUpdates');
23
var SyntheticEvent = require('SyntheticEvent');
24
@@ -100,10 +99,6 @@ var ReactTestUtils = {
99
(inst.constructor === type));
100
},
101
103
- isTextComponent: function(inst) {
104
- return inst instanceof ReactTextComponent.type;
105
- },
106
-
102
getInternalRepresentation: function(inst) {
103
// TODO: Remove this duck check once we have a separate DOM/Native instance
104
if (typeof inst.mountComponent === 'function') {
src/test/reactComponentExpect.js
+2
-2
@@ -12,7 +12,6 @@
12
13
"use strict";
14
15
-var ReactInstanceMap = require('ReactInstanceMap');
15
var ReactTestUtils = require('ReactTestUtils');
16
17
var assign = require('Object.assign');
@@ -131,7 +130,8 @@ assign(reactComponentExpect.prototype, {
130
},
131
132
toBeTextComponent: function() {
134
- expect(ReactTestUtils.isTextComponent(this.instance())).toBe(true);
133
+ var elementType = typeof this._instance._currentElement;
134
+ expect(elementType === 'string' || elementType === 'number').toBe(true);
135
return this;
136
},
137
src/utils/flattenChildren.js
+1
-14
@@ -11,8 +11,6 @@
11
12
"use strict";
13
14
-var ReactTextComponent = require('ReactTextComponent');
15
-
14
var traverseAllChildren = require('traverseAllChildren');
15
var warning = require('warning');
16
@@ -33,18 +31,7 @@ function flattenSingleChildIntoContext(traverseContext, child, name) {
31
name
32
);
33
if (keyUnique && child != null) {
36
- var type = typeof child;
37
- var normalizedValue;
38
-
39
- if (type === 'string') {
40
- normalizedValue = ReactTextComponent(child);
41
- } else if (type === 'number') {
42
- normalizedValue = ReactTextComponent('' + child);
43
- } else {
44
- normalizedValue = child;
45
- }
46
-
47
- result[name] = normalizedValue;
34
+ result[name] = child;
35
}
36
}
37
src/utils/traverseAllChildren.js
+2
-5
@@ -21,11 +21,8 @@ var SEPARATOR = ReactInstanceHandles.SEPARATOR;
21
var SUBSEPARATOR = ':';
22
23
/**
24
- * TODO: Test that:
25
- * 1. `mapChildren` transforms strings and numbers into `ReactTextComponent`.
26
- * 2. it('should fail when supplied duplicate key', function() {
27
- * 3. That a single child and an array with one item have the same key pattern.
28
- * });
24
+ * TODO: Test that a single child and an array with one item have the same key
25
+ * pattern.
26
*/
27
28
var userProvidedKeyEscaperLookup = {