You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
12 KiB
428 lines
12 KiB
/** |
|
* Copyright 2013-2015, Facebook, Inc. |
|
* All rights reserved. |
|
* |
|
* This source code is licensed under the BSD-style license found in the |
|
* LICENSE file in the root directory of this source tree. An additional grant |
|
* of patent rights can be found in the PATENTS file in the same directory. |
|
* |
|
* @providesModule ReactMultiChild |
|
* @typechecks static-only |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var ReactComponentEnvironment = require("./ReactComponentEnvironment"); |
|
var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); |
|
|
|
var ReactReconciler = require("./ReactReconciler"); |
|
var ReactChildReconciler = require("./ReactChildReconciler"); |
|
|
|
/** |
|
* Updating children of a component may trigger recursive updates. The depth is |
|
* used to batch recursive updates to render markup more efficiently. |
|
* |
|
* @type {number} |
|
* @private |
|
*/ |
|
var updateDepth = 0; |
|
|
|
/** |
|
* Queue of update configuration objects. |
|
* |
|
* Each object has a `type` property that is in `ReactMultiChildUpdateTypes`. |
|
* |
|
* @type {array<object>} |
|
* @private |
|
*/ |
|
var updateQueue = []; |
|
|
|
/** |
|
* Queue of markup to be rendered. |
|
* |
|
* @type {array<string>} |
|
* @private |
|
*/ |
|
var markupQueue = []; |
|
|
|
/** |
|
* 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, |
|
textContent: null, |
|
fromIndex: null, |
|
toIndex: toIndex |
|
}); |
|
} |
|
|
|
/** |
|
* 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, |
|
textContent: null, |
|
fromIndex: fromIndex, |
|
toIndex: toIndex |
|
}); |
|
} |
|
|
|
/** |
|
* 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, |
|
textContent: null, |
|
fromIndex: fromIndex, |
|
toIndex: null |
|
}); |
|
} |
|
|
|
/** |
|
* Enqueues setting the text content. |
|
* |
|
* @param {string} parentID ID of the parent component. |
|
* @param {string} textContent Text content to set. |
|
* @private |
|
*/ |
|
function enqueueTextContent(parentID, textContent) { |
|
// NOTE: Null values reduce hidden classes. |
|
updateQueue.push({ |
|
parentID: parentID, |
|
parentNode: null, |
|
type: ReactMultiChildUpdateTypes.TEXT_CONTENT, |
|
markupIndex: null, |
|
textContent: textContent, |
|
fromIndex: null, |
|
toIndex: null |
|
}); |
|
} |
|
|
|
/** |
|
* Processes any enqueued updates. |
|
* |
|
* @private |
|
*/ |
|
function processQueue() { |
|
if (updateQueue.length) { |
|
ReactComponentEnvironment.processChildrenUpdates( |
|
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 = { |
|
|
|
/** |
|
* Provides common functionality for components that must reconcile multiple |
|
* children. This is used by `ReactDOMComponent` 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 `ReactDOMComponent`, a mount image is a string of markup. |
|
* |
|
* @param {?object} nestedChildren Nested child maps. |
|
* @return {array} An array of mounted representations. |
|
* @internal |
|
*/ |
|
mountChildren: function(nestedChildren, transaction, context) { |
|
var children = ReactChildReconciler.instantiateChildren( |
|
nestedChildren, transaction, context |
|
); |
|
this._renderedChildren = children; |
|
var mountImages = []; |
|
var index = 0; |
|
for (var name in children) { |
|
if (children.hasOwnProperty(name)) { |
|
var child = children[name]; |
|
// Inlined for performance, see `ReactInstanceHandles.createReactID`. |
|
var rootID = this._rootNodeID + name; |
|
var mountImage = ReactReconciler.mountComponent( |
|
child, |
|
rootID, |
|
transaction, |
|
context |
|
); |
|
child._mountIndex = index; |
|
mountImages.push(mountImage); |
|
index++; |
|
} |
|
} |
|
return mountImages; |
|
}, |
|
|
|
/** |
|
* Replaces any rendered children with a text content string. |
|
* |
|
* @param {string} nextContent String of content. |
|
* @internal |
|
*/ |
|
updateTextContent: function(nextContent) { |
|
updateDepth++; |
|
var errorThrown = true; |
|
try { |
|
var prevChildren = this._renderedChildren; |
|
// Remove any rendered children. |
|
ReactChildReconciler.unmountChildren(prevChildren); |
|
// TODO: The setTextContent operation should be enough |
|
for (var name in prevChildren) { |
|
if (prevChildren.hasOwnProperty(name)) { |
|
this._unmountChildByName(prevChildren[name], name); |
|
} |
|
} |
|
// Set new text content. |
|
this.setTextContent(nextContent); |
|
errorThrown = false; |
|
} finally { |
|
updateDepth--; |
|
if (!updateDepth) { |
|
if (errorThrown) { |
|
clearQueue(); |
|
} else { |
|
processQueue(); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Updates the rendered children with new children. |
|
* |
|
* @param {?object} nextNestedChildren Nested child maps. |
|
* @param {ReactReconcileTransaction} transaction |
|
* @internal |
|
*/ |
|
updateChildren: function(nextNestedChildren, transaction, context) { |
|
updateDepth++; |
|
var errorThrown = true; |
|
try { |
|
this._updateChildren(nextNestedChildren, transaction, context); |
|
errorThrown = false; |
|
} finally { |
|
updateDepth--; |
|
if (!updateDepth) { |
|
if (errorThrown) { |
|
clearQueue(); |
|
} else { |
|
processQueue(); |
|
} |
|
} |
|
|
|
} |
|
}, |
|
|
|
/** |
|
* Improve performance by isolating this hot code path from the try/catch |
|
* block in `updateChildren`. |
|
* |
|
* @param {?object} nextNestedChildren Nested child maps. |
|
* @param {ReactReconcileTransaction} transaction |
|
* @final |
|
* @protected |
|
*/ |
|
_updateChildren: function(nextNestedChildren, transaction, context) { |
|
var prevChildren = this._renderedChildren; |
|
var nextChildren = ReactChildReconciler.updateChildren( |
|
prevChildren, nextNestedChildren, transaction, context |
|
); |
|
this._renderedChildren = nextChildren; |
|
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 (prevChild === nextChild) { |
|
this.moveChild(prevChild, nextIndex, lastIndex); |
|
lastIndex = Math.max(prevChild._mountIndex, lastIndex); |
|
prevChild._mountIndex = nextIndex; |
|
} else { |
|
if (prevChild) { |
|
// Update `lastIndex` before `_mountIndex` gets unset by unmounting. |
|
lastIndex = Math.max(prevChild._mountIndex, lastIndex); |
|
this._unmountChildByName(prevChild, name); |
|
} |
|
// The child must be instantiated before it's mounted. |
|
this._mountChildByNameAtIndex( |
|
nextChild, name, nextIndex, transaction, context |
|
); |
|
} |
|
nextIndex++; |
|
} |
|
// Remove children that are no longer present. |
|
for (name in prevChildren) { |
|
if (prevChildren.hasOwnProperty(name) && |
|
!(nextChildren && nextChildren.hasOwnProperty(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; |
|
ReactChildReconciler.unmountChildren(renderedChildren); |
|
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. |
|
* @param {string} mountImage Markup to insert. |
|
* @protected |
|
*/ |
|
createChild: function(child, mountImage) { |
|
enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex); |
|
}, |
|
|
|
/** |
|
* Removes a child component. |
|
* |
|
* @param {ReactComponent} child Child to remove. |
|
* @protected |
|
*/ |
|
removeChild: function(child) { |
|
enqueueRemove(this._rootNodeID, child._mountIndex); |
|
}, |
|
|
|
/** |
|
* Sets this text content string. |
|
* |
|
* @param {string} textContent Text content to set. |
|
* @protected |
|
*/ |
|
setTextContent: function(textContent) { |
|
enqueueTextContent(this._rootNodeID, textContent); |
|
}, |
|
|
|
/** |
|
* 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, |
|
context) { |
|
// Inlined for performance, see `ReactInstanceHandles.createReactID`. |
|
var rootID = this._rootNodeID + name; |
|
var mountImage = ReactReconciler.mountComponent( |
|
child, |
|
rootID, |
|
transaction, |
|
context |
|
); |
|
child._mountIndex = index; |
|
this.createChild(child, mountImage); |
|
}, |
|
|
|
/** |
|
* 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) { |
|
this.removeChild(child); |
|
child._mountIndex = null; |
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
module.exports = ReactMultiChild;
|
|
|