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