Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81155 views
1
/**
2
* Copyright 2013-2014, Facebook, Inc.
3
* All rights reserved.
4
*
5
* This source code is licensed under the BSD-style license found in the
6
* LICENSE file in the root directory of this source tree. An additional grant
7
* of patent rights can be found in the PATENTS file in the same directory.
8
*
9
* @providesModule ReactMount
10
*/
11
12
"use strict";
13
14
var DOMProperty = require('DOMProperty');
15
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
16
var ReactCurrentOwner = require('ReactCurrentOwner');
17
var ReactElement = require('ReactElement');
18
var ReactLegacyElement = require('ReactLegacyElement');
19
var ReactInstanceHandles = require('ReactInstanceHandles');
20
var ReactPerf = require('ReactPerf');
21
22
var containsNode = require('containsNode');
23
var deprecated = require('deprecated');
24
var getReactRootElementInContainer = require('getReactRootElementInContainer');
25
var instantiateReactComponent = require('instantiateReactComponent');
26
var invariant = require('invariant');
27
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
28
var warning = require('warning');
29
30
var createElement = ReactLegacyElement.wrapCreateElement(
31
ReactElement.createElement
32
);
33
34
var SEPARATOR = ReactInstanceHandles.SEPARATOR;
35
36
var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
37
var nodeCache = {};
38
39
var ELEMENT_NODE_TYPE = 1;
40
var DOC_NODE_TYPE = 9;
41
42
/** Mapping from reactRootID to React component instance. */
43
var instancesByReactRootID = {};
44
45
/** Mapping from reactRootID to `container` nodes. */
46
var containersByReactRootID = {};
47
48
if (__DEV__) {
49
/** __DEV__-only mapping from reactRootID to root elements. */
50
var rootElementsByReactRootID = {};
51
}
52
53
// Used to store breadth-first search state in findComponentRoot.
54
var findComponentRootReusableArray = [];
55
56
/**
57
* @param {DOMElement} container DOM element that may contain a React component.
58
* @return {?string} A "reactRoot" ID, if a React component is rendered.
59
*/
60
function getReactRootID(container) {
61
var rootElement = getReactRootElementInContainer(container);
62
return rootElement && ReactMount.getID(rootElement);
63
}
64
65
/**
66
* Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form
67
* element can return its control whose name or ID equals ATTR_NAME. All
68
* DOM nodes support `getAttributeNode` but this can also get called on
69
* other objects so just return '' if we're given something other than a
70
* DOM node (such as window).
71
*
72
* @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
73
* @return {string} ID of the supplied `domNode`.
74
*/
75
function getID(node) {
76
var id = internalGetID(node);
77
if (id) {
78
if (nodeCache.hasOwnProperty(id)) {
79
var cached = nodeCache[id];
80
if (cached !== node) {
81
invariant(
82
!isValid(cached, id),
83
'ReactMount: Two valid but unequal nodes with the same `%s`: %s',
84
ATTR_NAME, id
85
);
86
87
nodeCache[id] = node;
88
}
89
} else {
90
nodeCache[id] = node;
91
}
92
}
93
94
return id;
95
}
96
97
function internalGetID(node) {
98
// If node is something like a window, document, or text node, none of
99
// which support attributes or a .getAttribute method, gracefully return
100
// the empty string, as if the attribute were missing.
101
return node && node.getAttribute && node.getAttribute(ATTR_NAME) || '';
102
}
103
104
/**
105
* Sets the React-specific ID of the given node.
106
*
107
* @param {DOMElement} node The DOM node whose ID will be set.
108
* @param {string} id The value of the ID attribute.
109
*/
110
function setID(node, id) {
111
var oldID = internalGetID(node);
112
if (oldID !== id) {
113
delete nodeCache[oldID];
114
}
115
node.setAttribute(ATTR_NAME, id);
116
nodeCache[id] = node;
117
}
118
119
/**
120
* Finds the node with the supplied React-generated DOM ID.
121
*
122
* @param {string} id A React-generated DOM ID.
123
* @return {DOMElement} DOM node with the suppled `id`.
124
* @internal
125
*/
126
function getNode(id) {
127
if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
128
nodeCache[id] = ReactMount.findReactNodeByID(id);
129
}
130
return nodeCache[id];
131
}
132
133
/**
134
* A node is "valid" if it is contained by a currently mounted container.
135
*
136
* This means that the node does not have to be contained by a document in
137
* order to be considered valid.
138
*
139
* @param {?DOMElement} node The candidate DOM node.
140
* @param {string} id The expected ID of the node.
141
* @return {boolean} Whether the node is contained by a mounted container.
142
*/
143
function isValid(node, id) {
144
if (node) {
145
invariant(
146
internalGetID(node) === id,
147
'ReactMount: Unexpected modification of `%s`',
148
ATTR_NAME
149
);
150
151
var container = ReactMount.findReactContainerForID(id);
152
if (container && containsNode(container, node)) {
153
return true;
154
}
155
}
156
157
return false;
158
}
159
160
/**
161
* Causes the cache to forget about one React-specific ID.
162
*
163
* @param {string} id The ID to forget.
164
*/
165
function purgeID(id) {
166
delete nodeCache[id];
167
}
168
169
var deepestNodeSoFar = null;
170
function findDeepestCachedAncestorImpl(ancestorID) {
171
var ancestor = nodeCache[ancestorID];
172
if (ancestor && isValid(ancestor, ancestorID)) {
173
deepestNodeSoFar = ancestor;
174
} else {
175
// This node isn't populated in the cache, so presumably none of its
176
// descendants are. Break out of the loop.
177
return false;
178
}
179
}
180
181
/**
182
* Return the deepest cached node whose ID is a prefix of `targetID`.
183
*/
184
function findDeepestCachedAncestor(targetID) {
185
deepestNodeSoFar = null;
186
ReactInstanceHandles.traverseAncestors(
187
targetID,
188
findDeepestCachedAncestorImpl
189
);
190
191
var foundNode = deepestNodeSoFar;
192
deepestNodeSoFar = null;
193
return foundNode;
194
}
195
196
/**
197
* Mounting is the process of initializing a React component by creatings its
198
* representative DOM elements and inserting them into a supplied `container`.
199
* Any prior content inside `container` is destroyed in the process.
200
*
201
* ReactMount.render(
202
* component,
203
* document.getElementById('container')
204
* );
205
*
206
* <div id="container"> <-- Supplied `container`.
207
* <div data-reactid=".3"> <-- Rendered reactRoot of React
208
* // ... component.
209
* </div>
210
* </div>
211
*
212
* Inside of `container`, the first element rendered is the "reactRoot".
213
*/
214
var ReactMount = {
215
/** Exposed for debugging purposes **/
216
_instancesByReactRootID: instancesByReactRootID,
217
218
/**
219
* This is a hook provided to support rendering React components while
220
* ensuring that the apparent scroll position of its `container` does not
221
* change.
222
*
223
* @param {DOMElement} container The `container` being rendered into.
224
* @param {function} renderCallback This must be called once to do the render.
225
*/
226
scrollMonitor: function(container, renderCallback) {
227
renderCallback();
228
},
229
230
/**
231
* Take a component that's already mounted into the DOM and replace its props
232
* @param {ReactComponent} prevComponent component instance already in the DOM
233
* @param {ReactComponent} nextComponent component instance to render
234
* @param {DOMElement} container container to render into
235
* @param {?function} callback function triggered on completion
236
*/
237
_updateRootComponent: function(
238
prevComponent,
239
nextComponent,
240
container,
241
callback) {
242
var nextProps = nextComponent.props;
243
ReactMount.scrollMonitor(container, function() {
244
prevComponent.replaceProps(nextProps, callback);
245
});
246
247
if (__DEV__) {
248
// Record the root element in case it later gets transplanted.
249
rootElementsByReactRootID[getReactRootID(container)] =
250
getReactRootElementInContainer(container);
251
}
252
253
return prevComponent;
254
},
255
256
/**
257
* Register a component into the instance map and starts scroll value
258
* monitoring
259
* @param {ReactComponent} nextComponent component instance to render
260
* @param {DOMElement} container container to render into
261
* @return {string} reactRoot ID prefix
262
*/
263
_registerComponent: function(nextComponent, container) {
264
invariant(
265
container && (
266
container.nodeType === ELEMENT_NODE_TYPE ||
267
container.nodeType === DOC_NODE_TYPE
268
),
269
'_registerComponent(...): Target container is not a DOM element.'
270
);
271
272
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
273
274
var reactRootID = ReactMount.registerContainer(container);
275
instancesByReactRootID[reactRootID] = nextComponent;
276
return reactRootID;
277
},
278
279
/**
280
* Render a new component into the DOM.
281
* @param {ReactComponent} nextComponent component instance to render
282
* @param {DOMElement} container container to render into
283
* @param {boolean} shouldReuseMarkup if we should skip the markup insertion
284
* @return {ReactComponent} nextComponent
285
*/
286
_renderNewRootComponent: ReactPerf.measure(
287
'ReactMount',
288
'_renderNewRootComponent',
289
function(
290
nextComponent,
291
container,
292
shouldReuseMarkup) {
293
// Various parts of our code (such as ReactCompositeComponent's
294
// _renderValidatedComponent) assume that calls to render aren't nested;
295
// verify that that's the case.
296
warning(
297
ReactCurrentOwner.current == null,
298
'_renderNewRootComponent(): Render methods should be a pure function ' +
299
'of props and state; triggering nested component updates from ' +
300
'render is not allowed. If necessary, trigger nested updates in ' +
301
'componentDidUpdate.'
302
);
303
304
var componentInstance = instantiateReactComponent(nextComponent, null);
305
var reactRootID = ReactMount._registerComponent(
306
componentInstance,
307
container
308
);
309
componentInstance.mountComponentIntoNode(
310
reactRootID,
311
container,
312
shouldReuseMarkup
313
);
314
315
if (__DEV__) {
316
// Record the root element in case it later gets transplanted.
317
rootElementsByReactRootID[reactRootID] =
318
getReactRootElementInContainer(container);
319
}
320
321
return componentInstance;
322
}
323
),
324
325
/**
326
* Renders a React component into the DOM in the supplied `container`.
327
*
328
* If the React component was previously rendered into `container`, this will
329
* perform an update on it and only mutate the DOM as necessary to reflect the
330
* latest React component.
331
*
332
* @param {ReactElement} nextElement Component element to render.
333
* @param {DOMElement} container DOM element to render into.
334
* @param {?function} callback function triggered on completion
335
* @return {ReactComponent} Component instance rendered in `container`.
336
*/
337
render: function(nextElement, container, callback) {
338
invariant(
339
ReactElement.isValidElement(nextElement),
340
'renderComponent(): Invalid component element.%s',
341
(
342
typeof nextElement === 'string' ?
343
' Instead of passing an element string, make sure to instantiate ' +
344
'it by passing it to React.createElement.' :
345
ReactLegacyElement.isValidFactory(nextElement) ?
346
' Instead of passing a component class, make sure to instantiate ' +
347
'it by passing it to React.createElement.' :
348
// Check if it quacks like a element
349
typeof nextElement.props !== "undefined" ?
350
' This may be caused by unintentionally loading two independent ' +
351
'copies of React.' :
352
''
353
)
354
);
355
356
var prevComponent = instancesByReactRootID[getReactRootID(container)];
357
358
if (prevComponent) {
359
var prevElement = prevComponent._currentElement;
360
if (shouldUpdateReactComponent(prevElement, nextElement)) {
361
return ReactMount._updateRootComponent(
362
prevComponent,
363
nextElement,
364
container,
365
callback
366
);
367
} else {
368
ReactMount.unmountComponentAtNode(container);
369
}
370
}
371
372
var reactRootElement = getReactRootElementInContainer(container);
373
var containerHasReactMarkup =
374
reactRootElement && ReactMount.isRenderedByReact(reactRootElement);
375
376
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent;
377
378
var component = ReactMount._renderNewRootComponent(
379
nextElement,
380
container,
381
shouldReuseMarkup
382
);
383
callback && callback.call(component);
384
return component;
385
},
386
387
/**
388
* Constructs a component instance of `constructor` with `initialProps` and
389
* renders it into the supplied `container`.
390
*
391
* @param {function} constructor React component constructor.
392
* @param {?object} props Initial props of the component instance.
393
* @param {DOMElement} container DOM element to render into.
394
* @return {ReactComponent} Component instance rendered in `container`.
395
*/
396
constructAndRenderComponent: function(constructor, props, container) {
397
var element = createElement(constructor, props);
398
return ReactMount.render(element, container);
399
},
400
401
/**
402
* Constructs a component instance of `constructor` with `initialProps` and
403
* renders it into a container node identified by supplied `id`.
404
*
405
* @param {function} componentConstructor React component constructor
406
* @param {?object} props Initial props of the component instance.
407
* @param {string} id ID of the DOM element to render into.
408
* @return {ReactComponent} Component instance rendered in the container node.
409
*/
410
constructAndRenderComponentByID: function(constructor, props, id) {
411
var domNode = document.getElementById(id);
412
invariant(
413
domNode,
414
'Tried to get element with id of "%s" but it is not present on the page.',
415
id
416
);
417
return ReactMount.constructAndRenderComponent(constructor, props, domNode);
418
},
419
420
/**
421
* Registers a container node into which React components will be rendered.
422
* This also creates the "reactRoot" ID that will be assigned to the element
423
* rendered within.
424
*
425
* @param {DOMElement} container DOM element to register as a container.
426
* @return {string} The "reactRoot" ID of elements rendered within.
427
*/
428
registerContainer: function(container) {
429
var reactRootID = getReactRootID(container);
430
if (reactRootID) {
431
// If one exists, make sure it is a valid "reactRoot" ID.
432
reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
433
}
434
if (!reactRootID) {
435
// No valid "reactRoot" ID found, create one.
436
reactRootID = ReactInstanceHandles.createReactRootID();
437
}
438
containersByReactRootID[reactRootID] = container;
439
return reactRootID;
440
},
441
442
/**
443
* Unmounts and destroys the React component rendered in the `container`.
444
*
445
* @param {DOMElement} container DOM element containing a React component.
446
* @return {boolean} True if a component was found in and unmounted from
447
* `container`
448
*/
449
unmountComponentAtNode: function(container) {
450
// Various parts of our code (such as ReactCompositeComponent's
451
// _renderValidatedComponent) assume that calls to render aren't nested;
452
// verify that that's the case. (Strictly speaking, unmounting won't cause a
453
// render but we still don't expect to be in a render call here.)
454
warning(
455
ReactCurrentOwner.current == null,
456
'unmountComponentAtNode(): Render methods should be a pure function of ' +
457
'props and state; triggering nested component updates from render is ' +
458
'not allowed. If necessary, trigger nested updates in ' +
459
'componentDidUpdate.'
460
);
461
462
var reactRootID = getReactRootID(container);
463
var component = instancesByReactRootID[reactRootID];
464
if (!component) {
465
return false;
466
}
467
ReactMount.unmountComponentFromNode(component, container);
468
delete instancesByReactRootID[reactRootID];
469
delete containersByReactRootID[reactRootID];
470
if (__DEV__) {
471
delete rootElementsByReactRootID[reactRootID];
472
}
473
return true;
474
},
475
476
/**
477
* Unmounts a component and removes it from the DOM.
478
*
479
* @param {ReactComponent} instance React component instance.
480
* @param {DOMElement} container DOM element to unmount from.
481
* @final
482
* @internal
483
* @see {ReactMount.unmountComponentAtNode}
484
*/
485
unmountComponentFromNode: function(instance, container) {
486
instance.unmountComponent();
487
488
if (container.nodeType === DOC_NODE_TYPE) {
489
container = container.documentElement;
490
}
491
492
// http://jsperf.com/emptying-a-node
493
while (container.lastChild) {
494
container.removeChild(container.lastChild);
495
}
496
},
497
498
/**
499
* Finds the container DOM element that contains React component to which the
500
* supplied DOM `id` belongs.
501
*
502
* @param {string} id The ID of an element rendered by a React component.
503
* @return {?DOMElement} DOM element that contains the `id`.
504
*/
505
findReactContainerForID: function(id) {
506
var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
507
var container = containersByReactRootID[reactRootID];
508
509
if (__DEV__) {
510
var rootElement = rootElementsByReactRootID[reactRootID];
511
if (rootElement && rootElement.parentNode !== container) {
512
invariant(
513
// Call internalGetID here because getID calls isValid which calls
514
// findReactContainerForID (this function).
515
internalGetID(rootElement) === reactRootID,
516
'ReactMount: Root element ID differed from reactRootID.'
517
);
518
519
var containerChild = container.firstChild;
520
if (containerChild &&
521
reactRootID === internalGetID(containerChild)) {
522
// If the container has a new child with the same ID as the old
523
// root element, then rootElementsByReactRootID[reactRootID] is
524
// just stale and needs to be updated. The case that deserves a
525
// warning is when the container is empty.
526
rootElementsByReactRootID[reactRootID] = containerChild;
527
} else {
528
console.warn(
529
'ReactMount: Root element has been removed from its original ' +
530
'container. New container:', rootElement.parentNode
531
);
532
}
533
}
534
}
535
536
return container;
537
},
538
539
/**
540
* Finds an element rendered by React with the supplied ID.
541
*
542
* @param {string} id ID of a DOM node in the React component.
543
* @return {DOMElement} Root DOM node of the React component.
544
*/
545
findReactNodeByID: function(id) {
546
var reactRoot = ReactMount.findReactContainerForID(id);
547
return ReactMount.findComponentRoot(reactRoot, id);
548
},
549
550
/**
551
* True if the supplied `node` is rendered by React.
552
*
553
* @param {*} node DOM Element to check.
554
* @return {boolean} True if the DOM Element appears to be rendered by React.
555
* @internal
556
*/
557
isRenderedByReact: function(node) {
558
if (node.nodeType !== 1) {
559
// Not a DOMElement, therefore not a React component
560
return false;
561
}
562
var id = ReactMount.getID(node);
563
return id ? id.charAt(0) === SEPARATOR : false;
564
},
565
566
/**
567
* Traverses up the ancestors of the supplied node to find a node that is a
568
* DOM representation of a React component.
569
*
570
* @param {*} node
571
* @return {?DOMEventTarget}
572
* @internal
573
*/
574
getFirstReactDOM: function(node) {
575
var current = node;
576
while (current && current.parentNode !== current) {
577
if (ReactMount.isRenderedByReact(current)) {
578
return current;
579
}
580
current = current.parentNode;
581
}
582
return null;
583
},
584
585
/**
586
* Finds a node with the supplied `targetID` inside of the supplied
587
* `ancestorNode`. Exploits the ID naming scheme to perform the search
588
* quickly.
589
*
590
* @param {DOMEventTarget} ancestorNode Search from this root.
591
* @pararm {string} targetID ID of the DOM representation of the component.
592
* @return {DOMEventTarget} DOM node with the supplied `targetID`.
593
* @internal
594
*/
595
findComponentRoot: function(ancestorNode, targetID) {
596
var firstChildren = findComponentRootReusableArray;
597
var childIndex = 0;
598
599
var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode;
600
601
firstChildren[0] = deepestAncestor.firstChild;
602
firstChildren.length = 1;
603
604
while (childIndex < firstChildren.length) {
605
var child = firstChildren[childIndex++];
606
var targetChild;
607
608
while (child) {
609
var childID = ReactMount.getID(child);
610
if (childID) {
611
// Even if we find the node we're looking for, we finish looping
612
// through its siblings to ensure they're cached so that we don't have
613
// to revisit this node again. Otherwise, we make n^2 calls to getID
614
// when visiting the many children of a single node in order.
615
616
if (targetID === childID) {
617
targetChild = child;
618
} else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) {
619
// If we find a child whose ID is an ancestor of the given ID,
620
// then we can be sure that we only want to search the subtree
621
// rooted at this child, so we can throw out the rest of the
622
// search state.
623
firstChildren.length = childIndex = 0;
624
firstChildren.push(child.firstChild);
625
}
626
627
} else {
628
// If this child had no ID, then there's a chance that it was
629
// injected automatically by the browser, as when a `<table>`
630
// element sprouts an extra `<tbody>` child as a side effect of
631
// `.innerHTML` parsing. Optimistically continue down this
632
// branch, but not before examining the other siblings.
633
firstChildren.push(child.firstChild);
634
}
635
636
child = child.nextSibling;
637
}
638
639
if (targetChild) {
640
// Emptying firstChildren/findComponentRootReusableArray is
641
// not necessary for correctness, but it helps the GC reclaim
642
// any nodes that were left at the end of the search.
643
firstChildren.length = 0;
644
645
return targetChild;
646
}
647
}
648
649
firstChildren.length = 0;
650
651
invariant(
652
false,
653
'findComponentRoot(..., %s): Unable to find element. This probably ' +
654
'means the DOM was unexpectedly mutated (e.g., by the browser), ' +
655
'usually due to forgetting a <tbody> when using tables, nesting tags ' +
656
'like <form>, <p>, or <a>, or using non-SVG elements in an <svg> ' +
657
'parent. ' +
658
'Try inspecting the child nodes of the element with React ID `%s`.',
659
targetID,
660
ReactMount.getID(ancestorNode)
661
);
662
},
663
664
665
/**
666
* React ID utilities.
667
*/
668
669
getReactRootID: getReactRootID,
670
671
getID: getID,
672
673
setID: setID,
674
675
getNode: getNode,
676
677
purgeID: purgeID
678
};
679
680
// Deprecations (remove for 0.13)
681
ReactMount.renderComponent = deprecated(
682
'ReactMount',
683
'renderComponent',
684
'render',
685
this,
686
ReactMount.render
687
);
688
689
module.exports = ReactMount;
690
691