@samitouri / QOS-React-2 / commits / adffa9b0f4
Fork 0

The library for web and native user interfaces.

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.

Tim Yung committed Jul 26, 2013 at 11:22 UTC adffa9b0f4128ca4832ddd03fdeca2c9fe434689
11 files changed +666 -452
src/core/ReactDOMIDOperations.js
+10 -5
@@ -172,12 +172,17 @@ var ReactDOMIDOperations = {
172 },
173
174 /**
175 - * TODO: We only actually *need* to purge the cache when we remove elements.
176 - * Detect if any elements were removed instead of blindly purging.
175 + * Updates a component's children by processing a series of updates.
176 + *
177 + * @param {array<object>} updates List of update configurations.
178 + * @param {array<string>} markup List of markup strings.
179 + * @internal
180 */
178 - manageChildrenByParentID: function(parentID, domOperations) {
179 - var parent = ReactMount.getNode(parentID);
180 - DOMChildrenOperations.manageChildren(parent, domOperations);
181 + dangerouslyProcessChildrenUpdates: function(updates, markup) {
182 + for (var i = 0; i < updates.length; i++) {
183 + updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
184 + }
185 + DOMChildrenOperations.processUpdates(updates, markup);
186 }
187
188 };
src/core/ReactInstanceHandles.js
+12 -2
@@ -229,14 +229,24 @@ function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
229 */
230 var ReactInstanceHandles = {
231
232 - separator: SEPARATOR,
233 -
232 createReactRootID: function() {
233 return getReactRootIDString(
234 Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX)
235 );
236 },
237
238 + /**
239 + * Constructs a React ID by joining a root ID with a name.
240 + *
241 + * @param {string} rootID Root ID of a parent component.
242 + * @param {string} name A component's name (as flattened children).
243 + * @return {string} A React ID.
244 + * @internal
245 + */
246 + createReactID: function(rootID, name) {
247 + return rootID + SEPARATOR + name;
248 + },
249 +
250 /**
251 * Gets the DOM ID of the React component that is the root of the tree that
252 * contains the React component with the supplied DOM ID.
src/core/ReactMultiChild.js
+313 -151
@@ -14,194 +14,356 @@
14 * limitations under the License.
15 *
16 * @providesModule ReactMultiChild
17 + * @typechecks static-only
18 */
19
20 "use strict";
21
22 var ReactComponent = require('ReactComponent');
23 +var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
24
25 /**
24 - * Given a `curChild` and `newChild`, determines if `curChild` should be managed
25 - * as it exists, as opposed to being destroyed and/or replaced.
26 + * Given a `curChild` and `newChild`, determines if `curChild` should be
27 + * updated as opposed to being destroyed or replaced.
28 + *
29 * @param {?ReactComponent} curChild
30 * @param {?ReactComponent} newChild
28 - * @return {!boolean} Whether or not `curChild` should be updated with
29 - * `newChild`'s props
31 + * @return {boolean} True if `curChild` should be updated with `newChild`.
32 + * @protected
33 */
31 -function shouldManageExisting(curChild, newChild) {
34 +function shouldUpdateChild(curChild, newChild) {
35 return curChild && newChild && curChild.constructor === newChild.constructor;
36 }
37
38 /**
36 - * `ReactMultiChild` provides common functionality for components that have
37 - * multiple children. Standard `ReactCompositeComponent`s do not currently have
38 - * multiple children. `ReactNativeComponent`s do, however. Other specially
39 - * reconciled components will also have multiple children. Contains three
40 - * internally used properties that are used to keep track of state throughout
41 - * the `updateMultiChild` process.
39 + * Updating children of a component may trigger recursive updates. The depth is
40 + * used to batch recursive updates to render markup more efficiently.
41 *
43 - * @class ReactMultiChild
42 + * @type {number}
43 + * @private
44 */
45 +var updateDepth = 0;
46
47 /**
47 - * @lends `ReactMultiChildMixin`.
48 + * Queue of update configuration objects.
49 + *
50 + * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
51 + *
52 + * @type {array<object>}
53 + * @private
54 */
49 -var ReactMultiChildMixin = {
50 -
51 - enqueueMarkupAt: function(markup, insertAt) {
52 - this.domOperations = this.domOperations || [];
53 - this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
54 - },
55 -
56 - enqueueMove: function(originalIndex, finalIndex) {
57 - this.domOperations = this.domOperations || [];
58 - this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
59 - },
60 -
61 - enqueueUnmountChildByName: function(name, removeChild) {
62 - if (ReactComponent.isValidComponent(removeChild)) {
63 - this.domOperations = this.domOperations || [];
64 - this.domOperations.push({removeAt: removeChild._domIndex});
65 - removeChild.unmountComponent && removeChild.unmountComponent();
66 - delete this._renderedChildren[name];
67 - }
68 - },
55 +var updateQueue = [];
56 +
57 +/**
58 + * Queue of markup to be rendered.
59 + *
60 + * @type {array<string>}
61 + * @private
62 + */
63 +var markupQueue = [];
64 +
65 +/**
66 + * Enqueues markup to be rendered and inserted at a supplied index.
67 + *
68 + * @param {string} parentID ID of the parent component.
69 + * @param {string} markup Markup that renders into an element.
70 + * @param {number} toIndex Destination index.
71 + * @private
72 + */
73 +function enqueueMarkup(parentID, markup, toIndex) {
74 + // NOTE: Null values reduce hidden classes.
75 + updateQueue.push({
76 + parentID: parentID,
77 + parentNode: null,
78 + type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
79 + markupIndex: markupQueue.push(markup) - 1,
80 + fromIndex: null,
81 + toIndex: toIndex
82 + });
83 +}
84 +
85 +/**
86 + * Enqueues moving an existing element to another index.
87 + *
88 + * @param {string} parentID ID of the parent component.
89 + * @param {number} fromIndex Source index of the existing element.
90 + * @param {number} toIndex Destination index of the element.
91 + * @private
92 + */
93 +function enqueueMove(parentID, fromIndex, toIndex) {
94 + // NOTE: Null values reduce hidden classes.
95 + updateQueue.push({
96 + parentID: parentID,
97 + parentNode: null,
98 + type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
99 + markupIndex: null,
100 + fromIndex: fromIndex,
101 + toIndex: toIndex
102 + });
103 +}
104 +
105 +/**
106 + * Enqueues removing an element at an index.
107 + *
108 + * @param {string} parentID ID of the parent component.
109 + * @param {number} fromIndex Index of the element to remove.
110 + * @private
111 + */
112 +function enqueueRemove(parentID, fromIndex) {
113 + // NOTE: Null values reduce hidden classes.
114 + updateQueue.push({
115 + parentID: parentID,
116 + parentNode: null,
117 + type: ReactMultiChildUpdateTypes.REMOVE_NODE,
118 + markupIndex: null,
119 + fromIndex: fromIndex,
120 + toIndex: null
121 + });
122 +}
123 +
124 +/**
125 + * Processes any enqueued updates.
126 + *
127 + * @private
128 + */
129 +function processQueue() {
130 + if (updateQueue.length) {
131 + ReactComponent.DOMIDOperations.dangerouslyProcessChildrenUpdates(
132 + updateQueue,
133 + markupQueue
134 + );
135 + clearQueue();
136 + }
137 +}
138 +
139 +/**
140 + * Clears any enqueued updates.
141 + *
142 + * @private
143 + */
144 +function clearQueue() {
145 + updateQueue.length = 0;
146 + markupQueue.length = 0;
147 +}
148 +
149 +/**
150 + * ReactMultiChild are capable of reconciling multiple children.
151 + *
152 + * @class ReactMultiChild
153 + * @internal
154 + */
155 +var ReactMultiChild = {
156
157 /**
71 - * Process any pending DOM operations that have been accumulated when updating
72 - * the UI. By default, we execute the injected `DOMIDOperations` module's
73 - * `manageChildrenByParentID` which does executes the DOM operations without
74 - * any animation. It can be used as a reference implementation for special
75 - * animation based implementations.
158 + * Provides common functionality for components that must reconcile multiple
159 + * children. This is used by `ReactNativeComponent` to mount, update, and
160 + * unmount child components.
161 *
77 - * @abstract
162 + * @lends {ReactMultiChild.prototype}
163 */
79 - processChildDOMOperationsQueue: function() {
80 - if (this.domOperations) {
81 - ReactComponent.DOMIDOperations
82 - .manageChildrenByParentID(this._rootNodeID, this.domOperations);
83 - this.domOperations = null;
84 - }
85 - },
164 + Mixin: {
165
87 - unmountMultiChild: function() {
88 - var renderedChildren = this._renderedChildren;
89 - for (var name in renderedChildren) {
90 - if (renderedChildren.hasOwnProperty(name) && renderedChildren[name]) {
91 - var renderedChild = renderedChildren[name];
92 - renderedChild.unmountComponent && renderedChild.unmountComponent();
166 + /**
167 + * Generates a "mount image" for each of the supplied children. In the case
168 + * of `ReactNativeComponent`, a mount image is a string of markup.
169 + *
170 + * @param {?object} children As returned by `flattenChildren`.
171 + * @return {array} An array of mounted representations.
172 + * @internal
173 + */
174 + mountChildren: function(children, transaction) {
175 + var mountImages = [];
176 + var index = 0;
177 + for (var name in children) {
178 + var child = children[name];
179 + if (children.hasOwnProperty(name) && child) {
180 + var mountImage = child.mountComponent(
181 + // Inlined for performance, see `ReactID.createReactID`.
182 + this._rootNodeID + '.' + name,
183 + transaction
184 + );
185 + child._mountImage = mountImage;
186 + child._mountIndex = index;
187 + mountImages.push(mountImage);
188 + index++;
189 + }
190 }
94 - }
95 - this._renderedChildren = null;
96 - },
191 + this._renderedChildren = children;
192 + return mountImages;
193 + },
194
98 - /**
99 - * Generates markup for a component that holds multiple children. #todo: Allow
100 - * all `ReactMultiChildMixin`s to support having arrays of children without a
101 - * container node. This current implementation may assume that children exist
102 - * at domIndices [0, parentNode.length].
103 - *
104 - * Has side effects of (likely) causing events to be registered. Also, every
105 - * component instance may only be rendered once.
106 - *
107 - * @param {?Object} children Flattened children object.
108 - * @return {!String} The rendered markup.
109 - */
110 - mountMultiChild: function(children, transaction) {
111 - var accum = '';
112 - var index = 0;
113 - for (var name in children) {
114 - var child = children[name];
115 - if (children.hasOwnProperty(name) && child) {
116 - accum += child.mountComponent(
117 - this._rootNodeID + '.' + name,
118 - transaction
119 - );
120 - child._domIndex = index;
121 - index++;
195 + /**
196 + * Updates the rendered children with new children.
197 + *
198 + * @param {?object} nextChildren As returned by `flattenChildren`.
199 + * @param {ReactReconcileTransaction} transaction
200 + * @internal
201 + */
202 + updateChildren: function(nextChildren, transaction) {
203 + updateDepth++;
204 + try {
205 + this._updateChildren(nextChildren, transaction);
206 + } catch (error) {
207 + updateDepth--;
208 + updateDepth || clearQueue();
209 + throw error;
210 }
123 - }
124 - this._renderedChildren = children; // children are in just the right form!
125 - this.domOperations = null;
126 - return accum;
127 - },
211 + updateDepth--;
212 + updateDepth || processQueue();
213 + },
214
129 - /**
130 - * Reconciles new children with old children in three phases.
131 - *
132 - * - Adds new content while updating existing children that should remain.
133 - * - Remove children that are no longer present in the next children.
134 - * - As a very last step, moves existing dom structures around.
135 - * - (Comment 1) `curChildrenDOMIndex` is the largest index of the current
136 - * rendered children that appears in the next children and did not need to
137 - * be "moved".
138 - * - (Comment 2) This is the key insight. If any non-removed child's previous
139 - * index is less than `curChildrenDOMIndex` it must be moved.
140 - *
141 - * @param {?Object} children Flattened children object.
142 - */
143 - updateMultiChild: function(nextChildren, transaction) {
144 - if (!nextChildren && !this._renderedChildren) {
145 - return;
146 - } else if (nextChildren && !this._renderedChildren) {
147 - this._renderedChildren = {}; // lazily allocate backing store with nothing
148 - } else if (!nextChildren && this._renderedChildren) {
149 - nextChildren = {};
150 - }
151 - var rootDomIdDot = this._rootNodeID + '.';
152 - var markupBuffer = null; // Accumulate adjacent new children markup.
153 - var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
154 - var loopDomIndex = 0; // Index of loop through new children.
155 - var curChildrenDOMIndex = 0; // See (Comment 1)
156 - for (var name in nextChildren) {
157 - if (!nextChildren.hasOwnProperty(name)) {continue;}
158 - var curChild = this._renderedChildren[name];
159 - var nextChild = nextChildren[name];
160 - if (shouldManageExisting(curChild, nextChild)) {
161 - if (markupBuffer) {
162 - this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
163 - markupBuffer = null;
215 + /**
216 + * Improve performance by isolating this hot code path from the try/catch
217 + * block in `updateChildren`.
218 + *
219 + * @param {?object} nextChildren As returned by `flattenChildren`.
220 + * @param {ReactReconcileTransaction} transaction
221 + * @final
222 + * @protected
223 + */
224 + _updateChildren: function(nextChildren, transaction) {
225 + var prevChildren = this._renderedChildren;
226 + if (!nextChildren && !prevChildren) {
227 + return;
228 + }
229 + var name;
230 + // `nextIndex` will increment for each child in `nextChildren`, but
231 + // `lastIndex` will be the last index visited in `prevChildren`.
232 + var lastIndex = 0;
233 + var nextIndex = 0;
234 + for (name in nextChildren) {
235 + if (!nextChildren.hasOwnProperty(name)) {
236 + continue;
237 }
165 - numPendingInsert = 0;
166 - if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
167 - this.enqueueMove(curChild._domIndex, loopDomIndex);
238 + var prevChild = prevChildren && prevChildren[name];
239 + var nextChild = nextChildren[name];
240 + if (shouldUpdateChild(prevChild, nextChild)) {
241 + this.moveChild(prevChild, nextIndex, lastIndex);
242 + lastIndex = Math.max(prevChild._mountIndex, lastIndex);
243 + prevChild.receiveProps(nextChild.props, transaction);
244 + prevChild._mountIndex = nextIndex;
245 + } else {
246 + if (prevChild) {
247 + this._unmountChildByName(prevChild, name);
248 + lastIndex = Math.max(prevChild._mountIndex, lastIndex);
249 + }
250 + if (nextChild) {
251 + this._mountChildByNameAtIndex(
252 + nextChild, name, nextIndex, transaction
253 + );
254 + }
255 }
169 - curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
170 - curChild.receiveProps(nextChild.props, transaction);
171 - curChild._domIndex = loopDomIndex;
172 - } else {
173 - if (curChild) { // !shouldUpdate && curChild => delete
174 - this.enqueueUnmountChildByName(name, curChild);
175 - curChildrenDOMIndex =
176 - Math.max(curChild._domIndex, curChildrenDOMIndex);
256 + if (nextChild) {
257 + nextIndex++;
258 }
178 - if (nextChild) { // !shouldUpdate && nextChild => insert
179 - this._renderedChildren[name] = nextChild;
180 - var nextMarkup =
181 - nextChild.mountComponent(rootDomIdDot + name, transaction);
182 - markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
183 - numPendingInsert++;
184 - nextChild._domIndex = loopDomIndex;
259 + }
260 + // Remove children that are no longer present.
261 + for (name in prevChildren) {
262 + if (prevChildren.hasOwnProperty(name) &&
263 + prevChildren[name] &&
264 + !(nextChildren && nextChildren[name])) {
265 + this._unmountChildByName(prevChildren[name], name);
266 }
267 }
187 - loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
188 - }
189 - if (markupBuffer) {
190 - this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
191 - }
192 - for (var childName in this._renderedChildren) { // from other direction
193 - if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
194 - var child = this._renderedChildren[childName];
195 - if (child && !nextChildren[childName]) {
196 - this.enqueueUnmountChildByName(childName, child);
268 + },
269 +
270 + /**
271 + * Unmounts all rendered children. This should be used to clean up children
272 + * when this component is unmounted.
273 + *
274 + * @internal
275 + */
276 + unmountChildren: function() {
277 + var renderedChildren = this._renderedChildren;
278 + for (var name in renderedChildren) {
279 + var renderedChild = renderedChildren[name];
280 + if (renderedChild && renderedChild.unmountComponent) {
281 + renderedChild.unmountComponent();
282 + }
283 + }
284 + this._renderedChildren = null;
285 + },
286 +
287 + /**
288 + * Moves a child component to the supplied index.
289 + *
290 + * @param {ReactComponent} child Component to move.
291 + * @param {number} toIndex Destination index of the element.
292 + * @param {number} lastIndex Last index visited of the siblings of `child`.
293 + * @protected
294 + */
295 + moveChild: function(child, toIndex, lastIndex) {
296 + // If the index of `child` is less than `lastIndex`, then it needs to
297 + // be moved. Otherwise, we do not need to move it because a child will be
298 + // inserted or moved before `child`.
299 + if (child._mountIndex < lastIndex) {
300 + enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
301 + }
302 + },
303 +
304 + /**
305 + * Creates a child component.
306 + *
307 + * @param {ReactComponent} child Component to create.
308 + * @protected
309 + */
310 + createChild: function(child) {
311 + enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex);
312 + },
313 +
314 + /**
315 + * Removes a child component.
316 + *
317 + * @param {ReactComponent} child Child to remove.
318 + * @protected
319 + */
320 + removeChild: function(child) {
321 + enqueueRemove(this._rootNodeID, child._mountIndex);
322 + },
323 +
324 + /**
325 + * Mounts a child with the supplied name.
326 + *
327 + * NOTE: This is part of `updateChildren` and is here for readability.
328 + *
329 + * @param {ReactComponent} child Component to mount.
330 + * @param {string} name Name of the child.
331 + * @param {number} index Index at which to insert the child.
332 + * @param {ReactReconcileTransaction} transaction
333 + * @private
334 + */
335 + _mountChildByNameAtIndex: function(child, name, index, transaction) {
336 + // Inlined for performance, see `ReactID.createReactID`.
337 + var rootID = this._rootNodeID + '.' + name;
338 + var mountImage = child.mountComponent(rootID, transaction);
339 + child._mountImage = mountImage;
340 + child._mountIndex = index;
341 + this.createChild(child);
342 + this._renderedChildren = this._renderedChildren || {};
343 + this._renderedChildren[name] = child;
344 + },
345 +
346 + /**
347 + * Unmounts a rendered child by name.
348 + *
349 + * NOTE: This is part of `updateChildren` and is here for readability.
350 + *
351 + * @param {ReactComponent} child Component to unmount.
352 + * @param {string} name Name of the child in `this._renderedChildren`.
353 + * @private
354 + */
355 + _unmountChildByName: function(child, name) {
356 + if (ReactComponent.isValidComponent(child)) {
357 + this.removeChild(child);
358 + child._mountImage = null;
359 + child._mountIndex = null;
360 + child.unmountComponent();
361 + delete this._renderedChildren[name];
362 }
363 }
199 - this.processChildDOMOperationsQueue();
364 +
365 }
201 -};
366
203 -var ReactMultiChild = {
204 - Mixin: ReactMultiChildMixin
367 };
368
369 module.exports = ReactMultiChild;
src/core/ReactMultiChildUpdateTypes.js new
+35
@@ -0,0 +1,35 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactMultiChildUpdateTypes
17 + */
18 +
19 +var keyMirror = require('keyMirror');
20 +
21 +/**
22 + * When a component's children are updated, a series of update configuration
23 + * objects are created in order to batch and serialize the required changes.
24 + *
25 + * Enumerates all the possible types of update configurations.
26 + *
27 + * @internal
28 + */
29 +var ReactMultiChildUpdateTypes = keyMirror({
30 + INSERT_MARKUP: null,
31 + MOVE_EXISTING: null,
32 + REMOVE_NODE: null
33 +});
34 +
35 +module.exports = ReactMultiChildUpdateTypes;
src/core/ReactNativeComponent.js
+5 -4
@@ -160,10 +160,11 @@ ReactNativeComponent.Mixin = {
160 if (contentToUse != null) {
161 return escapeTextForBrowser(contentToUse);
162 } else if (childrenToUse != null) {
163 - return this.mountMultiChild(
163 + var mountImages = this.mountChildren(
164 flattenChildren(childrenToUse),
165 transaction
166 );
167 + return mountImages.join('');
168 }
169 }
170 return '';
@@ -320,7 +321,7 @@ ReactNativeComponent.Mixin = {
321 if (contentToUse != null) {
322 var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
323 if (childrenRemoved) {
323 - this.updateMultiChild(null, transaction);
324 + this.updateChildren(null, transaction);
325 }
326 if (lastUsedContent !== contentToUse) {
327 ReactComponent.DOMIDOperations.updateTextContentByID(
@@ -336,7 +337,7 @@ ReactNativeComponent.Mixin = {
337 ''
338 );
339 }
339 - this.updateMultiChild(flattenChildren(nextProps.children), transaction);
340 + this.updateChildren(flattenChildren(nextProps.children), transaction);
341 }
342 },
343
@@ -349,7 +350,7 @@ ReactNativeComponent.Mixin = {
350 unmountComponent: function() {
351 ReactEventEmitter.deleteAllListeners(this._rootNodeID);
352 ReactComponent.Mixin.unmountComponent.call(this);
352 - this.unmountMultiChild();
353 + this.unmountChildren();
354 }
355
356 };
src/core/__tests__/ReactMultiChild-test.js new
+63
@@ -0,0 +1,63 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +describe('ReactMultiChild', function() {
21 + var React;
22 + var setInnerHTML;
23 +
24 + beforeEach(function() {
25 + require('mock-modules').dumpCache();
26 + React = require('React');
27 +
28 + var innerHTMLDescriptor = Object.getOwnPropertyDescriptor(
29 + Element.prototype,
30 + 'innerHTML'
31 + );
32 + Object.defineProperty(Element.prototype, 'innerHTML', {
33 + set: setInnerHTML = jasmine.createSpy().andCallFake(
34 + innerHTMLDescriptor.set
35 + )
36 + });
37 + });
38 +
39 + it('should only set `innerHTML` once on update', function() {
40 + var container = document.createElement('div');
41 +
42 + React.renderComponent(
43 + <div>
44 + <p><span /></p>
45 + <p><span /></p>
46 + <p><span /></p>
47 + </div>,
48 + container
49 + );
50 + expect(setInnerHTML).toHaveBeenCalled();
51 + var callCountOnMount = setInnerHTML.callCount;
52 +
53 + React.renderComponent(
54 + <div>
55 + <p><span /><span /></p>
56 + <p><span /><span /></p>
57 + <p><span /><span /></p>
58 + </div>,
59 + container
60 + );
61 + expect(setInnerHTML.callCount).toBe(callCountOnMount + 1);
62 + });
63 +});
src/core/__tests__/ReactMultiChildReconcile-test.js
+1 -1
@@ -94,7 +94,7 @@ var FriendsStatusDisplay = React.createClass({
94 var child = statusDisplays[name];
95 var isPresent = !!child;
96 if (isPresent) {
97 - orderOfUsernames[child._domIndex] = getOriginalKey(name);
97 + orderOfUsernames[child._mountIndex] = getOriginalKey(name);
98 }
99 }
100 var res = {};
src/dom/DOMChildrenOperations.js
+82 -105
@@ -14,131 +14,108 @@
14 * limitations under the License.
15 *
16 * @providesModule DOMChildrenOperations
17 + * @typechecks static-only
18 */
19
19 -// Empty blocks improve readability so disable that warning
20 -// jshint -W035
21 -
20 "use strict";
21
22 var Danger = require('Danger');
25 -
26 -var insertNodeAt = require('insertNodeAt');
27 -var keyOf = require('keyOf');
28 -var throwIf = require('throwIf');
29 -
30 -var NON_INCREASING_OPERATIONS;
31 -if (__DEV__) {
32 - NON_INCREASING_OPERATIONS =
33 - 'DOM child management operations must be provided in order ' +
34 - 'of increasing destination index. This is likely an issue with ' +
35 - 'the core framework.';
36 -}
37 -
38 -var MOVE_NODE_AT_ORIG_INDEX = keyOf({moveFrom: null});
39 -var INSERT_MARKUP = keyOf({insertMarkup: null});
40 -var REMOVE_AT = keyOf({removeAt: null});
23 +var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
24
25 /**
43 - * In order to carry out movement of DOM nodes without knowing their IDs, we
44 - * have to first store knowledge about nodes' original indices before beginning
45 - * to carry out the sequence of operations. Once we begin the sequence, the DOM
46 - * indices in future instructions are no longer valid.
26 + * Inserts `childNode` as a child of `parentNode` at the `index`.
27 *
48 - * @param {Element} parent Parent DOM node.
49 - * @param {Object} childOperations Description of child operations.
50 - * @return {Array?} Sparse array containing elements by their current index in
51 - * the DOM.
28 + * @param {DOMElement} parentNode Parent node in which to insert.
29 + * @param {DOMElement} childNode Child node to insert.
30 + * @param {number} index Index at which to insert the child.
31 + * @internal
32 */
53 -var _getNodesByOriginalIndex = function(parent, childOperations) {
54 - var nodesByOriginalIndex; // Sparse array.
55 - var childOperation;
56 - var origIndex;
57 - for (var i = 0; i < childOperations.length; i++) {
58 - childOperation = childOperations[i];
59 - if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
60 - nodesByOriginalIndex = nodesByOriginalIndex || [];
61 - origIndex = childOperation.moveFrom;
62 - nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
63 - } else if (REMOVE_AT in childOperation) {
64 - nodesByOriginalIndex = nodesByOriginalIndex || [];
65 - origIndex = childOperation.removeAt;
66 - nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
67 - }
33 +function insertChildAt(parentNode, childNode, index) {
34 + var childNodes = parentNode.childNodes;
35 + if (childNodes[index] === childNode) {
36 + return;
37 }
69 - return nodesByOriginalIndex;
70 -};
38 + // If `childNode` is already a child of `parentNode`, remove it so that
39 + // computing `childNodes[index]` takes into account the removal.
40 + if (childNode.parentNode === parentNode) {
41 + parentNode.removeChild(childNode);
42 + }
43 + if (index >= childNodes.length) {
44 + parentNode.appendChild(childNode);
45 + } else {
46 + parentNode.insertBefore(childNode, childNodes[index]);
47 + }
48 +}
49
50 /**
73 - * Removes DOM elements from their parent, or moved.
74 - * @param {Element} parent Parent DOM node.
75 - * @param {Array} nodesByOriginalIndex Child nodes by their original index
76 - * (potentially sparse.)
51 + * Operations for updating with DOM children.
52 */
78 -var _removeChildrenByOriginalIndex = function(parent, nodesByOriginalIndex) {
79 - for (var j = 0; j < nodesByOriginalIndex.length; j++) {
80 - var nodeToRemove = nodesByOriginalIndex[j];
81 - if (nodeToRemove) { // We used a sparse array.
82 - parent.removeChild(nodesByOriginalIndex[j]);
53 +var DOMChildrenOperations = {
54 +
55 + dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
56 +
57 + /**
58 + * Updates a component's children by processing a series of updates. The
59 + * update configurations are each expected to have a `parentNode` property.
60 + *
61 + * @param {array<object>} updates List of update configurations.
62 + * @param {array<string>} markupList List of markup strings.
63 + * @internal
64 + */
65 + processUpdates: function(updates, markupList) {
66 + var update;
67 + // Mapping from parent IDs to initial child orderings.
68 + var initialChildren = null;
69 + // List of children that will be moved or removed.
70 + var updatedChildren = null;
71 +
72 + for (var i = 0; update = updates[i]; i++) {
73 + if (update.type !== ReactMultiChildUpdateTypes.INSERT_MARKUP) {
74 + var updatedIndex = update.fromIndex;
75 + var updatedChild = update.parentNode.childNodes[updatedIndex];
76 + var parentID = update.parentID;
77 +
78 + initialChildren = initialChildren || {};
79 + initialChildren[parentID] = initialChildren[parentID] || [];
80 + initialChildren[parentID][updatedIndex] = updatedChild;
81 +
82 + updatedChildren = updatedChildren || [];
83 + updatedChildren.push(updatedChild);
84 + }
85 }
84 - }
85 -};
86
87 -/**
88 - * Once all nodes that will be removed or moved - are removed from the parent
89 - * node, we can begin the process of placing nodes into their final locations.
90 - * We must perform all operations in the order of the final DOM index -
91 - * otherwise, we couldn't count on the fact that an insertion at index X, will
92 - * remain at index X. This will iterate through the child operations, adding
93 - * content where needed, skip over removals (they've already been removed) and
94 - * insert "moved" Elements that were previously removed. The "moved" elements
95 - * are only temporarily removed from the parent, so that index calculations can
96 - * be manageable and perform well in the cases that matter.
97 - */
98 -var _placeNodesAtDestination =
99 - function(parent, childOperations, nodesByOriginalIndex) {
100 - var origNode;
101 - var finalIndex;
102 - var lastFinalIndex = -1;
103 - var childOperation;
104 - for (var k = 0; k < childOperations.length; k++) {
105 - childOperation = childOperations[k];
106 - if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
107 - origNode = nodesByOriginalIndex[childOperation.moveFrom];
108 - finalIndex = childOperation.finalIndex;
109 - insertNodeAt(parent, origNode, finalIndex);
110 - if (__DEV__) {
111 - throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
112 - lastFinalIndex = finalIndex;
113 - }
114 - } else if (REMOVE_AT in childOperation) {
115 - } else if (INSERT_MARKUP in childOperation) {
116 - finalIndex = childOperation.finalIndex;
117 - var markup = childOperation.insertMarkup;
118 - Danger.dangerouslyInsertMarkupAt(parent, markup, finalIndex);
119 - if (__DEV__) {
120 - throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
121 - lastFinalIndex = finalIndex;
122 - }
87 + var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
88 +
89 + // Remove updated children first so that `toIndex` is consistent.
90 + if (updatedChildren) {
91 + for (var j = 0; j < updatedChildren.length; j++) {
92 + updatedChildren[j].parentNode.removeChild(updatedChildren[j]);
93 }
94 }
125 - };
95
127 -var manageChildren = function(parent, childOperations) {
128 - var nodesByOriginalIndex = _getNodesByOriginalIndex(parent, childOperations);
129 - if (nodesByOriginalIndex) {
130 - _removeChildrenByOriginalIndex(parent, nodesByOriginalIndex);
96 + for (var k = 0; update = updates[k]; k++) {
97 + switch (update.type) {
98 + case ReactMultiChildUpdateTypes.INSERT_MARKUP:
99 + insertChildAt(
100 + update.parentNode,
101 + renderedMarkup[update.markupIndex],
102 + update.toIndex
103 + );
104 + break;
105 + case ReactMultiChildUpdateTypes.MOVE_EXISTING:
106 + insertChildAt(
107 + update.parentNode,
108 + initialChildren[update.parentID][update.fromIndex],
109 + update.toIndex
110 + );
111 + break;
112 + case ReactMultiChildUpdateTypes.REMOVE_NODE:
113 + // Already removed by the for-loop above.
114 + break;
115 + }
116 + }
117 }
132 - _placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
133 -};
118
135 -/**
136 - * Also reexport all of the dangerous functions. It helps to have all dangerous
137 - * functions located in a single module `Danger`.
138 - */
139 -var DOMChildrenOperations = {
140 - dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
141 - manageChildren: manageChildren
119 };
120
121 module.exports = DOMChildrenOperations;
src/dom/Danger.js
+93 -124
@@ -14,6 +14,7 @@
14 * limitations under the License.
15 *
16 * @providesModule Danger
17 + * @typechecks static-only
18 */
19
20 /*jslint evil: true, sub: true */
@@ -22,33 +23,7 @@
23
24 var ExecutionEnvironment = require('ExecutionEnvironment');
25
25 -var throwIf = require('throwIf');
26 -
27 -var DOM_UNSUPPORTED;
28 -var NO_MARKUP_PARENT;
29 -var NO_MULTI_MARKUP;
30 -if (__DEV__) {
31 - DOM_UNSUPPORTED =
32 - 'You may not insert markup into the document while you are in a worker ' +
33 - 'thread. It\'s not you, it\'s me. This is likely the fault of the ' +
34 - 'framework. Please report this immediately.';
35 - NO_MARKUP_PARENT =
36 - 'You have attempted to inject markup without a suitable parent. This is ' +
37 - 'likely the fault of the framework - please report immediately.';
38 - NO_MULTI_MARKUP =
39 - 'The framework has attempted to either insert zero or multiple markup ' +
40 - 'roots into a single location when it should not. This is a serious ' +
41 - 'error - a fault of the framework - please report immediately.';
42 -}
43 -
44 -var validateMarkupParams;
45 -if (__DEV__) {
46 - validateMarkupParams = function(parentNode, markup) {
47 - throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
48 - throwIf(!parentNode || !parentNode.tagName, NO_MARKUP_PARENT);
49 - throwIf(!markup, NO_MULTI_MARKUP);
50 - };
51 -}
26 +var invariant = require('invariant');
27
28 /**
29 * Dummy container used to render all markup.
@@ -103,22 +78,35 @@ if (dummyNode) {
78 }
79
80 /**
106 - * Renders markup into nodes. The returned HTMLCollection is live and should be
107 - * used immediately (or at least before the next invocation to `renderMarkup`).
81 + * Extracts the `nodeName` from a string of markup. This does not require a
82 + * regular expression match because we make assumptions about React-generated
83 + * markup (i.e. there are no spaces surrounding the opening tag and there is at
84 + * least an ID attribute).
85 *
86 * NOTE: Extracting the `nodeName` does not require a regular expression match
87 * because we make assumptions about React-generated markup (i.e. there are no
88 * spaces surrounding the opening tag and there is at least one attribute).
89 + *
90 + * @param {string} markup String of markup.
91 + * @return {string} Node name of the supplied markup.
92 * @see http://jsperf.com/extract-nodename
93 + */
94 +function getNodeName(markup) {
95 + return markup.substring(1, markup.indexOf(' '));
96 +}
97 +
98 +/**
99 + * Renders markup into nodes. The returned HTMLCollection is live and should be
100 + * used immediately (or at least before the next invocation to `renderMarkup`).
101 *
114 - * @param {string} markup
102 + * @param {string} markup Markup for one or more nodes with the same `nodeName`.
103 + * @param {?string} nodeName Optional, the lowercase node name of the markup.
104 * @return {*} An HTMLCollection.
105 */
117 -function renderMarkup(markup) {
106 +function renderMarkup(markup, nodeName) {
107 + nodeName = nodeName || getNodeName(markup);
108 var node = dummyNode;
119 - var nodeName = markup.substring(1, markup.indexOf(' '));
120 -
121 - var wrap = markupWrap[nodeName.toLowerCase()] || defaultWrap;
109 + var wrap = markupWrap[nodeName] || defaultWrap;
110 if (wrap) {
111 node.innerHTML = wrap[1] + markup + wrap[2];
112
@@ -132,99 +120,80 @@ function renderMarkup(markup) {
120 return node.childNodes;
121 }
122
135 -/**
136 - * Inserts node after 'after'. If 'after' is null, inserts it after nothing,
137 - * which is inserting it at the beginning.
138 - *
139 - * @param {Element} elem Parent element.
140 - * @param {Element} insert Element to insert.
141 - * @param {Element} after Element to insert after.
142 - * @return {Element} Element that was inserted.
143 - */
144 -function insertNodeAfterNode(elem, insert, after) {
145 - if (__DEV__) {
146 - throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
147 - }
148 - if (after) {
149 - if (after.nextSibling) {
150 - return elem.insertBefore(insert, after.nextSibling);
151 - } else {
152 - return elem.appendChild(insert);
153 - }
154 - } else {
155 - return elem.insertBefore(insert, elem.firstChild);
156 - }
157 -}
158 -
159 -/**
160 - * Slow: Should only be used when it is known there are a few (or one) element
161 - * in the node list.
162 - * @param {Element} parentRootDomNode Parent element.
163 - * @param {HTMLCollection} htmlCollection HTMLCollection to insert.
164 - * @param {Element} after Element to insert the node list after.
165 - */
166 -function inefficientlyInsertHTMLCollectionAfter(
167 - parentRootDomNode,
168 - htmlCollection,
169 - after) {
170 -
171 - if (__DEV__) {
172 - throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
173 - }
174 - var ret;
175 - var originalLength = htmlCollection.length;
176 - // Access htmlCollection[0] because htmlCollection shrinks as we remove items.
177 - // `insertNodeAfterNode` will remove items from the htmlCollection.
178 - for (var i = 0; i < originalLength; i++) {
179 - ret =
180 - insertNodeAfterNode(parentRootDomNode, htmlCollection[0], ret || after);
181 - }
182 -}
183 -
184 -/**
185 - * Super-dangerously inserts markup into existing DOM structure. Seriously, you
186 - * don't want to use this module unless you are building a framework. This
187 - * requires that the markup that you are inserting represents the root of a
188 - * tree. We do not support the case where there `markup` represents several
189 - * roots.
190 - *
191 - * @param {Element} parentNode Parent DOM element.
192 - * @param {string} markup Markup to dangerously insert.
193 - * @param {number} index Position to insert markup at.
194 - */
195 -function dangerouslyInsertMarkupAt(parentNode, markup, index) {
196 - if (__DEV__) {
197 - validateMarkupParams(parentNode, markup);
198 - }
199 - var htmlCollection = renderMarkup(markup);
200 - var afterNode = index ? parentNode.childNodes[index - 1] : null;
201 - inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
202 -}
123 +var Danger = {
124
204 -/**
205 - * Replaces a node with a string of markup at its current position within its
206 - * parent. `childNode` must be in the document (or at least within a parent
207 - * node). The string of markup must represent a tree of markup with a single
208 - * root.
209 - *
210 - * @param {Element} childNode Child node to replace.
211 - * @param {string} markup Markup to dangerously replace child with.
212 - */
213 -function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
214 - var parentNode = childNode.parentNode;
215 - if (__DEV__) {
216 - validateMarkupParams(parentNode, markup);
217 - }
218 - var htmlCollection = renderMarkup(markup);
219 - if (__DEV__) {
220 - throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
125 + /**
126 + * Renders markup into an array of nodes. The markup is expected to render
127 + * into a list of root nodes. Also, the length of `parentNodes` and `markup`
128 + * should be the same.
129 + *
130 + * @param {array<string>} markupList List of markup strings to render.
131 + * @return {array<DOMElement>} List of rendered nodes.
132 + * @internal
133 + */
134 + dangerouslyRenderMarkup: function(markupList) {
135 + invariant(
136 + ExecutionEnvironment.canUseDOM,
137 + 'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' +
138 + 'thread. This is likely a bug in the framework. Please report ' +
139 + 'immediately.'
140 + );
141 + var nodeName;
142 + var markupByNodeName = {};
143 + // Group markup by `nodeName` if a wrap is necessary, else by '*'.
144 + for (var i = 0; i < markupList.length; i++) {
145 + invariant(
146 + markupList[i],
147 + 'dangerouslyRenderMarkup(...): Missing markup.'
148 + );
149 + nodeName = getNodeName(markupList[i]);
150 + nodeName = markupWrap[nodeName] ? nodeName : '*';
151 + markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
152 + markupByNodeName[nodeName][i] = markupList[i];
153 + }
154 + var renderedMarkup = [];
155 + for (nodeName in markupByNodeName) {
156 + if (!markupByNodeName.hasOwnProperty(nodeName)) {
157 + continue;
158 + }
159 + var markupListByNodeName = markupByNodeName[nodeName];
160 + var markup = markupListByNodeName.join('');
161 + // Render each group of markup.
162 + var childNode = renderMarkup(markup, nodeName)[0];
163 + // Restore the initial ordering.
164 + for (var j = 0; j < markupListByNodeName.length; j++) {
165 + // `markupListByNodeName` may be a sparse array.
166 + if (markupListByNodeName[j]) {
167 + invariant(childNode, 'dangerouslyRenderMarkup(...): Missing node.');
168 + renderedMarkup[j] = childNode;
169 + childNode = childNode.nextSibling;
170 + }
171 + }
172 + invariant(!childNode, 'dangerouslyRenderMarkupO(...): Unexpected nodes.');
173 + }
174 + return renderedMarkup;
175 + },
176 +
177 + /**
178 + * Replaces a node with a string of markup at its current position within its
179 + * parent. The markup must render into a single root node.
180 + *
181 + * @param {DOMElement} oldChild Child node to replace.
182 + * @param {string} markup Markup to render in place of the child node.
183 + * @internal
184 + */
185 + dangerouslyReplaceNodeWithMarkup: function(oldChild, markup) {
186 + invariant(
187 + ExecutionEnvironment.canUseDOM,
188 + 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a ' +
189 + 'worker thread. This is likely a bug in the framework. Please report ' +
190 + 'immediately.'
191 + );
192 + invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.');
193 + var newChild = renderMarkup(markup)[0];
194 + oldChild.parentNode.replaceChild(newChild, oldChild);
195 }
222 - parentNode.replaceChild(htmlCollection[0], childNode);
223 -}
196
225 -var Danger = {
226 - dangerouslyInsertMarkupAt: dangerouslyInsertMarkupAt,
227 - dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup
197 };
198
199 module.exports = Danger;
src/dom/__tests__/Danger-test.js
+52 -14
@@ -23,7 +23,7 @@ var React = require('React');
23
24 describe('Danger', function() {
25
26 - describe('dangerouslyInsertMarkupAt', function() {
26 + describe('dangerouslyRenderMarkup', function() {
27 var Danger;
28 var transaction;
29
@@ -37,33 +37,71 @@ describe('Danger', function() {
37
38 it('should render markup', function() {
39 var markup = (<div />).mountComponent('.rX', transaction);
40 - var parent = document.createElement('div');
40 + var output = Danger.dangerouslyRenderMarkup([markup])[0];
41
42 - Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
43 -
44 - expect(parent.innerHTML).toBe('<div data-reactid=".rX"></div>');
42 + expect(output.nodeName).toBe('DIV');
43 });
44
45 it('should render markup with props', function() {
46 var markup = (<div className="foo" />).mountComponent('.rX', transaction);
49 - var parent = document.createElement('div');
50 -
51 - Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
47 + var output = Danger.dangerouslyRenderMarkup([markup])[0];
48
53 - expect(parent.innerHTML).toBe(
54 - '<div class="foo" data-reactid=".rX"></div>'
55 - );
49 + expect(output.nodeName).toBe('DIV');
50 + expect(output.className).toBe('foo');
51 });
52
53 it('should render wrapped markup', function() {
54 var markup = (<th />).mountComponent('.rX', transaction);
60 - var parent = document.createElement('div');
55 + var output = Danger.dangerouslyRenderMarkup([markup])[0];
56 +
57 + expect(output.nodeName).toBe('TH');
58 + });
59 +
60 + it('should render lists of markup with similar `nodeName`', function() {
61 + var renderedMarkup = Danger.dangerouslyRenderMarkup(
62 + ['<p>1</p>', '<p>2</p>', '<p>3</p>']
63 + );
64 +
65 + expect(renderedMarkup.length).toBe(3);
66 +
67 + expect(renderedMarkup[0].nodeName).toBe('P');
68 + expect(renderedMarkup[1].nodeName).toBe('P');
69 + expect(renderedMarkup[2].nodeName).toBe('P');
70
62 - Danger.dangerouslyInsertMarkupAt(parent, markup, 0);
71 + expect(renderedMarkup[0].innerHTML).toBe('1');
72 + expect(renderedMarkup[1].innerHTML).toBe('2');
73 + expect(renderedMarkup[2].innerHTML).toBe('3');
74 + });
75 +
76 + it('should render lists of markup with different `nodeName`', function() {
77 + var renderedMarkup = Danger.dangerouslyRenderMarkup(
78 + ['<p>1</p>', '<tr>2</tr>', '<p>3</p>']
79 + );
80 +
81 + expect(renderedMarkup.length).toBe(3);
82 +
83 + expect(renderedMarkup[0].nodeName).toBe('P');
84 + expect(renderedMarkup[1].nodeName).toBe('TR');
85 + expect(renderedMarkup[2].nodeName).toBe('P');
86
64 - expect(parent.innerHTML).toBe('<th data-reactid=".rX"></th>');
87 + expect(renderedMarkup[0].innerHTML).toBe('1');
88 + expect(renderedMarkup[1].innerHTML).toBe('2');
89 + expect(renderedMarkup[2].innerHTML).toBe('3');
90 });
91
92 + it('should throw when rendering invalid markup', function() {
93 + expect(function() {
94 + Danger.dangerouslyRenderMarkup(['']);
95 + }).toThrow(
96 + 'Invariant Violation: dangerouslyRenderMarkup(...): Missing markup.'
97 + );
98 +
99 + expect(function() {
100 + Danger.dangerouslyRenderMarkup(['<p></p><p></p>']);
101 + }).toThrow(
102 + 'Invariant Violation: dangerouslyRenderMarkupO(...): Unexpected nodes.'
103 + );
104 + });
105 });
106
107 });
src/dom/insertNodeAt.js deleted
-46
@@ -1,46 +0,0 @@
1 -/**
2 - * Copyright 2013 Facebook, Inc.
3 - *
4 - * Licensed under the Apache License, Version 2.0 (the "License");
5 - * you may not use this file except in compliance with the License.
6 - * You may obtain a copy of the License at
7 - *
8 - * http://www.apache.org/licenses/LICENSE-2.0
9 - *
10 - * Unless required by applicable law or agreed to in writing, software
11 - * distributed under the License is distributed on an "AS IS" BASIS,
12 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 - * See the License for the specific language governing permissions and
14 - * limitations under the License.
15 - *
16 - * @providesModule insertNodeAt
17 - */
18 -
19 -"use strict";
20 -
21 -/**
22 - * Inserts `node` at a particular child index. Other nodes move to make room.
23 - * @param {!Element} root The parent root node to insert into.
24 - * @param {!node} node The node to insert.
25 - * @param {!number} atIndex The index in `root` that `node` should exist at.
26 - */
27 -function insertNodeAt(root, node, atIndex) {
28 - var childNodes = root.childNodes;
29 - // Remove from parent so that if node is already child of root,
30 - // `childNodes[atIndex]` already takes into account the removal.
31 - var curAtIndex = root.childNodes[atIndex];
32 - if (curAtIndex === node) {
33 - return node;
34 - }
35 - if (node.parentNode) {
36 - node.parentNode.removeChild(node);
37 - }
38 - if (atIndex >= childNodes.length) {
39 - root.appendChild(node);
40 - } else {
41 - root.insertBefore(node, childNodes[atIndex]);
42 - }
43 - return node;
44 -}
45 -
46 -module.exports = insertNodeAt;