Wrap every DOM node in a Composite Component

...unless they already have a wrapper. Also, add tagName to every wrapper.

This ensures that refs are consistent. They always look like composite
components. This effectively hides the internal implementation details of
real DOM components since you can no longer get a ref to one.

In the future we might want to drop this wrapper and have refs refer
directly to the DOM node.

I currently use a hacky way of auto-wrapping inside of ReactNativeComponent
so that any given string can be wrapped. Better suggestions are welcome.
This commit is contained in:
Sebastian Markbage
2014-11-16 10:32:46 -08:00
parent 795290d1b0
commit 9c3e2d833d
18 changed files with 80 additions and 61 deletions
+3 -4
View File
@@ -15,7 +15,6 @@
var React = require('React');
var ReactDOM = require('ReactDOM');
var ReactMount = require('ReactMount');
var ReactTestUtils = require('ReactTestUtils');
var div = React.createFactory('div');
@@ -53,7 +52,7 @@ describe('ReactDOM', function() {
var argDiv = ReactTestUtils.renderIntoDocument(
div(null, 'child')
);
var argNode = ReactMount.getNode(argDiv._rootNodeID);
var argNode = argDiv.getDOMNode();
expect(argNode.innerHTML).toBe('child');
});
@@ -61,7 +60,7 @@ describe('ReactDOM', function() {
var conflictDiv = ReactTestUtils.renderIntoDocument(
div({children: 'fakechild'}, 'child')
);
var conflictNode = ReactMount.getNode(conflictDiv._rootNodeID);
var conflictNode = conflictDiv.getDOMNode();
expect(conflictNode.innerHTML).toBe('child');
});
@@ -103,7 +102,7 @@ describe('ReactDOM', function() {
theBird: <div className="bird" />
}
});
var root = ReactMount.getNode(myDiv._rootNodeID);
var root = myDiv.getDOMNode();
var dog = root.childNodes[0];
expect(dog.className).toBe('bigdog');
});
+1 -4
View File
@@ -15,7 +15,6 @@
var CSSPropertyOperations = require('CSSPropertyOperations');
var DOMProperty = require('DOMProperty');
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactComponent = require('ReactComponent');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactMount = require('ReactMount');
@@ -148,7 +147,6 @@ function validateDangerousTag(tag) {
function ReactDOMComponent(tag) {
validateDangerousTag(tag);
this._tag = tag;
this.tagName = tag.toUpperCase();
}
ReactDOMComponent.displayName = 'ReactDOMComponent';
@@ -488,8 +486,7 @@ assign(
ReactDOMComponent.prototype,
ReactComponent.Mixin,
ReactDOMComponent.Mixin,
ReactMultiChild.Mixin,
ReactBrowserComponentMixin
ReactMultiChild.Mixin
);
module.exports = ReactDOMComponent;
+1 -7
View File
@@ -135,13 +135,7 @@ function getNode(id) {
* @internal
*/
function getNodeFromInstance(instance) {
// This instance can currently be either a public or private instance since
// native nodes are still public.
var id = instance._rootNodeID;
// TODO: Once these are only public instances, remove this conditional.
if (id == null) {
id = ReactInstanceMap.get(instance)._rootNodeID;
}
var id = ReactInstanceMap.get(instance)._rootNodeID;
if (ReactEmptyComponent.isNullComponentID(id)) {
return null;
}
@@ -39,6 +39,7 @@ var mouseListenerNames = keyMirror({
*/
var ReactDOMButton = ReactClass.createClass({
displayName: 'ReactDOMButton',
tagName: 'BUTTON',
mixins: [AutoFocusMixin, ReactBrowserComponentMixin],
@@ -27,6 +27,7 @@ var form = ReactElement.createFactory('form');
*/
var ReactDOMForm = ReactClass.createClass({
displayName: 'ReactDOMForm',
tagName: 'FORM',
mixins: [ReactBrowserComponentMixin, LocalEventTrapMixin],
@@ -52,6 +52,7 @@ function forceUpdateIfMounted() {
*/
var ReactDOMInput = ReactClass.createClass({
displayName: 'ReactDOMInput',
tagName: 'INPUT',
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],
@@ -24,6 +24,7 @@ var option = ReactElement.createFactory('option');
*/
var ReactDOMOption = ReactClass.createClass({
displayName: 'ReactDOMOption',
tagName: 'OPTION',
mixins: [ReactBrowserComponentMixin],
@@ -109,6 +109,7 @@ function updateOptions(component, propValue) {
*/
var ReactDOMSelect = ReactClass.createClass({
displayName: 'ReactDOMSelect',
tagName: 'SELECT',
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],
@@ -50,6 +50,7 @@ function forceUpdateIfMounted() {
*/
var ReactDOMTextarea = ReactClass.createClass({
displayName: 'ReactDOMTextarea',
tagName: 'TEXTAREA',
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],
+5 -1
View File
@@ -453,7 +453,11 @@ var ReactComponent = {
* @internal
*/
getPublicInstance: function() {
return this;
invariant(
false,
'getPublicInstance should never be called on the base class. It must ' +
'be overriden.'
);
}
}
};
+20 -6
View File
@@ -11,6 +11,9 @@
"use strict";
var ReactClass = require('ReactClass');
var ReactElement = require('ReactElement');
var assign = require('Object.assign');
var invariant = require('invariant');
@@ -37,6 +40,22 @@ var ReactNativeComponentInjection = {
}
};
function autoGenerateWrapperClass(type) {
return ReactClass.createClass({
tagName: type.toUpperCase(),
render: function() {
return new ReactElement(
type,
null,
null,
null,
null,
this.props
);
}
});
}
/**
* Create an internal class for a specific tag.
*
@@ -47,12 +66,7 @@ var ReactNativeComponentInjection = {
function createInstanceForTag(tag, props, parentType) {
var componentClass = tagToComponentClass[tag];
if (componentClass == null) {
invariant(
genericComponentClass,
'There is no registered component for the tag %s',
tag
);
return new genericComponentClass(tag, props);
tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
}
if (parentType === tag) {
// Avoid recursion
+3 -7
View File
@@ -26,10 +26,6 @@ describe('ReactComponent', function() {
reactComponentExpect = require('reactComponentExpect');
getMountDepth = function(instance) {
if (instance.mountComponent) {
// Native instance
return instance._mountDepth;
}
return ReactInstanceMap.get(instance)._mountDepth;
};
});
@@ -306,9 +302,9 @@ describe('ReactComponent', function() {
expect(getMountDepth(root)).toBe(0);
expect(getMountDepth(root.refs.switcher)).toBe(1);
expect(getMountDepth(root.refs.switcher.refs.box)).toBe(2);
expect(getMountDepth(root.refs.switcher.refs.switcherDiv)).toBe(4);
expect(getMountDepth(root.refs.child)).toBe(5);
expect(getMountDepth(root.refs.switcher.refs.switcherDiv)).toBe(5);
expect(getMountDepth(root.refs.child)).toBe(7);
expect(getMountDepth(root.refs.switcher.refs.box.refs.boxDiv)).toBe(3);
expect(getMountDepth(root.refs.child.refs.span)).toBe(6);
expect(getMountDepth(root.refs.child.refs.span)).toBe(8);
});
});
@@ -66,8 +66,8 @@ describe('ReactCompositeComponent', function() {
* reallocated again.
*/
ChildUpdates = React.createClass({
getAnchorID: function() {
return this.refs.anch._rootNodeID;
getAnchor: function() {
return this.refs.anch;
},
render: function() {
var className = cx({'anchorClass': this.props.anchorClassOn});
@@ -182,8 +182,8 @@ describe('ReactCompositeComponent', function() {
instance.setProps({renderAnchor: false}); // Clear out the anchor
// rerender
instance.setProps({renderAnchor: true, anchorClassOn: false});
var anchorID = instance.getAnchorID();
var actualDOMAnchorNode = ReactMount.getNode(anchorID);
var anchor = instance.getAnchor();
var actualDOMAnchorNode = anchor.getDOMNode();
expect(actualDOMAnchorNode.className).toBe('');
});
@@ -779,9 +779,11 @@ describe('ReactCompositeComponent', function() {
React.unmountComponentAtNode(container);
expect(innerUnmounted).toBe(true);
// <Component />, <Inner />, and both <div /> elements each call
// unmountIDFromEnvironment which calls purgeID, for a total of 4.
expect(ReactMount.purgeID.callCount).toBe(4);
// <Component />, <Inner />, and both <div /> elements and their wrappers
// each call unmountIDFromEnvironment which calls purgeID, for a total of 6.
// TODO: Test the effect of this. E.g. does the node cache get repopulated
// after a getDOMNode call?
expect(ReactMount.purgeID.callCount).toBe(6);
});
it('should warn when shouldComponentUpdate() returns undefined', function() {
@@ -26,8 +26,8 @@ describe('ReactCompositeComponent-error', function() {
}
});
var instance = <Component />;
instance = ReactTestUtils.renderIntoDocument(instance);
new Component();
expect(ReactErrorUtils.guard.mostRecentCall.args[1])
.toEqual('Component.someHandler');
});
@@ -12,6 +12,7 @@
"use strict";
var React = require('React');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactTestUtils = require('ReactTestUtils');
var ReactMount = require('ReactMount');
@@ -66,8 +67,7 @@ describe('ReactInstanceHandles', function() {
if (instance === null) {
return '';
}
var internal = ReactTestUtils.getInternalRepresentation(instance);
return internal._rootNodeID;
return ReactInstanceMap.get(instance)._rootNodeID;
}
beforeEach(function() {
@@ -87,7 +87,10 @@ var FriendsStatusDisplay = React.createClass({
// TODO: Update this to a better test that doesn't rely so much on internal
// implementation details.
var statusDisplays =
ReactInstanceMap.get(this)._renderedComponent._renderedChildren;
ReactInstanceMap.get(this)
._renderedComponent
._renderedComponent
._renderedChildren;
for (name in statusDisplays) {
var child = statusDisplays[name];
var isPresent = !!child;
+4 -9
View File
@@ -99,14 +99,6 @@ var ReactTestUtils = {
(inst.constructor === type));
},
getInternalRepresentation: function(inst) {
// TODO: Remove this duck check once we have a separate DOM/Native instance
if (typeof inst.mountComponent === 'function') {
return inst;
}
return ReactInstanceMap.get(inst);
},
getRenderedChildOfCompositeComponent: function(inst) {
if (!ReactTestUtils.isCompositeComponent(inst)) {
return null;
@@ -121,7 +113,10 @@ var ReactTestUtils = {
}
var ret = test(inst) ? [inst] : [];
if (ReactTestUtils.isDOMComponent(inst)) {
var renderedChildren = inst._renderedChildren;
var internalInstance = ReactInstanceMap.get(inst);
var renderedChildren = internalInstance
._renderedComponent
._renderedChildren;
var key;
for (key in renderedChildren) {
if (!renderedChildren.hasOwnProperty(key)) {
+20 -11
View File
@@ -12,6 +12,7 @@
"use strict";
var ReactInstanceMap = require('ReactInstanceMap');
var ReactTestUtils = require('ReactTestUtils');
var assign = require('Object.assign');
@@ -27,14 +28,20 @@ function reactComponentExpect(instance) {
expect(instance).not.toBeNull();
this._instance = ReactTestUtils.getInternalRepresentation(instance);
var internalInstance = ReactInstanceMap.get(instance);
expect(typeof instance).toBe('object');
expect(typeof instance.constructor).toBe('function');
expect(ReactTestUtils.isElement(instance)).toBe(false);
expect(typeof internalInstance).toBe('object');
expect(typeof internalInstance.constructor).toBe('function');
expect(ReactTestUtils.isElement(internalInstance)).toBe(false);
return new reactComponentExpectInternal(internalInstance);
}
assign(reactComponentExpect.prototype, {
function reactComponentExpectInternal(internalInstance) {
this._instance = internalInstance;
}
assign(reactComponentExpectInternal.prototype, {
// Getters -------------------------------------------------------------------
/**
@@ -62,7 +69,7 @@ assign(reactComponentExpect.prototype, {
this.toBeCompositeComponent();
var child = this._instance._renderedComponent;
// TODO: Hide ReactEmptyComponent instances here?
return new reactComponentExpect(child);
return new reactComponentExpectInternal(child);
},
/**
@@ -72,14 +79,15 @@ assign(reactComponentExpect.prototype, {
// Currently only dom components have arrays of children, but that will
// change soon.
this.toBeDOMComponent();
var renderedChildren = this._instance._renderedChildren || {};
var renderedChildren =
this._instance._renderedComponent._renderedChildren || {};
for (var name in renderedChildren) {
if (!renderedChildren.hasOwnProperty(name)) {
continue;
}
if (renderedChildren[name]) {
if (renderedChildren[name]._mountIndex === childIndex) {
return new reactComponentExpect(renderedChildren[name]);
return new reactComponentExpectInternal(renderedChildren[name]);
}
}
}
@@ -88,15 +96,16 @@ assign(reactComponentExpect.prototype, {
toBeDOMComponentWithChildCount: function(n) {
this.toBeDOMComponent();
expect(this._instance._renderedChildren).toBeTruthy();
var len = Object.keys(this._instance._renderedChildren).length;
expect(this._instance._renderedComponent._renderedChildren).toBeTruthy();
var len = Object.keys(this._instance._renderedComponent._renderedChildren)
.length;
expect(len).toBe(n);
return this;
},
toBeDOMComponentWithNoChildren: function() {
this.toBeDOMComponent();
expect(this._instance._renderedChildren).toBeFalsy();
expect(this._instance._renderedComponent._renderedChildren).toBeFalsy();
return this;
},