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 ReactDOMComponent
10
* @typechecks static-only
11
*/
12
13
"use strict";
14
15
var CSSPropertyOperations = require('CSSPropertyOperations');
16
var DOMProperty = require('DOMProperty');
17
var DOMPropertyOperations = require('DOMPropertyOperations');
18
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
19
var ReactComponent = require('ReactComponent');
20
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
21
var ReactMount = require('ReactMount');
22
var ReactMultiChild = require('ReactMultiChild');
23
var ReactPerf = require('ReactPerf');
24
25
var assign = require('Object.assign');
26
var escapeTextForBrowser = require('escapeTextForBrowser');
27
var invariant = require('invariant');
28
var isEventSupported = require('isEventSupported');
29
var keyOf = require('keyOf');
30
var monitorCodeUse = require('monitorCodeUse');
31
32
var deleteListener = ReactBrowserEventEmitter.deleteListener;
33
var listenTo = ReactBrowserEventEmitter.listenTo;
34
var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
35
36
// For quickly matching children type, to test if can be treated as content.
37
var CONTENT_TYPES = {'string': true, 'number': true};
38
39
var STYLE = keyOf({style: null});
40
41
var ELEMENT_NODE_TYPE = 1;
42
43
/**
44
* @param {?object} props
45
*/
46
function assertValidProps(props) {
47
if (!props) {
48
return;
49
}
50
// Note the use of `==` which checks for null or undefined.
51
invariant(
52
props.children == null || props.dangerouslySetInnerHTML == null,
53
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
54
);
55
if (__DEV__) {
56
if (props.contentEditable && props.children != null) {
57
console.warn(
58
'A component is `contentEditable` and contains `children` managed by ' +
59
'React. It is now your responsibility to guarantee that none of those '+
60
'nodes are unexpectedly modified or duplicated. This is probably not ' +
61
'intentional.'
62
);
63
}
64
}
65
invariant(
66
props.style == null || typeof props.style === 'object',
67
'The `style` prop expects a mapping from style properties to values, ' +
68
'not a string.'
69
);
70
}
71
72
function putListener(id, registrationName, listener, transaction) {
73
if (__DEV__) {
74
// IE8 has no API for event capturing and the `onScroll` event doesn't
75
// bubble.
76
if (registrationName === 'onScroll' &&
77
!isEventSupported('scroll', true)) {
78
monitorCodeUse('react_no_scroll_event');
79
console.warn('This browser doesn\'t support the `onScroll` event');
80
}
81
}
82
var container = ReactMount.findReactContainerForID(id);
83
if (container) {
84
var doc = container.nodeType === ELEMENT_NODE_TYPE ?
85
container.ownerDocument :
86
container;
87
listenTo(registrationName, doc);
88
}
89
transaction.getPutListenerQueue().enqueuePutListener(
90
id,
91
registrationName,
92
listener
93
);
94
}
95
96
// For HTML, certain tags should omit their close tag. We keep a whitelist for
97
// those special cased tags.
98
99
var omittedCloseTags = {
100
'area': true,
101
'base': true,
102
'br': true,
103
'col': true,
104
'embed': true,
105
'hr': true,
106
'img': true,
107
'input': true,
108
'keygen': true,
109
'link': true,
110
'meta': true,
111
'param': true,
112
'source': true,
113
'track': true,
114
'wbr': true
115
// NOTE: menuitem's close tag should be omitted, but that causes problems.
116
};
117
118
// We accept any tag to be rendered but since this gets injected into abitrary
119
// HTML, we want to make sure that it's a safe tag.
120
// http://www.w3.org/TR/REC-xml/#NT-Name
121
122
var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
123
var validatedTagCache = {};
124
var hasOwnProperty = {}.hasOwnProperty;
125
126
function validateDangerousTag(tag) {
127
if (!hasOwnProperty.call(validatedTagCache, tag)) {
128
invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag);
129
validatedTagCache[tag] = true;
130
}
131
}
132
133
/**
134
* Creates a new React class that is idempotent and capable of containing other
135
* React components. It accepts event listeners and DOM properties that are
136
* valid according to `DOMProperty`.
137
*
138
* - Event listeners: `onClick`, `onMouseDown`, etc.
139
* - DOM properties: `className`, `name`, `title`, etc.
140
*
141
* The `style` property functions differently from the DOM API. It accepts an
142
* object mapping of style properties to values.
143
*
144
* @constructor ReactDOMComponent
145
* @extends ReactComponent
146
* @extends ReactMultiChild
147
*/
148
function ReactDOMComponent(tag) {
149
validateDangerousTag(tag);
150
this._tag = tag;
151
this.tagName = tag.toUpperCase();
152
}
153
154
ReactDOMComponent.displayName = 'ReactDOMComponent';
155
156
ReactDOMComponent.Mixin = {
157
158
/**
159
* Generates root tag markup then recurses. This method has side effects and
160
* is not idempotent.
161
*
162
* @internal
163
* @param {string} rootID The root DOM ID for this node.
164
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
165
* @param {number} mountDepth number of components in the owner hierarchy
166
* @return {string} The computed markup.
167
*/
168
mountComponent: ReactPerf.measure(
169
'ReactDOMComponent',
170
'mountComponent',
171
function(rootID, transaction, mountDepth) {
172
ReactComponent.Mixin.mountComponent.call(
173
this,
174
rootID,
175
transaction,
176
mountDepth
177
);
178
assertValidProps(this.props);
179
var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>';
180
return (
181
this._createOpenTagMarkupAndPutListeners(transaction) +
182
this._createContentMarkup(transaction) +
183
closeTag
184
);
185
}
186
),
187
188
/**
189
* Creates markup for the open tag and all attributes.
190
*
191
* This method has side effects because events get registered.
192
*
193
* Iterating over object properties is faster than iterating over arrays.
194
* @see http://jsperf.com/obj-vs-arr-iteration
195
*
196
* @private
197
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
198
* @return {string} Markup of opening tag.
199
*/
200
_createOpenTagMarkupAndPutListeners: function(transaction) {
201
var props = this.props;
202
var ret = '<' + this._tag;
203
204
for (var propKey in props) {
205
if (!props.hasOwnProperty(propKey)) {
206
continue;
207
}
208
var propValue = props[propKey];
209
if (propValue == null) {
210
continue;
211
}
212
if (registrationNameModules.hasOwnProperty(propKey)) {
213
putListener(this._rootNodeID, propKey, propValue, transaction);
214
} else {
215
if (propKey === STYLE) {
216
if (propValue) {
217
propValue = props.style = assign({}, props.style);
218
}
219
propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
220
}
221
var markup =
222
DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
223
if (markup) {
224
ret += ' ' + markup;
225
}
226
}
227
}
228
229
// For static pages, no need to put React ID and checksum. Saves lots of
230
// bytes.
231
if (transaction.renderToStaticMarkup) {
232
return ret + '>';
233
}
234
235
var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
236
return ret + ' ' + markupForID + '>';
237
},
238
239
/**
240
* Creates markup for the content between the tags.
241
*
242
* @private
243
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
244
* @return {string} Content markup.
245
*/
246
_createContentMarkup: function(transaction) {
247
// Intentional use of != to avoid catching zero/false.
248
var innerHTML = this.props.dangerouslySetInnerHTML;
249
if (innerHTML != null) {
250
if (innerHTML.__html != null) {
251
return innerHTML.__html;
252
}
253
} else {
254
var contentToUse =
255
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
256
var childrenToUse = contentToUse != null ? null : this.props.children;
257
if (contentToUse != null) {
258
return escapeTextForBrowser(contentToUse);
259
} else if (childrenToUse != null) {
260
var mountImages = this.mountChildren(
261
childrenToUse,
262
transaction
263
);
264
return mountImages.join('');
265
}
266
}
267
return '';
268
},
269
270
receiveComponent: function(nextElement, transaction) {
271
if (nextElement === this._currentElement &&
272
nextElement._owner != null) {
273
// Since elements are immutable after the owner is rendered,
274
// we can do a cheap identity compare here to determine if this is a
275
// superfluous reconcile. It's possible for state to be mutable but such
276
// change should trigger an update of the owner which would recreate
277
// the element. We explicitly check for the existence of an owner since
278
// it's possible for a element created outside a composite to be
279
// deeply mutated and reused.
280
return;
281
}
282
283
ReactComponent.Mixin.receiveComponent.call(
284
this,
285
nextElement,
286
transaction
287
);
288
},
289
290
/**
291
* Updates a native DOM component after it has already been allocated and
292
* attached to the DOM. Reconciles the root DOM node, then recurses.
293
*
294
* @param {ReactReconcileTransaction} transaction
295
* @param {ReactElement} prevElement
296
* @internal
297
* @overridable
298
*/
299
updateComponent: ReactPerf.measure(
300
'ReactDOMComponent',
301
'updateComponent',
302
function(transaction, prevElement) {
303
assertValidProps(this._currentElement.props);
304
ReactComponent.Mixin.updateComponent.call(
305
this,
306
transaction,
307
prevElement
308
);
309
this._updateDOMProperties(prevElement.props, transaction);
310
this._updateDOMChildren(prevElement.props, transaction);
311
}
312
),
313
314
/**
315
* Reconciles the properties by detecting differences in property values and
316
* updating the DOM as necessary. This function is probably the single most
317
* critical path for performance optimization.
318
*
319
* TODO: Benchmark whether checking for changed values in memory actually
320
* improves performance (especially statically positioned elements).
321
* TODO: Benchmark the effects of putting this at the top since 99% of props
322
* do not change for a given reconciliation.
323
* TODO: Benchmark areas that can be improved with caching.
324
*
325
* @private
326
* @param {object} lastProps
327
* @param {ReactReconcileTransaction} transaction
328
*/
329
_updateDOMProperties: function(lastProps, transaction) {
330
var nextProps = this.props;
331
var propKey;
332
var styleName;
333
var styleUpdates;
334
for (propKey in lastProps) {
335
if (nextProps.hasOwnProperty(propKey) ||
336
!lastProps.hasOwnProperty(propKey)) {
337
continue;
338
}
339
if (propKey === STYLE) {
340
var lastStyle = lastProps[propKey];
341
for (styleName in lastStyle) {
342
if (lastStyle.hasOwnProperty(styleName)) {
343
styleUpdates = styleUpdates || {};
344
styleUpdates[styleName] = '';
345
}
346
}
347
} else if (registrationNameModules.hasOwnProperty(propKey)) {
348
deleteListener(this._rootNodeID, propKey);
349
} else if (
350
DOMProperty.isStandardName[propKey] ||
351
DOMProperty.isCustomAttribute(propKey)) {
352
ReactComponent.BackendIDOperations.deletePropertyByID(
353
this._rootNodeID,
354
propKey
355
);
356
}
357
}
358
for (propKey in nextProps) {
359
var nextProp = nextProps[propKey];
360
var lastProp = lastProps[propKey];
361
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
362
continue;
363
}
364
if (propKey === STYLE) {
365
if (nextProp) {
366
nextProp = nextProps.style = assign({}, nextProp);
367
}
368
if (lastProp) {
369
// Unset styles on `lastProp` but not on `nextProp`.
370
for (styleName in lastProp) {
371
if (lastProp.hasOwnProperty(styleName) &&
372
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
373
styleUpdates = styleUpdates || {};
374
styleUpdates[styleName] = '';
375
}
376
}
377
// Update styles that changed since `lastProp`.
378
for (styleName in nextProp) {
379
if (nextProp.hasOwnProperty(styleName) &&
380
lastProp[styleName] !== nextProp[styleName]) {
381
styleUpdates = styleUpdates || {};
382
styleUpdates[styleName] = nextProp[styleName];
383
}
384
}
385
} else {
386
// Relies on `updateStylesByID` not mutating `styleUpdates`.
387
styleUpdates = nextProp;
388
}
389
} else if (registrationNameModules.hasOwnProperty(propKey)) {
390
putListener(this._rootNodeID, propKey, nextProp, transaction);
391
} else if (
392
DOMProperty.isStandardName[propKey] ||
393
DOMProperty.isCustomAttribute(propKey)) {
394
ReactComponent.BackendIDOperations.updatePropertyByID(
395
this._rootNodeID,
396
propKey,
397
nextProp
398
);
399
}
400
}
401
if (styleUpdates) {
402
ReactComponent.BackendIDOperations.updateStylesByID(
403
this._rootNodeID,
404
styleUpdates
405
);
406
}
407
},
408
409
/**
410
* Reconciles the children with the various properties that affect the
411
* children content.
412
*
413
* @param {object} lastProps
414
* @param {ReactReconcileTransaction} transaction
415
*/
416
_updateDOMChildren: function(lastProps, transaction) {
417
var nextProps = this.props;
418
419
var lastContent =
420
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
421
var nextContent =
422
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
423
424
var lastHtml =
425
lastProps.dangerouslySetInnerHTML &&
426
lastProps.dangerouslySetInnerHTML.__html;
427
var nextHtml =
428
nextProps.dangerouslySetInnerHTML &&
429
nextProps.dangerouslySetInnerHTML.__html;
430
431
// Note the use of `!=` which checks for null or undefined.
432
var lastChildren = lastContent != null ? null : lastProps.children;
433
var nextChildren = nextContent != null ? null : nextProps.children;
434
435
// If we're switching from children to content/html or vice versa, remove
436
// the old content
437
var lastHasContentOrHtml = lastContent != null || lastHtml != null;
438
var nextHasContentOrHtml = nextContent != null || nextHtml != null;
439
if (lastChildren != null && nextChildren == null) {
440
this.updateChildren(null, transaction);
441
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
442
this.updateTextContent('');
443
}
444
445
if (nextContent != null) {
446
if (lastContent !== nextContent) {
447
this.updateTextContent('' + nextContent);
448
}
449
} else if (nextHtml != null) {
450
if (lastHtml !== nextHtml) {
451
ReactComponent.BackendIDOperations.updateInnerHTMLByID(
452
this._rootNodeID,
453
nextHtml
454
);
455
}
456
} else if (nextChildren != null) {
457
this.updateChildren(nextChildren, transaction);
458
}
459
},
460
461
/**
462
* Destroys all event registrations for this instance. Does not remove from
463
* the DOM. That must be done by the parent.
464
*
465
* @internal
466
*/
467
unmountComponent: function() {
468
this.unmountChildren();
469
ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
470
ReactComponent.Mixin.unmountComponent.call(this);
471
}
472
473
};
474
475
assign(
476
ReactDOMComponent.prototype,
477
ReactComponent.Mixin,
478
ReactDOMComponent.Mixin,
479
ReactMultiChild.Mixin,
480
ReactBrowserComponentMixin
481
);
482
483
module.exports = ReactDOMComponent;
484
485