Skip to content

Commit

Permalink
Add functions to find the instance given a node
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiebits committed Nov 4, 2015
1 parent ce52845 commit 6d20556
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 16 deletions.
62 changes: 62 additions & 0 deletions src/renderers/dom/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ var invariant = require('invariant');
var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
var Flags = ReactDOMComponentFlags;

var internalInstanceKey =
'__reactInternalInstance$' + Math.random().toString(36).slice(2);

/**
* Drill down (through composites and empty components) until we get a native or
* native text component.
Expand All @@ -41,6 +44,15 @@ function getRenderedNativeOrTextFromComponent(component) {
function precacheNode(inst, node) {
var nativeInst = getRenderedNativeOrTextFromComponent(inst);
nativeInst._nativeNode = node;
node[internalInstanceKey] = nativeInst;
}

function uncacheNode(inst) {
var node = inst._nativeNode;
if (node) {
delete node[internalInstanceKey];
inst._nativeNode = null;
}
}

/**
Expand Down Expand Up @@ -84,6 +96,53 @@ function precacheChildNodes(inst, node) {
inst._flags |= Flags.hasCachedChildNodes;
}

/**
* Given a DOM node, return the closest ReactDOMComponent or
* ReactDOMTextComponent instance ancestor.
*/
function getClosestInstanceFromNode(node) {
if (node[internalInstanceKey]) {
return node[internalInstanceKey];
}

// Walk up the tree until we find an ancestor whose instance we have cached.
var parents = [];
while (!node[internalInstanceKey]) {
parents.push(node);
if (node.parentNode) {
node = node.parentNode;
} else {
// Top of the tree. This node must not be part of a React tree (or is
// unmounted, potentially).
return null;
}
}

var closest;
var inst;
for (; node && (inst = node[internalInstanceKey]); node = parents.pop()) {
closest = inst;
if (parents.length) {
precacheChildNodes(inst, node);
}
}

return closest;
}

/**
* Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent
* instance, or null if the node was not rendered by this React.
*/
function getInstanceFromNode(node) {
var inst = getClosestInstanceFromNode(node);
if (inst != null && inst._nativeNode === node) {
return inst;
} else {
return null;
}
}

/**
* Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding
* DOM node.
Expand Down Expand Up @@ -114,9 +173,12 @@ function getNodeFromInstance(inst) {
}

var ReactDOMComponentTree = {
getClosestInstanceFromNode: getClosestInstanceFromNode,
getInstanceFromNode: getInstanceFromNode,
getNodeFromInstance: getNodeFromInstance,
precacheChildNodes: precacheChildNodes,
precacheNode: precacheNode,
uncacheNode: uncacheNode,
};

module.exports = ReactDOMComponentTree;
47 changes: 47 additions & 0 deletions src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
describe('ReactDOMComponentTree', function() {
var React;
var ReactDOM;
var ReactDOMComponentTree;
var ReactDOMServer;

function renderMarkupIntoDocument(elt) {
Expand All @@ -26,6 +27,7 @@ describe('ReactDOMComponentTree', function() {
beforeEach(function() {
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMComponentTree = require('ReactDOMComponentTree');
ReactDOMServer = require('ReactDOMServer');
});

Expand Down Expand Up @@ -59,4 +61,49 @@ describe('ReactDOMComponentTree', function() {
expect(renderAndGetRef('input')).toBe('INPUT');
});

it('finds instances for nodes', function() {
var Component = React.createClass({
render: function() {
return (
<div>
<h1>hello</h1>
<p>
<input />
</p>
goodbye.
<main dangerouslySetInnerHTML={{__html: '<b><img></b>'}} />
</div>
);
},
});

function renderAndQuery(sel) {
var root = renderMarkupIntoDocument(<section><Component /></section>);
return sel ? root.querySelector(sel) : root;
}

function renderAndGetInstance(sel) {
return ReactDOMComponentTree.getInstanceFromNode(renderAndQuery(sel));
}

function renderAndGetClosest(sel) {
return ReactDOMComponentTree.getClosestInstanceFromNode(
renderAndQuery(sel)
);
}

expect(renderAndGetInstance(null)._currentElement.type).toBe('section');
expect(renderAndGetInstance('div')._currentElement.type).toBe('div');
expect(renderAndGetInstance('h1')._currentElement.type).toBe('h1');
expect(renderAndGetInstance('p')._currentElement.type).toBe('p');
expect(renderAndGetInstance('input')._currentElement.type).toBe('input');
expect(renderAndGetInstance('main')._currentElement.type).toBe('main');

// This one's a text component!
expect(renderAndGetInstance('span')._stringText).toBe('goodbye.');

expect(renderAndGetClosest('b')._currentElement.type).toBe('main');
expect(renderAndGetClosest('img')._currentElement.type).toBe('main');
});

});
26 changes: 10 additions & 16 deletions src/renderers/dom/shared/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ if (__DEV__) {
props: {
enumerable: false,
get: function() {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
warning(
false,
'ReactDOMComponent: Do not access .props of a DOM node; instead, ' +
Expand All @@ -99,7 +99,7 @@ if (__DEV__) {

function legacyGetDOMNode() {
if (__DEV__) {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
warning(
false,
'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' +
Expand All @@ -111,7 +111,7 @@ function legacyGetDOMNode() {
}

function legacyIsMounted() {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
if (__DEV__) {
warning(
false,
Expand All @@ -124,7 +124,7 @@ function legacyIsMounted() {

function legacySetStateEtc() {
if (__DEV__) {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
warning(
false,
'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' +
Expand All @@ -135,7 +135,7 @@ function legacySetStateEtc() {
}

function legacySetProps(partialProps, callback) {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
if (__DEV__) {
warning(
false,
Expand All @@ -154,7 +154,7 @@ function legacySetProps(partialProps, callback) {
}

function legacyReplaceProps(partialProps, callback) {
var component = this._reactInternalComponent;
var component = ReactDOMComponentTree.getInstanceFromNode(this);
if (__DEV__) {
warning(
false,
Expand Down Expand Up @@ -661,7 +661,7 @@ ReactDOMComponent.Mixin = {
this._currentElement.type
);
}
this._nativeNode = el;
ReactDOMComponentTree.precacheNode(this, el);
this._flags |= Flags.hasCachedChildNodes;
DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
// Populate node cache
Expand Down Expand Up @@ -1145,11 +1145,7 @@ ReactDOMComponent.Mixin = {
break;
}

if (this._flags & Flags.nodeHasLegacyProperties) {
this._nativeNode._reactInternalComponent = null;
}
this._nativeNode = null;

ReactDOMComponentTree.uncacheNode(this);
this.unmountChildren();
EventPluginHub.deleteAllListeners(this._rootNodeID);
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
Expand All @@ -1158,12 +1154,10 @@ ReactDOMComponent.Mixin = {
},

getPublicInstance: function() {
var node = getNode(this);
if (this._flags & Flags.nodeHasLegacyProperties) {
return this._nativeNode;
return node;
} else {
var node = getNode(this);

node._reactInternalComponent = this;
node.getDOMNode = legacyGetDOMNode;
node.isMounted = legacyIsMounted;
node.setState = legacySetStateEtc;
Expand Down

0 comments on commit 6d20556

Please sign in to comment.