/** * 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} * @private */ var updateQueue = []; /** * Queue of markup to be rendered. * * @type {array} * @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;