mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Batch Child Markup Generation
Setting `innerHTML` is slow: http://jsperf.com/react-child-creation/2 This reduces the number of times we set `innerHTML` by batching markup generation in a component tree. As usual, I cleaned up the `ReactMultiChild` module significantly. == Children Reconciliation == When a `ReactNativeComponent` reconciles, it compares currently rendered children, `prevChildren`, with the new children, `nextChildren`. It figures out the shortest series of updates required to render `nextChildren` where each update is one of: - Create nodes for a new child and insert it at an index. - Update an existing node and, if necessary, move it to an index. - Remove an existing node. This serializable series of updates is sent to `ReactDOMIDOperations` where the actions are actually acted on. == Problem == There are two problems: # When a `ReactNativeComponent` renders new children, it sets `innerHTML` once for each contiguous set of children. # Each `ReactNativeComponent` renders its children in isolation, so two components that both render new children will do so separately. For example, consider the following update: React.renderComponent(<div><p><span /></p><p><span /></p></div>, ...); React.renderComponent(<div><p><img /><span /><img /></p><p><img /><span /><img /></p></div>, ...); This will trigger setting `innerHTML` four times. == Solution == Instead of enqueuing the series of updates per component, this diff changes `ReactMultiChild` to enqueue updates per component tree (which works by counting recursive calls to `updateChildren`). Once all updates in the tree are accounted for, we render all markup using a single `innerHTML` set.
This commit is contained in:
committed by
Paul O’Shannessy
parent
2e37f65bdc
commit
adffa9b0f4
@@ -172,12 +172,17 @@ var ReactDOMIDOperations = {
|
||||
},
|
||||
|
||||
/**
|
||||
* TODO: We only actually *need* to purge the cache when we remove elements.
|
||||
* Detect if any elements were removed instead of blindly purging.
|
||||
* Updates a component's children by processing a series of updates.
|
||||
*
|
||||
* @param {array<object>} updates List of update configurations.
|
||||
* @param {array<string>} markup List of markup strings.
|
||||
* @internal
|
||||
*/
|
||||
manageChildrenByParentID: function(parentID, domOperations) {
|
||||
var parent = ReactMount.getNode(parentID);
|
||||
DOMChildrenOperations.manageChildren(parent, domOperations);
|
||||
dangerouslyProcessChildrenUpdates: function(updates, markup) {
|
||||
for (var i = 0; i < updates.length; i++) {
|
||||
updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
|
||||
}
|
||||
DOMChildrenOperations.processUpdates(updates, markup);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -229,14 +229,24 @@ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
|
||||
*/
|
||||
var ReactInstanceHandles = {
|
||||
|
||||
separator: SEPARATOR,
|
||||
|
||||
createReactRootID: function() {
|
||||
return getReactRootIDString(
|
||||
Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a React ID by joining a root ID with a name.
|
||||
*
|
||||
* @param {string} rootID Root ID of a parent component.
|
||||
* @param {string} name A component's name (as flattened children).
|
||||
* @return {string} A React ID.
|
||||
* @internal
|
||||
*/
|
||||
createReactID: function(rootID, name) {
|
||||
return rootID + SEPARATOR + name;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the DOM ID of the React component that is the root of the tree that
|
||||
* contains the React component with the supplied DOM ID.
|
||||
|
||||
+323
-161
@@ -14,194 +14,356 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactMultiChild
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
|
||||
|
||||
/**
|
||||
* Given a `curChild` and `newChild`, determines if `curChild` should be managed
|
||||
* as it exists, as opposed to being destroyed and/or replaced.
|
||||
* Given a `curChild` and `newChild`, determines if `curChild` should be
|
||||
* updated as opposed to being destroyed or replaced.
|
||||
*
|
||||
* @param {?ReactComponent} curChild
|
||||
* @param {?ReactComponent} newChild
|
||||
* @return {!boolean} Whether or not `curChild` should be updated with
|
||||
* `newChild`'s props
|
||||
* @return {boolean} True if `curChild` should be updated with `newChild`.
|
||||
* @protected
|
||||
*/
|
||||
function shouldManageExisting(curChild, newChild) {
|
||||
function shouldUpdateChild(curChild, newChild) {
|
||||
return curChild && newChild && curChild.constructor === newChild.constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ReactMultiChild` provides common functionality for components that have
|
||||
* multiple children. Standard `ReactCompositeComponent`s do not currently have
|
||||
* multiple children. `ReactNativeComponent`s do, however. Other specially
|
||||
* reconciled components will also have multiple children. Contains three
|
||||
* internally used properties that are used to keep track of state throughout
|
||||
* the `updateMultiChild` process.
|
||||
* Updating children of a component may trigger recursive updates. The depth is
|
||||
* used to batch recursive updates to render markup more efficiently.
|
||||
*
|
||||
* @class ReactMultiChild
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
var updateDepth = 0;
|
||||
|
||||
/**
|
||||
* @lends `ReactMultiChildMixin`.
|
||||
* Queue of update configuration objects.
|
||||
*
|
||||
* Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
|
||||
*
|
||||
* @type {array<object>}
|
||||
* @private
|
||||
*/
|
||||
var ReactMultiChildMixin = {
|
||||
var updateQueue = [];
|
||||
|
||||
enqueueMarkupAt: function(markup, insertAt) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
|
||||
},
|
||||
/**
|
||||
* Queue of markup to be rendered.
|
||||
*
|
||||
* @type {array<string>}
|
||||
* @private
|
||||
*/
|
||||
var markupQueue = [];
|
||||
|
||||
enqueueMove: function(originalIndex, finalIndex) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
|
||||
},
|
||||
/**
|
||||
* Enqueues markup to be rendered and inserted at a supplied index.
|
||||
*
|
||||
* @param {string} parentID ID of the parent component.
|
||||
* @param {string} markup Markup that renders into an element.
|
||||
* @param {number} toIndex Destination index.
|
||||
* @private
|
||||
*/
|
||||
function enqueueMarkup(parentID, markup, toIndex) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
updateQueue.push({
|
||||
parentID: parentID,
|
||||
parentNode: null,
|
||||
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
|
||||
markupIndex: markupQueue.push(markup) - 1,
|
||||
fromIndex: null,
|
||||
toIndex: toIndex
|
||||
});
|
||||
}
|
||||
|
||||
enqueueUnmountChildByName: function(name, removeChild) {
|
||||
if (ReactComponent.isValidComponent(removeChild)) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({removeAt: removeChild._domIndex});
|
||||
removeChild.unmountComponent && removeChild.unmountComponent();
|
||||
delete this._renderedChildren[name];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Enqueues moving an existing element to another index.
|
||||
*
|
||||
* @param {string} parentID ID of the parent component.
|
||||
* @param {number} fromIndex Source index of the existing element.
|
||||
* @param {number} toIndex Destination index of the element.
|
||||
* @private
|
||||
*/
|
||||
function enqueueMove(parentID, fromIndex, toIndex) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
updateQueue.push({
|
||||
parentID: parentID,
|
||||
parentNode: null,
|
||||
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
|
||||
markupIndex: null,
|
||||
fromIndex: fromIndex,
|
||||
toIndex: toIndex
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any pending DOM operations that have been accumulated when updating
|
||||
* the UI. By default, we execute the injected `DOMIDOperations` module's
|
||||
* `manageChildrenByParentID` which does executes the DOM operations without
|
||||
* any animation. It can be used as a reference implementation for special
|
||||
* animation based implementations.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
processChildDOMOperationsQueue: function() {
|
||||
if (this.domOperations) {
|
||||
ReactComponent.DOMIDOperations
|
||||
.manageChildrenByParentID(this._rootNodeID, this.domOperations);
|
||||
this.domOperations = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Enqueues removing an element at an index.
|
||||
*
|
||||
* @param {string} parentID ID of the parent component.
|
||||
* @param {number} fromIndex Index of the element to remove.
|
||||
* @private
|
||||
*/
|
||||
function enqueueRemove(parentID, fromIndex) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
updateQueue.push({
|
||||
parentID: parentID,
|
||||
parentNode: null,
|
||||
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
|
||||
markupIndex: null,
|
||||
fromIndex: fromIndex,
|
||||
toIndex: null
|
||||
});
|
||||
}
|
||||
|
||||
unmountMultiChild: function() {
|
||||
var renderedChildren = this._renderedChildren;
|
||||
for (var name in renderedChildren) {
|
||||
if (renderedChildren.hasOwnProperty(name) && renderedChildren[name]) {
|
||||
var renderedChild = renderedChildren[name];
|
||||
renderedChild.unmountComponent && renderedChild.unmountComponent();
|
||||
}
|
||||
}
|
||||
this._renderedChildren = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates markup for a component that holds multiple children. #todo: Allow
|
||||
* all `ReactMultiChildMixin`s to support having arrays of children without a
|
||||
* container node. This current implementation may assume that children exist
|
||||
* at domIndices [0, parentNode.length].
|
||||
*
|
||||
* Has side effects of (likely) causing events to be registered. Also, every
|
||||
* component instance may only be rendered once.
|
||||
*
|
||||
* @param {?Object} children Flattened children object.
|
||||
* @return {!String} The rendered markup.
|
||||
*/
|
||||
mountMultiChild: function(children, transaction) {
|
||||
var accum = '';
|
||||
var index = 0;
|
||||
for (var name in children) {
|
||||
var child = children[name];
|
||||
if (children.hasOwnProperty(name) && child) {
|
||||
accum += child.mountComponent(
|
||||
this._rootNodeID + '.' + name,
|
||||
transaction
|
||||
);
|
||||
child._domIndex = index;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
this._renderedChildren = children; // children are in just the right form!
|
||||
this.domOperations = null;
|
||||
return accum;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reconciles new children with old children in three phases.
|
||||
*
|
||||
* - Adds new content while updating existing children that should remain.
|
||||
* - Remove children that are no longer present in the next children.
|
||||
* - As a very last step, moves existing dom structures around.
|
||||
* - (Comment 1) `curChildrenDOMIndex` is the largest index of the current
|
||||
* rendered children that appears in the next children and did not need to
|
||||
* be "moved".
|
||||
* - (Comment 2) This is the key insight. If any non-removed child's previous
|
||||
* index is less than `curChildrenDOMIndex` it must be moved.
|
||||
*
|
||||
* @param {?Object} children Flattened children object.
|
||||
*/
|
||||
updateMultiChild: function(nextChildren, transaction) {
|
||||
if (!nextChildren && !this._renderedChildren) {
|
||||
return;
|
||||
} else if (nextChildren && !this._renderedChildren) {
|
||||
this._renderedChildren = {}; // lazily allocate backing store with nothing
|
||||
} else if (!nextChildren && this._renderedChildren) {
|
||||
nextChildren = {};
|
||||
}
|
||||
var rootDomIdDot = this._rootNodeID + '.';
|
||||
var markupBuffer = null; // Accumulate adjacent new children markup.
|
||||
var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
|
||||
var loopDomIndex = 0; // Index of loop through new children.
|
||||
var curChildrenDOMIndex = 0; // See (Comment 1)
|
||||
for (var name in nextChildren) {
|
||||
if (!nextChildren.hasOwnProperty(name)) {continue;}
|
||||
var curChild = this._renderedChildren[name];
|
||||
var nextChild = nextChildren[name];
|
||||
if (shouldManageExisting(curChild, nextChild)) {
|
||||
if (markupBuffer) {
|
||||
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
|
||||
markupBuffer = null;
|
||||
}
|
||||
numPendingInsert = 0;
|
||||
if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
|
||||
this.enqueueMove(curChild._domIndex, loopDomIndex);
|
||||
}
|
||||
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
|
||||
curChild.receiveProps(nextChild.props, transaction);
|
||||
curChild._domIndex = loopDomIndex;
|
||||
} else {
|
||||
if (curChild) { // !shouldUpdate && curChild => delete
|
||||
this.enqueueUnmountChildByName(name, curChild);
|
||||
curChildrenDOMIndex =
|
||||
Math.max(curChild._domIndex, curChildrenDOMIndex);
|
||||
}
|
||||
if (nextChild) { // !shouldUpdate && nextChild => insert
|
||||
this._renderedChildren[name] = nextChild;
|
||||
var nextMarkup =
|
||||
nextChild.mountComponent(rootDomIdDot + name, transaction);
|
||||
markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
|
||||
numPendingInsert++;
|
||||
nextChild._domIndex = loopDomIndex;
|
||||
}
|
||||
}
|
||||
loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
|
||||
}
|
||||
if (markupBuffer) {
|
||||
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
|
||||
}
|
||||
for (var childName in this._renderedChildren) { // from other direction
|
||||
if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
|
||||
var child = this._renderedChildren[childName];
|
||||
if (child && !nextChildren[childName]) {
|
||||
this.enqueueUnmountChildByName(childName, child);
|
||||
}
|
||||
}
|
||||
this.processChildDOMOperationsQueue();
|
||||
/**
|
||||
* Processes any enqueued updates.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function processQueue() {
|
||||
if (updateQueue.length) {
|
||||
ReactComponent.DOMIDOperations.dangerouslyProcessChildrenUpdates(
|
||||
updateQueue,
|
||||
markupQueue
|
||||
);
|
||||
clearQueue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any enqueued updates.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function clearQueue() {
|
||||
updateQueue.length = 0;
|
||||
markupQueue.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReactMultiChild are capable of reconciling multiple children.
|
||||
*
|
||||
* @class ReactMultiChild
|
||||
* @internal
|
||||
*/
|
||||
var ReactMultiChild = {
|
||||
Mixin: ReactMultiChildMixin
|
||||
|
||||
/**
|
||||
* Provides common functionality for components that must reconcile multiple
|
||||
* children. This is used by `ReactNativeComponent` to mount, update, and
|
||||
* unmount child components.
|
||||
*
|
||||
* @lends {ReactMultiChild.prototype}
|
||||
*/
|
||||
Mixin: {
|
||||
|
||||
/**
|
||||
* Generates a "mount image" for each of the supplied children. In the case
|
||||
* of `ReactNativeComponent`, a mount image is a string of markup.
|
||||
*
|
||||
* @param {?object} children As returned by `flattenChildren`.
|
||||
* @return {array} An array of mounted representations.
|
||||
* @internal
|
||||
*/
|
||||
mountChildren: function(children, transaction) {
|
||||
var mountImages = [];
|
||||
var index = 0;
|
||||
for (var name in children) {
|
||||
var child = children[name];
|
||||
if (children.hasOwnProperty(name) && child) {
|
||||
var mountImage = child.mountComponent(
|
||||
// Inlined for performance, see `ReactID.createReactID`.
|
||||
this._rootNodeID + '.' + name,
|
||||
transaction
|
||||
);
|
||||
child._mountImage = mountImage;
|
||||
child._mountIndex = index;
|
||||
mountImages.push(mountImage);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
this._renderedChildren = children;
|
||||
return mountImages;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the rendered children with new children.
|
||||
*
|
||||
* @param {?object} nextChildren As returned by `flattenChildren`.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
updateChildren: function(nextChildren, transaction) {
|
||||
updateDepth++;
|
||||
try {
|
||||
this._updateChildren(nextChildren, transaction);
|
||||
} catch (error) {
|
||||
updateDepth--;
|
||||
updateDepth || clearQueue();
|
||||
throw error;
|
||||
}
|
||||
updateDepth--;
|
||||
updateDepth || processQueue();
|
||||
},
|
||||
|
||||
/**
|
||||
* Improve performance by isolating this hot code path from the try/catch
|
||||
* block in `updateChildren`.
|
||||
*
|
||||
* @param {?object} nextChildren As returned by `flattenChildren`.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
_updateChildren: function(nextChildren, transaction) {
|
||||
var prevChildren = this._renderedChildren;
|
||||
if (!nextChildren && !prevChildren) {
|
||||
return;
|
||||
}
|
||||
var name;
|
||||
// `nextIndex` will increment for each child in `nextChildren`, but
|
||||
// `lastIndex` will be the last index visited in `prevChildren`.
|
||||
var lastIndex = 0;
|
||||
var nextIndex = 0;
|
||||
for (name in nextChildren) {
|
||||
if (!nextChildren.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
var prevChild = prevChildren && prevChildren[name];
|
||||
var nextChild = nextChildren[name];
|
||||
if (shouldUpdateChild(prevChild, nextChild)) {
|
||||
this.moveChild(prevChild, nextIndex, lastIndex);
|
||||
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
|
||||
prevChild.receiveProps(nextChild.props, transaction);
|
||||
prevChild._mountIndex = nextIndex;
|
||||
} else {
|
||||
if (prevChild) {
|
||||
this._unmountChildByName(prevChild, name);
|
||||
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
|
||||
}
|
||||
if (nextChild) {
|
||||
this._mountChildByNameAtIndex(
|
||||
nextChild, name, nextIndex, transaction
|
||||
);
|
||||
}
|
||||
}
|
||||
if (nextChild) {
|
||||
nextIndex++;
|
||||
}
|
||||
}
|
||||
// Remove children that are no longer present.
|
||||
for (name in prevChildren) {
|
||||
if (prevChildren.hasOwnProperty(name) &&
|
||||
prevChildren[name] &&
|
||||
!(nextChildren && nextChildren[name])) {
|
||||
this._unmountChildByName(prevChildren[name], name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmounts all rendered children. This should be used to clean up children
|
||||
* when this component is unmounted.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
unmountChildren: function() {
|
||||
var renderedChildren = this._renderedChildren;
|
||||
for (var name in renderedChildren) {
|
||||
var renderedChild = renderedChildren[name];
|
||||
if (renderedChild && renderedChild.unmountComponent) {
|
||||
renderedChild.unmountComponent();
|
||||
}
|
||||
}
|
||||
this._renderedChildren = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a child component to the supplied index.
|
||||
*
|
||||
* @param {ReactComponent} child Component to move.
|
||||
* @param {number} toIndex Destination index of the element.
|
||||
* @param {number} lastIndex Last index visited of the siblings of `child`.
|
||||
* @protected
|
||||
*/
|
||||
moveChild: function(child, toIndex, lastIndex) {
|
||||
// If the index of `child` is less than `lastIndex`, then it needs to
|
||||
// be moved. Otherwise, we do not need to move it because a child will be
|
||||
// inserted or moved before `child`.
|
||||
if (child._mountIndex < lastIndex) {
|
||||
enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a child component.
|
||||
*
|
||||
* @param {ReactComponent} child Component to create.
|
||||
* @protected
|
||||
*/
|
||||
createChild: function(child) {
|
||||
enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a child component.
|
||||
*
|
||||
* @param {ReactComponent} child Child to remove.
|
||||
* @protected
|
||||
*/
|
||||
removeChild: function(child) {
|
||||
enqueueRemove(this._rootNodeID, child._mountIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mounts a child with the supplied name.
|
||||
*
|
||||
* NOTE: This is part of `updateChildren` and is here for readability.
|
||||
*
|
||||
* @param {ReactComponent} child Component to mount.
|
||||
* @param {string} name Name of the child.
|
||||
* @param {number} index Index at which to insert the child.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @private
|
||||
*/
|
||||
_mountChildByNameAtIndex: function(child, name, index, transaction) {
|
||||
// Inlined for performance, see `ReactID.createReactID`.
|
||||
var rootID = this._rootNodeID + '.' + name;
|
||||
var mountImage = child.mountComponent(rootID, transaction);
|
||||
child._mountImage = mountImage;
|
||||
child._mountIndex = index;
|
||||
this.createChild(child);
|
||||
this._renderedChildren = this._renderedChildren || {};
|
||||
this._renderedChildren[name] = child;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmounts a rendered child by name.
|
||||
*
|
||||
* NOTE: This is part of `updateChildren` and is here for readability.
|
||||
*
|
||||
* @param {ReactComponent} child Component to unmount.
|
||||
* @param {string} name Name of the child in `this._renderedChildren`.
|
||||
* @private
|
||||
*/
|
||||
_unmountChildByName: function(child, name) {
|
||||
if (ReactComponent.isValidComponent(child)) {
|
||||
this.removeChild(child);
|
||||
child._mountImage = null;
|
||||
child._mountIndex = null;
|
||||
child.unmountComponent();
|
||||
delete this._renderedChildren[name];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactMultiChild;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactMultiChildUpdateTypes
|
||||
*/
|
||||
|
||||
var keyMirror = require('keyMirror');
|
||||
|
||||
/**
|
||||
* When a component's children are updated, a series of update configuration
|
||||
* objects are created in order to batch and serialize the required changes.
|
||||
*
|
||||
* Enumerates all the possible types of update configurations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
var ReactMultiChildUpdateTypes = keyMirror({
|
||||
INSERT_MARKUP: null,
|
||||
MOVE_EXISTING: null,
|
||||
REMOVE_NODE: null
|
||||
});
|
||||
|
||||
module.exports = ReactMultiChildUpdateTypes;
|
||||
@@ -160,10 +160,11 @@ ReactNativeComponent.Mixin = {
|
||||
if (contentToUse != null) {
|
||||
return escapeTextForBrowser(contentToUse);
|
||||
} else if (childrenToUse != null) {
|
||||
return this.mountMultiChild(
|
||||
var mountImages = this.mountChildren(
|
||||
flattenChildren(childrenToUse),
|
||||
transaction
|
||||
);
|
||||
return mountImages.join('');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
@@ -320,7 +321,7 @@ ReactNativeComponent.Mixin = {
|
||||
if (contentToUse != null) {
|
||||
var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
|
||||
if (childrenRemoved) {
|
||||
this.updateMultiChild(null, transaction);
|
||||
this.updateChildren(null, transaction);
|
||||
}
|
||||
if (lastUsedContent !== contentToUse) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
@@ -336,7 +337,7 @@ ReactNativeComponent.Mixin = {
|
||||
''
|
||||
);
|
||||
}
|
||||
this.updateMultiChild(flattenChildren(nextProps.children), transaction);
|
||||
this.updateChildren(flattenChildren(nextProps.children), transaction);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -349,7 +350,7 @@ ReactNativeComponent.Mixin = {
|
||||
unmountComponent: function() {
|
||||
ReactEventEmitter.deleteAllListeners(this._rootNodeID);
|
||||
ReactComponent.Mixin.unmountComponent.call(this);
|
||||
this.unmountMultiChild();
|
||||
this.unmountChildren();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
describe('ReactMultiChild', function() {
|
||||
var React;
|
||||
var setInnerHTML;
|
||||
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
React = require('React');
|
||||
|
||||
var innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
|
||||
Element.prototype,
|
||||
'innerHTML'
|
||||
);
|
||||
Object.defineProperty(Element.prototype, 'innerHTML', {
|
||||
set: setInnerHTML = jasmine.createSpy().andCallFake(
|
||||
innerHTMLDescriptor.set
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
it('should only set `innerHTML` once on update', function() {
|
||||
var container = document.createElement('div');
|
||||
|
||||
React.renderComponent(
|
||||
<div>
|
||||
<p><span /></p>
|
||||
<p><span /></p>
|
||||
<p><span /></p>
|
||||
</div>,
|
||||
container
|
||||
);
|
||||
expect(setInnerHTML).toHaveBeenCalled();
|
||||
var callCountOnMount = setInnerHTML.callCount;
|
||||
|
||||
React.renderComponent(
|
||||
<div>
|
||||
<p><span /><span /></p>
|
||||
<p><span /><span /></p>
|
||||
<p><span /><span /></p>
|
||||
</div>,
|
||||
container
|
||||
);
|
||||
expect(setInnerHTML.callCount).toBe(callCountOnMount + 1);
|
||||
});
|
||||
});
|
||||
@@ -94,7 +94,7 @@ var FriendsStatusDisplay = React.createClass({
|
||||
var child = statusDisplays[name];
|
||||
var isPresent = !!child;
|
||||
if (isPresent) {
|
||||
orderOfUsernames[child._domIndex] = getOriginalKey(name);
|
||||
orderOfUsernames[child._mountIndex] = getOriginalKey(name);
|
||||
}
|
||||
}
|
||||
var res = {};
|
||||
|
||||
@@ -14,131 +14,108 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule DOMChildrenOperations
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
// Empty blocks improve readability so disable that warning
|
||||
// jshint -W035
|
||||
|
||||
"use strict";
|
||||
|
||||
var Danger = require('Danger');
|
||||
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
|
||||
|
||||
var insertNodeAt = require('insertNodeAt');
|
||||
var keyOf = require('keyOf');
|
||||
var throwIf = require('throwIf');
|
||||
|
||||
var NON_INCREASING_OPERATIONS;
|
||||
if (__DEV__) {
|
||||
NON_INCREASING_OPERATIONS =
|
||||
'DOM child management operations must be provided in order ' +
|
||||
'of increasing destination index. This is likely an issue with ' +
|
||||
'the core framework.';
|
||||
/**
|
||||
* Inserts `childNode` as a child of `parentNode` at the `index`.
|
||||
*
|
||||
* @param {DOMElement} parentNode Parent node in which to insert.
|
||||
* @param {DOMElement} childNode Child node to insert.
|
||||
* @param {number} index Index at which to insert the child.
|
||||
* @internal
|
||||
*/
|
||||
function insertChildAt(parentNode, childNode, index) {
|
||||
var childNodes = parentNode.childNodes;
|
||||
if (childNodes[index] === childNode) {
|
||||
return;
|
||||
}
|
||||
// If `childNode` is already a child of `parentNode`, remove it so that
|
||||
// computing `childNodes[index]` takes into account the removal.
|
||||
if (childNode.parentNode === parentNode) {
|
||||
parentNode.removeChild(childNode);
|
||||
}
|
||||
if (index >= childNodes.length) {
|
||||
parentNode.appendChild(childNode);
|
||||
} else {
|
||||
parentNode.insertBefore(childNode, childNodes[index]);
|
||||
}
|
||||
}
|
||||
|
||||
var MOVE_NODE_AT_ORIG_INDEX = keyOf({moveFrom: null});
|
||||
var INSERT_MARKUP = keyOf({insertMarkup: null});
|
||||
var REMOVE_AT = keyOf({removeAt: null});
|
||||
|
||||
/**
|
||||
* In order to carry out movement of DOM nodes without knowing their IDs, we
|
||||
* have to first store knowledge about nodes' original indices before beginning
|
||||
* to carry out the sequence of operations. Once we begin the sequence, the DOM
|
||||
* indices in future instructions are no longer valid.
|
||||
*
|
||||
* @param {Element} parent Parent DOM node.
|
||||
* @param {Object} childOperations Description of child operations.
|
||||
* @return {Array?} Sparse array containing elements by their current index in
|
||||
* the DOM.
|
||||
*/
|
||||
var _getNodesByOriginalIndex = function(parent, childOperations) {
|
||||
var nodesByOriginalIndex; // Sparse array.
|
||||
var childOperation;
|
||||
var origIndex;
|
||||
for (var i = 0; i < childOperations.length; i++) {
|
||||
childOperation = childOperations[i];
|
||||
if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
|
||||
nodesByOriginalIndex = nodesByOriginalIndex || [];
|
||||
origIndex = childOperation.moveFrom;
|
||||
nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
|
||||
} else if (REMOVE_AT in childOperation) {
|
||||
nodesByOriginalIndex = nodesByOriginalIndex || [];
|
||||
origIndex = childOperation.removeAt;
|
||||
nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
|
||||
}
|
||||
}
|
||||
return nodesByOriginalIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes DOM elements from their parent, or moved.
|
||||
* @param {Element} parent Parent DOM node.
|
||||
* @param {Array} nodesByOriginalIndex Child nodes by their original index
|
||||
* (potentially sparse.)
|
||||
*/
|
||||
var _removeChildrenByOriginalIndex = function(parent, nodesByOriginalIndex) {
|
||||
for (var j = 0; j < nodesByOriginalIndex.length; j++) {
|
||||
var nodeToRemove = nodesByOriginalIndex[j];
|
||||
if (nodeToRemove) { // We used a sparse array.
|
||||
parent.removeChild(nodesByOriginalIndex[j]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Once all nodes that will be removed or moved - are removed from the parent
|
||||
* node, we can begin the process of placing nodes into their final locations.
|
||||
* We must perform all operations in the order of the final DOM index -
|
||||
* otherwise, we couldn't count on the fact that an insertion at index X, will
|
||||
* remain at index X. This will iterate through the child operations, adding
|
||||
* content where needed, skip over removals (they've already been removed) and
|
||||
* insert "moved" Elements that were previously removed. The "moved" elements
|
||||
* are only temporarily removed from the parent, so that index calculations can
|
||||
* be manageable and perform well in the cases that matter.
|
||||
*/
|
||||
var _placeNodesAtDestination =
|
||||
function(parent, childOperations, nodesByOriginalIndex) {
|
||||
var origNode;
|
||||
var finalIndex;
|
||||
var lastFinalIndex = -1;
|
||||
var childOperation;
|
||||
for (var k = 0; k < childOperations.length; k++) {
|
||||
childOperation = childOperations[k];
|
||||
if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
|
||||
origNode = nodesByOriginalIndex[childOperation.moveFrom];
|
||||
finalIndex = childOperation.finalIndex;
|
||||
insertNodeAt(parent, origNode, finalIndex);
|
||||
if (__DEV__) {
|
||||
throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
|
||||
lastFinalIndex = finalIndex;
|
||||
}
|
||||
} else if (REMOVE_AT in childOperation) {
|
||||
} else if (INSERT_MARKUP in childOperation) {
|
||||
finalIndex = childOperation.finalIndex;
|
||||
var markup = childOperation.insertMarkup;
|
||||
Danger.dangerouslyInsertMarkupAt(parent, markup, finalIndex);
|
||||
if (__DEV__) {
|
||||
throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
|
||||
lastFinalIndex = finalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var manageChildren = function(parent, childOperations) {
|
||||
var nodesByOriginalIndex = _getNodesByOriginalIndex(parent, childOperations);
|
||||
if (nodesByOriginalIndex) {
|
||||
_removeChildrenByOriginalIndex(parent, nodesByOriginalIndex);
|
||||
}
|
||||
_placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Also reexport all of the dangerous functions. It helps to have all dangerous
|
||||
* functions located in a single module `Danger`.
|
||||
* Operations for updating with DOM children.
|
||||
*/
|
||||
var DOMChildrenOperations = {
|
||||
|
||||
dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
|
||||
manageChildren: manageChildren
|
||||
|
||||
/**
|
||||
* Updates a component's children by processing a series of updates. The
|
||||
* update configurations are each expected to have a `parentNode` property.
|
||||
*
|
||||
* @param {array<object>} updates List of update configurations.
|
||||
* @param {array<string>} markupList List of markup strings.
|
||||
* @internal
|
||||
*/
|
||||
processUpdates: function(updates, markupList) {
|
||||
var update;
|
||||
// Mapping from parent IDs to initial child orderings.
|
||||
var initialChildren = null;
|
||||
// List of children that will be moved or removed.
|
||||
var updatedChildren = null;
|
||||
|
||||
for (var i = 0; update = updates[i]; i++) {
|
||||
if (update.type !== ReactMultiChildUpdateTypes.INSERT_MARKUP) {
|
||||
var updatedIndex = update.fromIndex;
|
||||
var updatedChild = update.parentNode.childNodes[updatedIndex];
|
||||
var parentID = update.parentID;
|
||||
|
||||
initialChildren = initialChildren || {};
|
||||
initialChildren[parentID] = initialChildren[parentID] || [];
|
||||
initialChildren[parentID][updatedIndex] = updatedChild;
|
||||
|
||||
updatedChildren = updatedChildren || [];
|
||||
updatedChildren.push(updatedChild);
|
||||
}
|
||||
}
|
||||
|
||||
var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
|
||||
|
||||
// Remove updated children first so that `toIndex` is consistent.
|
||||
if (updatedChildren) {
|
||||
for (var j = 0; j < updatedChildren.length; j++) {
|
||||
updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
|
||||
}
|
||||
}
|
||||
|
||||
for (var k = 0; update = updates[k]; k++) {
|
||||
switch (update.type) {
|
||||
case ReactMultiChildUpdateTypes.INSERT_MARKUP:
|
||||
insertChildAt(
|
||||
update.parentNode,
|
||||
renderedMarkup[update.markupIndex],
|
||||
update.toIndex
|
||||
);
|
||||
break;
|
||||
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
|
||||
insertChildAt(
|
||||
update.parentNode,
|
||||
initialChildren[update.parentID][update.fromIndex],
|
||||
update.toIndex
|
||||
);
|
||||
break;
|
||||
case ReactMultiChildUpdateTypes.REMOVE_NODE:
|
||||
// Already removed by the for-loop above.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = DOMChildrenOperations;
|
||||
|
||||
+96
-127
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule Danger
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
/*jslint evil: true, sub: true */
|
||||
@@ -22,33 +23,7 @@
|
||||
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
|
||||
var throwIf = require('throwIf');
|
||||
|
||||
var DOM_UNSUPPORTED;
|
||||
var NO_MARKUP_PARENT;
|
||||
var NO_MULTI_MARKUP;
|
||||
if (__DEV__) {
|
||||
DOM_UNSUPPORTED =
|
||||
'You may not insert markup into the document while you are in a worker ' +
|
||||
'thread. It\'s not you, it\'s me. This is likely the fault of the ' +
|
||||
'framework. Please report this immediately.';
|
||||
NO_MARKUP_PARENT =
|
||||
'You have attempted to inject markup without a suitable parent. This is ' +
|
||||
'likely the fault of the framework - please report immediately.';
|
||||
NO_MULTI_MARKUP =
|
||||
'The framework has attempted to either insert zero or multiple markup ' +
|
||||
'roots into a single location when it should not. This is a serious ' +
|
||||
'error - a fault of the framework - please report immediately.';
|
||||
}
|
||||
|
||||
var validateMarkupParams;
|
||||
if (__DEV__) {
|
||||
validateMarkupParams = function(parentNode, markup) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
throwIf(!parentNode || !parentNode.tagName, NO_MARKUP_PARENT);
|
||||
throwIf(!markup, NO_MULTI_MARKUP);
|
||||
};
|
||||
}
|
||||
var invariant = require('invariant');
|
||||
|
||||
/**
|
||||
* Dummy container used to render all markup.
|
||||
@@ -103,22 +78,35 @@ if (dummyNode) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders markup into nodes. The returned HTMLCollection is live and should be
|
||||
* used immediately (or at least before the next invocation to `renderMarkup`).
|
||||
* Extracts the `nodeName` from a string of markup. This does not require a
|
||||
* regular expression match because we make assumptions about React-generated
|
||||
* markup (i.e. there are no spaces surrounding the opening tag and there is at
|
||||
* least an ID attribute).
|
||||
*
|
||||
* NOTE: Extracting the `nodeName` does not require a regular expression match
|
||||
* because we make assumptions about React-generated markup (i.e. there are no
|
||||
* spaces surrounding the opening tag and there is at least one attribute).
|
||||
* @see http://jsperf.com/extract-nodename
|
||||
*
|
||||
* @param {string} markup
|
||||
* @param {string} markup String of markup.
|
||||
* @return {string} Node name of the supplied markup.
|
||||
* @see http://jsperf.com/extract-nodename
|
||||
*/
|
||||
function getNodeName(markup) {
|
||||
return markup.substring(1, markup.indexOf(' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders markup into nodes. The returned HTMLCollection is live and should be
|
||||
* used immediately (or at least before the next invocation to `renderMarkup`).
|
||||
*
|
||||
* @param {string} markup Markup for one or more nodes with the same `nodeName`.
|
||||
* @param {?string} nodeName Optional, the lowercase node name of the markup.
|
||||
* @return {*} An HTMLCollection.
|
||||
*/
|
||||
function renderMarkup(markup) {
|
||||
function renderMarkup(markup, nodeName) {
|
||||
nodeName = nodeName || getNodeName(markup);
|
||||
var node = dummyNode;
|
||||
var nodeName = markup.substring(1, markup.indexOf(' '));
|
||||
|
||||
var wrap = markupWrap[nodeName.toLowerCase()] || defaultWrap;
|
||||
var wrap = markupWrap[nodeName] || defaultWrap;
|
||||
if (wrap) {
|
||||
node.innerHTML = wrap[1] + markup + wrap[2];
|
||||
|
||||
@@ -132,99 +120,80 @@ function renderMarkup(markup) {
|
||||
return node.childNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts node after 'after'. If 'after' is null, inserts it after nothing,
|
||||
* which is inserting it at the beginning.
|
||||
*
|
||||
* @param {Element} elem Parent element.
|
||||
* @param {Element} insert Element to insert.
|
||||
* @param {Element} after Element to insert after.
|
||||
* @return {Element} Element that was inserted.
|
||||
*/
|
||||
function insertNodeAfterNode(elem, insert, after) {
|
||||
if (__DEV__) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
}
|
||||
if (after) {
|
||||
if (after.nextSibling) {
|
||||
return elem.insertBefore(insert, after.nextSibling);
|
||||
} else {
|
||||
return elem.appendChild(insert);
|
||||
}
|
||||
} else {
|
||||
return elem.insertBefore(insert, elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slow: Should only be used when it is known there are a few (or one) element
|
||||
* in the node list.
|
||||
* @param {Element} parentRootDomNode Parent element.
|
||||
* @param {HTMLCollection} htmlCollection HTMLCollection to insert.
|
||||
* @param {Element} after Element to insert the node list after.
|
||||
*/
|
||||
function inefficientlyInsertHTMLCollectionAfter(
|
||||
parentRootDomNode,
|
||||
htmlCollection,
|
||||
after) {
|
||||
|
||||
if (__DEV__) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
}
|
||||
var ret;
|
||||
var originalLength = htmlCollection.length;
|
||||
// Access htmlCollection[0] because htmlCollection shrinks as we remove items.
|
||||
// `insertNodeAfterNode` will remove items from the htmlCollection.
|
||||
for (var i = 0; i < originalLength; i++) {
|
||||
ret =
|
||||
insertNodeAfterNode(parentRootDomNode, htmlCollection[0], ret || after);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Super-dangerously inserts markup into existing DOM structure. Seriously, you
|
||||
* don't want to use this module unless you are building a framework. This
|
||||
* requires that the markup that you are inserting represents the root of a
|
||||
* tree. We do not support the case where there `markup` represents several
|
||||
* roots.
|
||||
*
|
||||
* @param {Element} parentNode Parent DOM element.
|
||||
* @param {string} markup Markup to dangerously insert.
|
||||
* @param {number} index Position to insert markup at.
|
||||
*/
|
||||
function dangerouslyInsertMarkupAt(parentNode, markup, index) {
|
||||
if (__DEV__) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var htmlCollection = renderMarkup(markup);
|
||||
var afterNode = index ? parentNode.childNodes[index - 1] : null;
|
||||
inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a node with a string of markup at its current position within its
|
||||
* parent. `childNode` must be in the document (or at least within a parent
|
||||
* node). The string of markup must represent a tree of markup with a single
|
||||
* root.
|
||||
*
|
||||
* @param {Element} childNode Child node to replace.
|
||||
* @param {string} markup Markup to dangerously replace child with.
|
||||
*/
|
||||
function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
|
||||
var parentNode = childNode.parentNode;
|
||||
if (__DEV__) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var htmlCollection = renderMarkup(markup);
|
||||
if (__DEV__) {
|
||||
throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
|
||||
}
|
||||
parentNode.replaceChild(htmlCollection[0], childNode);
|
||||
}
|
||||
|
||||
var Danger = {
|
||||
dangerouslyInsertMarkupAt: dangerouslyInsertMarkupAt,
|
||||
dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup
|
||||
|
||||
/**
|
||||
* Renders markup into an array of nodes. The markup is expected to render
|
||||
* into a list of root nodes. Also, the length of `parentNodes` and `markup`
|
||||
* should be the same.
|
||||
*
|
||||
* @param {array<string>} markupList List of markup strings to render.
|
||||
* @return {array<DOMElement>} List of rendered nodes.
|
||||
* @internal
|
||||
*/
|
||||
dangerouslyRenderMarkup: function(markupList) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' +
|
||||
'thread. This is likely a bug in the framework. Please report ' +
|
||||
'immediately.'
|
||||
);
|
||||
var nodeName;
|
||||
var markupByNodeName = {};
|
||||
// Group markup by `nodeName` if a wrap is necessary, else by '*'.
|
||||
for (var i = 0; i < markupList.length; i++) {
|
||||
invariant(
|
||||
markupList[i],
|
||||
'dangerouslyRenderMarkup(...): Missing markup.'
|
||||
);
|
||||
nodeName = getNodeName(markupList[i]);
|
||||
nodeName = markupWrap[nodeName] ? nodeName : '*';
|
||||
markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
|
||||
markupByNodeName[nodeName][i] = markupList[i];
|
||||
}
|
||||
var renderedMarkup = [];
|
||||
for (nodeName in markupByNodeName) {
|
||||
if (!markupByNodeName.hasOwnProperty(nodeName)) {
|
||||
continue;
|
||||
}
|
||||
var markupListByNodeName = markupByNodeName[nodeName];
|
||||
var markup = markupListByNodeName.join('');
|
||||
// Render each group of markup.
|
||||
var childNode = renderMarkup(markup, nodeName)[0];
|
||||
// Restore the initial ordering.
|
||||
for (var j = 0; j < markupListByNodeName.length; j++) {
|
||||
// `markupListByNodeName` may be a sparse array.
|
||||
if (markupListByNodeName[j]) {
|
||||
invariant(childNode, 'dangerouslyRenderMarkup(...): Missing node.');
|
||||
renderedMarkup[j] = childNode;
|
||||
childNode = childNode.nextSibling;
|
||||
}
|
||||
}
|
||||
invariant(!childNode, 'dangerouslyRenderMarkupO(...): Unexpected nodes.');
|
||||
}
|
||||
return renderedMarkup;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces a node with a string of markup at its current position within its
|
||||
* parent. The markup must render into a single root node.
|
||||
*
|
||||
* @param {DOMElement} oldChild Child node to replace.
|
||||
* @param {string} markup Markup to render in place of the child node.
|
||||
* @internal
|
||||
*/
|
||||
dangerouslyReplaceNodeWithMarkup: function(oldChild, markup) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' +
|
||||
'worker thread. This is likely a bug in the framework. Please report ' +
|
||||
'immediately.'
|
||||
);
|
||||
invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.');
|
||||
var newChild = renderMarkup(markup)[0];
|
||||
oldChild.parentNode.replaceChild(newChild, oldChild);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Danger;
|
||||
|
||||
@@ -23,7 +23,7 @@ var React = require('React');
|
||||
|
||||
describe('Danger', function() {
|
||||
|
||||
describe('dangerouslyInsertMarkupAt', function() {
|
||||
describe('dangerouslyRenderMarkup', function() {
|
||||
var Danger;
|
||||
var transaction;
|
||||
|
||||
@@ -37,33 +37,71 @@ describe('Danger', function() {
|
||||
|
||||
it('should render markup', function() {
|
||||
var markup = (<div />).mountComponent('.rX', transaction);
|
||||
var parent = document.createElement('div');
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
|
||||
|
||||
expect(parent.innerHTML).toBe('<div data-reactid=".rX"></div>');
|
||||
expect(output.nodeName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('should render markup with props', function() {
|
||||
var markup = (<div className="foo" />).mountComponent('.rX', transaction);
|
||||
var parent = document.createElement('div');
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
|
||||
|
||||
expect(parent.innerHTML).toBe(
|
||||
'<div class="foo" data-reactid=".rX"></div>'
|
||||
);
|
||||
expect(output.nodeName).toBe('DIV');
|
||||
expect(output.className).toBe('foo');
|
||||
});
|
||||
|
||||
it('should render wrapped markup', function() {
|
||||
var markup = (<th />).mountComponent('.rX', transaction);
|
||||
var parent = document.createElement('div');
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
|
||||
|
||||
expect(parent.innerHTML).toBe('<th data-reactid=".rX"></th>');
|
||||
expect(output.nodeName).toBe('TH');
|
||||
});
|
||||
|
||||
it('should render lists of markup with similar `nodeName`', function() {
|
||||
var renderedMarkup = Danger.dangerouslyRenderMarkup(
|
||||
['<p>1</p>', '<p>2</p>', '<p>3</p>']
|
||||
);
|
||||
|
||||
expect(renderedMarkup.length).toBe(3);
|
||||
|
||||
expect(renderedMarkup[0].nodeName).toBe('P');
|
||||
expect(renderedMarkup[1].nodeName).toBe('P');
|
||||
expect(renderedMarkup[2].nodeName).toBe('P');
|
||||
|
||||
expect(renderedMarkup[0].innerHTML).toBe('1');
|
||||
expect(renderedMarkup[1].innerHTML).toBe('2');
|
||||
expect(renderedMarkup[2].innerHTML).toBe('3');
|
||||
});
|
||||
|
||||
it('should render lists of markup with different `nodeName`', function() {
|
||||
var renderedMarkup = Danger.dangerouslyRenderMarkup(
|
||||
['<p>1</p>', '<tr>2</tr>', '<p>3</p>']
|
||||
);
|
||||
|
||||
expect(renderedMarkup.length).toBe(3);
|
||||
|
||||
expect(renderedMarkup[0].nodeName).toBe('P');
|
||||
expect(renderedMarkup[1].nodeName).toBe('TR');
|
||||
expect(renderedMarkup[2].nodeName).toBe('P');
|
||||
|
||||
expect(renderedMarkup[0].innerHTML).toBe('1');
|
||||
expect(renderedMarkup[1].innerHTML).toBe('2');
|
||||
expect(renderedMarkup[2].innerHTML).toBe('3');
|
||||
});
|
||||
|
||||
it('should throw when rendering invalid markup', function() {
|
||||
expect(function() {
|
||||
Danger.dangerouslyRenderMarkup(['']);
|
||||
}).toThrow(
|
||||
'Invariant Violation: dangerouslyRenderMarkup(...): Missing markup.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
Danger.dangerouslyRenderMarkup(['<p></p><p></p>']);
|
||||
}).toThrow(
|
||||
'Invariant Violation: dangerouslyRenderMarkupO(...): Unexpected nodes.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule insertNodeAt
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Inserts `node` at a particular child index. Other nodes move to make room.
|
||||
* @param {!Element} root The parent root node to insert into.
|
||||
* @param {!node} node The node to insert.
|
||||
* @param {!number} atIndex The index in `root` that `node` should exist at.
|
||||
*/
|
||||
function insertNodeAt(root, node, atIndex) {
|
||||
var childNodes = root.childNodes;
|
||||
// Remove from parent so that if node is already child of root,
|
||||
// `childNodes[atIndex]` already takes into account the removal.
|
||||
var curAtIndex = root.childNodes[atIndex];
|
||||
if (curAtIndex === node) {
|
||||
return node;
|
||||
}
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
if (atIndex >= childNodes.length) {
|
||||
root.appendChild(node);
|
||||
} else {
|
||||
root.insertBefore(node, childNodes[atIndex]);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
module.exports = insertNodeAt;
|
||||
Reference in New Issue
Block a user