UNPKG

38.9 kBJavaScriptView Raw
1/**
2 * Copyright 2013-present, 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 */
10
11/* global hasOwnProperty:true */
12
13'use strict';
14
15var _prodInvariant = require('./reactProdInvariant'),
16 _assign = require('object-assign');
17
18var AutoFocusUtils = require('./AutoFocusUtils');
19var CSSPropertyOperations = require('./CSSPropertyOperations');
20var DOMLazyTree = require('./DOMLazyTree');
21var DOMNamespaces = require('./DOMNamespaces');
22var DOMProperty = require('./DOMProperty');
23var DOMPropertyOperations = require('./DOMPropertyOperations');
24var EventPluginHub = require('./EventPluginHub');
25var EventPluginRegistry = require('./EventPluginRegistry');
26var ReactBrowserEventEmitter = require('./ReactBrowserEventEmitter');
27var ReactDOMComponentFlags = require('./ReactDOMComponentFlags');
28var ReactDOMComponentTree = require('./ReactDOMComponentTree');
29var ReactDOMInput = require('./ReactDOMInput');
30var ReactDOMOption = require('./ReactDOMOption');
31var ReactDOMSelect = require('./ReactDOMSelect');
32var ReactDOMTextarea = require('./ReactDOMTextarea');
33var ReactInstrumentation = require('./ReactInstrumentation');
34var ReactMultiChild = require('./ReactMultiChild');
35var ReactServerRenderingTransaction = require('./ReactServerRenderingTransaction');
36
37var emptyFunction = require('fbjs/lib/emptyFunction');
38var escapeTextContentForBrowser = require('./escapeTextContentForBrowser');
39var invariant = require('fbjs/lib/invariant');
40var isEventSupported = require('./isEventSupported');
41var shallowEqual = require('fbjs/lib/shallowEqual');
42var inputValueTracking = require('./inputValueTracking');
43var validateDOMNesting = require('./validateDOMNesting');
44var warning = require('fbjs/lib/warning');
45
46var Flags = ReactDOMComponentFlags;
47var deleteListener = EventPluginHub.deleteListener;
48var getNode = ReactDOMComponentTree.getNodeFromInstance;
49var listenTo = ReactBrowserEventEmitter.listenTo;
50var registrationNameModules = EventPluginRegistry.registrationNameModules;
51
52// For quickly matching children type, to test if can be treated as content.
53var CONTENT_TYPES = { string: true, number: true };
54
55var STYLE = 'style';
56var HTML = '__html';
57var RESERVED_PROPS = {
58 children: null,
59 dangerouslySetInnerHTML: null,
60 suppressContentEditableWarning: null
61};
62
63// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
64var DOC_FRAGMENT_TYPE = 11;
65
66function getDeclarationErrorAddendum(internalInstance) {
67 if (internalInstance) {
68 var owner = internalInstance._currentElement._owner || null;
69 if (owner) {
70 var name = owner.getName();
71 if (name) {
72 return ' This DOM node was rendered by `' + name + '`.';
73 }
74 }
75 }
76 return '';
77}
78
79function friendlyStringify(obj) {
80 if (typeof obj === 'object') {
81 if (Array.isArray(obj)) {
82 return '[' + obj.map(friendlyStringify).join(', ') + ']';
83 } else {
84 var pairs = [];
85 for (var key in obj) {
86 if (Object.prototype.hasOwnProperty.call(obj, key)) {
87 var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
88 pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
89 }
90 }
91 return '{' + pairs.join(', ') + '}';
92 }
93 } else if (typeof obj === 'string') {
94 return JSON.stringify(obj);
95 } else if (typeof obj === 'function') {
96 return '[function object]';
97 }
98 // Differs from JSON.stringify in that undefined because undefined and that
99 // inf and nan don't become null
100 return String(obj);
101}
102
103var styleMutationWarning = {};
104
105function checkAndWarnForMutatedStyle(style1, style2, component) {
106 if (style1 == null || style2 == null) {
107 return;
108 }
109 if (shallowEqual(style1, style2)) {
110 return;
111 }
112
113 var componentName = component._tag;
114 var owner = component._currentElement._owner;
115 var ownerName;
116 if (owner) {
117 ownerName = owner.getName();
118 }
119
120 var hash = ownerName + '|' + componentName;
121
122 if (styleMutationWarning.hasOwnProperty(hash)) {
123 return;
124 }
125
126 styleMutationWarning[hash] = true;
127
128 process.env.NODE_ENV !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : void 0;
129}
130
131/**
132 * @param {object} component
133 * @param {?object} props
134 */
135function assertValidProps(component, props) {
136 if (!props) {
137 return;
138 }
139 // Note the use of `==` which checks for null or undefined.
140 if (voidElementTags[component._tag]) {
141 !(props.children == null && props.dangerouslySetInnerHTML == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : _prodInvariant('137', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : void 0;
142 }
143 if (props.dangerouslySetInnerHTML != null) {
144 !(props.children == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : _prodInvariant('60') : void 0;
145 !(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? process.env.NODE_ENV !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.') : _prodInvariant('61') : void 0;
146 }
147 if (process.env.NODE_ENV !== 'production') {
148 process.env.NODE_ENV !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : void 0;
149 process.env.NODE_ENV !== 'production' ? warning(props.suppressContentEditableWarning || !props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : void 0;
150 process.env.NODE_ENV !== 'production' ? warning(props.onFocusIn == null && props.onFocusOut == null, 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + 'are not needed/supported by React.') : void 0;
151 }
152 !(props.style == null || typeof props.style === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + \'em\'}} when using JSX.%s', getDeclarationErrorAddendum(component)) : _prodInvariant('62', getDeclarationErrorAddendum(component)) : void 0;
153}
154
155function enqueuePutListener(inst, registrationName, listener, transaction) {
156 if (transaction instanceof ReactServerRenderingTransaction) {
157 return;
158 }
159 if (process.env.NODE_ENV !== 'production') {
160 // IE8 has no API for event capturing and the `onScroll` event doesn't
161 // bubble.
162 process.env.NODE_ENV !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), "This browser doesn't support the `onScroll` event") : void 0;
163 }
164 var containerInfo = inst._hostContainerInfo;
165 var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
166 var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
167 listenTo(registrationName, doc);
168 transaction.getReactMountReady().enqueue(putListener, {
169 inst: inst,
170 registrationName: registrationName,
171 listener: listener
172 });
173}
174
175function putListener() {
176 var listenerToPut = this;
177 EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
178}
179
180function inputPostMount() {
181 var inst = this;
182 ReactDOMInput.postMountWrapper(inst);
183}
184
185function textareaPostMount() {
186 var inst = this;
187 ReactDOMTextarea.postMountWrapper(inst);
188}
189
190function optionPostMount() {
191 var inst = this;
192 ReactDOMOption.postMountWrapper(inst);
193}
194
195var setAndValidateContentChildDev = emptyFunction;
196if (process.env.NODE_ENV !== 'production') {
197 setAndValidateContentChildDev = function (content) {
198 var hasExistingContent = this._contentDebugID != null;
199 var debugID = this._debugID;
200 // This ID represents the inlined child that has no backing instance:
201 var contentDebugID = -debugID;
202
203 if (content == null) {
204 if (hasExistingContent) {
205 ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
206 }
207 this._contentDebugID = null;
208 return;
209 }
210
211 validateDOMNesting(null, String(content), this, this._ancestorInfo);
212 this._contentDebugID = contentDebugID;
213 if (hasExistingContent) {
214 ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
215 ReactInstrumentation.debugTool.onUpdateComponent(contentDebugID);
216 } else {
217 ReactInstrumentation.debugTool.onBeforeMountComponent(contentDebugID, content, debugID);
218 ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
219 ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
220 }
221 };
222}
223
224// There are so many media events, it makes sense to just
225// maintain a list rather than create a `trapBubbledEvent` for each
226var mediaEvents = {
227 topAbort: 'abort',
228 topCanPlay: 'canplay',
229 topCanPlayThrough: 'canplaythrough',
230 topDurationChange: 'durationchange',
231 topEmptied: 'emptied',
232 topEncrypted: 'encrypted',
233 topEnded: 'ended',
234 topError: 'error',
235 topLoadedData: 'loadeddata',
236 topLoadedMetadata: 'loadedmetadata',
237 topLoadStart: 'loadstart',
238 topPause: 'pause',
239 topPlay: 'play',
240 topPlaying: 'playing',
241 topProgress: 'progress',
242 topRateChange: 'ratechange',
243 topSeeked: 'seeked',
244 topSeeking: 'seeking',
245 topStalled: 'stalled',
246 topSuspend: 'suspend',
247 topTimeUpdate: 'timeupdate',
248 topVolumeChange: 'volumechange',
249 topWaiting: 'waiting'
250};
251
252function trackInputValue() {
253 inputValueTracking.track(this);
254}
255
256function trapBubbledEventsLocal() {
257 var inst = this;
258 // If a component renders to null or if another component fatals and causes
259 // the state of the tree to be corrupted, `node` here can be null.
260 !inst._rootNodeID ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Must be mounted to trap events') : _prodInvariant('63') : void 0;
261 var node = getNode(inst);
262 !node ? process.env.NODE_ENV !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : _prodInvariant('64') : void 0;
263
264 switch (inst._tag) {
265 case 'iframe':
266 case 'object':
267 inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent('topLoad', 'load', node)];
268 break;
269 case 'video':
270 case 'audio':
271 inst._wrapperState.listeners = [];
272 // Create listener for each media event
273 for (var event in mediaEvents) {
274 if (mediaEvents.hasOwnProperty(event)) {
275 inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(event, mediaEvents[event], node));
276 }
277 }
278 break;
279 case 'source':
280 inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent('topError', 'error', node)];
281 break;
282 case 'img':
283 inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent('topError', 'error', node), ReactBrowserEventEmitter.trapBubbledEvent('topLoad', 'load', node)];
284 break;
285 case 'form':
286 inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent('topReset', 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent('topSubmit', 'submit', node)];
287 break;
288 case 'input':
289 case 'select':
290 case 'textarea':
291 inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent('topInvalid', 'invalid', node)];
292 break;
293 }
294}
295
296function postUpdateSelectWrapper() {
297 ReactDOMSelect.postUpdateWrapper(this);
298}
299
300// For HTML, certain tags should omit their close tag. We keep a whitelist for
301// those special-case tags.
302
303var omittedCloseTags = {
304 area: true,
305 base: true,
306 br: true,
307 col: true,
308 embed: true,
309 hr: true,
310 img: true,
311 input: true,
312 keygen: true,
313 link: true,
314 meta: true,
315 param: true,
316 source: true,
317 track: true,
318 wbr: true
319 // NOTE: menuitem's close tag should be omitted, but that causes problems.
320};
321
322var newlineEatingTags = {
323 listing: true,
324 pre: true,
325 textarea: true
326};
327
328// For HTML, certain tags cannot have children. This has the same purpose as
329// `omittedCloseTags` except that `menuitem` should still have its closing tag.
330
331var voidElementTags = _assign({
332 menuitem: true
333}, omittedCloseTags);
334
335// We accept any tag to be rendered but since this gets injected into arbitrary
336// HTML, we want to make sure that it's a safe tag.
337// http://www.w3.org/TR/REC-xml/#NT-Name
338
339var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
340var validatedTagCache = {};
341var hasOwnProperty = {}.hasOwnProperty;
342
343function validateDangerousTag(tag) {
344 if (!hasOwnProperty.call(validatedTagCache, tag)) {
345 !VALID_TAG_REGEX.test(tag) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : _prodInvariant('65', tag) : void 0;
346 validatedTagCache[tag] = true;
347 }
348}
349
350function isCustomComponent(tagName, props) {
351 return tagName.indexOf('-') >= 0 || props.is != null;
352}
353
354var globalIdCounter = 1;
355
356/**
357 * Creates a new React class that is idempotent and capable of containing other
358 * React components. It accepts event listeners and DOM properties that are
359 * valid according to `DOMProperty`.
360 *
361 * - Event listeners: `onClick`, `onMouseDown`, etc.
362 * - DOM properties: `className`, `name`, `title`, etc.
363 *
364 * The `style` property functions differently from the DOM API. It accepts an
365 * object mapping of style properties to values.
366 *
367 * @constructor ReactDOMComponent
368 * @extends ReactMultiChild
369 */
370function ReactDOMComponent(element) {
371 var tag = element.type;
372 validateDangerousTag(tag);
373 this._currentElement = element;
374 this._tag = tag.toLowerCase();
375 this._namespaceURI = null;
376 this._renderedChildren = null;
377 this._previousStyle = null;
378 this._previousStyleCopy = null;
379 this._hostNode = null;
380 this._hostParent = null;
381 this._rootNodeID = 0;
382 this._domID = 0;
383 this._hostContainerInfo = null;
384 this._wrapperState = null;
385 this._topLevelWrapper = null;
386 this._flags = 0;
387 if (process.env.NODE_ENV !== 'production') {
388 this._ancestorInfo = null;
389 setAndValidateContentChildDev.call(this, null);
390 }
391}
392
393ReactDOMComponent.displayName = 'ReactDOMComponent';
394
395ReactDOMComponent.Mixin = {
396 /**
397 * Generates root tag markup then recurses. This method has side effects and
398 * is not idempotent.
399 *
400 * @internal
401 * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
402 * @param {?ReactDOMComponent} the parent component instance
403 * @param {?object} info about the host container
404 * @param {object} context
405 * @return {string} The computed markup.
406 */
407 mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
408 this._rootNodeID = globalIdCounter++;
409 this._domID = hostContainerInfo._idCounter++;
410 this._hostParent = hostParent;
411 this._hostContainerInfo = hostContainerInfo;
412
413 var props = this._currentElement.props;
414
415 switch (this._tag) {
416 case 'audio':
417 case 'form':
418 case 'iframe':
419 case 'img':
420 case 'link':
421 case 'object':
422 case 'source':
423 case 'video':
424 this._wrapperState = {
425 listeners: null
426 };
427 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
428 break;
429 case 'input':
430 ReactDOMInput.mountWrapper(this, props, hostParent);
431 props = ReactDOMInput.getHostProps(this, props);
432 transaction.getReactMountReady().enqueue(trackInputValue, this);
433 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
434 break;
435 case 'option':
436 ReactDOMOption.mountWrapper(this, props, hostParent);
437 props = ReactDOMOption.getHostProps(this, props);
438 break;
439 case 'select':
440 ReactDOMSelect.mountWrapper(this, props, hostParent);
441 props = ReactDOMSelect.getHostProps(this, props);
442 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
443 break;
444 case 'textarea':
445 ReactDOMTextarea.mountWrapper(this, props, hostParent);
446 props = ReactDOMTextarea.getHostProps(this, props);
447 transaction.getReactMountReady().enqueue(trackInputValue, this);
448 transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
449 break;
450 }
451
452 assertValidProps(this, props);
453
454 // We create tags in the namespace of their parent container, except HTML
455 // tags get no namespace.
456 var namespaceURI;
457 var parentTag;
458 if (hostParent != null) {
459 namespaceURI = hostParent._namespaceURI;
460 parentTag = hostParent._tag;
461 } else if (hostContainerInfo._tag) {
462 namespaceURI = hostContainerInfo._namespaceURI;
463 parentTag = hostContainerInfo._tag;
464 }
465 if (namespaceURI == null || namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
466 namespaceURI = DOMNamespaces.html;
467 }
468 if (namespaceURI === DOMNamespaces.html) {
469 if (this._tag === 'svg') {
470 namespaceURI = DOMNamespaces.svg;
471 } else if (this._tag === 'math') {
472 namespaceURI = DOMNamespaces.mathml;
473 }
474 }
475 this._namespaceURI = namespaceURI;
476
477 if (process.env.NODE_ENV !== 'production') {
478 var parentInfo;
479 if (hostParent != null) {
480 parentInfo = hostParent._ancestorInfo;
481 } else if (hostContainerInfo._tag) {
482 parentInfo = hostContainerInfo._ancestorInfo;
483 }
484 if (parentInfo) {
485 // parentInfo should always be present except for the top-level
486 // component when server rendering
487 validateDOMNesting(this._tag, null, this, parentInfo);
488 }
489 this._ancestorInfo = validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
490 }
491
492 var mountImage;
493 if (transaction.useCreateElement) {
494 var ownerDocument = hostContainerInfo._ownerDocument;
495 var el;
496 if (namespaceURI === DOMNamespaces.html) {
497 if (this._tag === 'script') {
498 // Create the script via .innerHTML so its "parser-inserted" flag is
499 // set to true and it does not execute
500 var div = ownerDocument.createElement('div');
501 var type = this._currentElement.type;
502 div.innerHTML = '<' + type + '></' + type + '>';
503 el = div.removeChild(div.firstChild);
504 } else if (props.is) {
505 el = ownerDocument.createElement(this._currentElement.type, props.is);
506 } else {
507 // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
508 // See discussion in https://github.com/facebook/react/pull/6896
509 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
510 el = ownerDocument.createElement(this._currentElement.type);
511 }
512 } else {
513 el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type);
514 }
515 ReactDOMComponentTree.precacheNode(this, el);
516 this._flags |= Flags.hasCachedChildNodes;
517 if (!this._hostParent) {
518 DOMPropertyOperations.setAttributeForRoot(el);
519 }
520 this._updateDOMProperties(null, props, transaction);
521 var lazyTree = DOMLazyTree(el);
522 this._createInitialChildren(transaction, props, context, lazyTree);
523 mountImage = lazyTree;
524 } else {
525 var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
526 var tagContent = this._createContentMarkup(transaction, props, context);
527 if (!tagContent && omittedCloseTags[this._tag]) {
528 mountImage = tagOpen + '/>';
529 } else {
530 mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
531 }
532 }
533
534 switch (this._tag) {
535 case 'input':
536 transaction.getReactMountReady().enqueue(inputPostMount, this);
537 if (props.autoFocus) {
538 transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
539 }
540 break;
541 case 'textarea':
542 transaction.getReactMountReady().enqueue(textareaPostMount, this);
543 if (props.autoFocus) {
544 transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
545 }
546 break;
547 case 'select':
548 if (props.autoFocus) {
549 transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
550 }
551 break;
552 case 'button':
553 if (props.autoFocus) {
554 transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
555 }
556 break;
557 case 'option':
558 transaction.getReactMountReady().enqueue(optionPostMount, this);
559 break;
560 }
561
562 return mountImage;
563 },
564
565 /**
566 * Creates markup for the open tag and all attributes.
567 *
568 * This method has side effects because events get registered.
569 *
570 * Iterating over object properties is faster than iterating over arrays.
571 * @see http://jsperf.com/obj-vs-arr-iteration
572 *
573 * @private
574 * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
575 * @param {object} props
576 * @return {string} Markup of opening tag.
577 */
578 _createOpenTagMarkupAndPutListeners: function (transaction, props) {
579 var ret = '<' + this._currentElement.type;
580
581 for (var propKey in props) {
582 if (!props.hasOwnProperty(propKey)) {
583 continue;
584 }
585 var propValue = props[propKey];
586 if (propValue == null) {
587 continue;
588 }
589 if (registrationNameModules.hasOwnProperty(propKey)) {
590 if (propValue) {
591 enqueuePutListener(this, propKey, propValue, transaction);
592 }
593 } else {
594 if (propKey === STYLE) {
595 if (propValue) {
596 if (process.env.NODE_ENV !== 'production') {
597 // See `_updateDOMProperties`. style block
598 this._previousStyle = propValue;
599 }
600 propValue = this._previousStyleCopy = _assign({}, props.style);
601 }
602 propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this);
603 }
604 var markup = null;
605 if (this._tag != null && isCustomComponent(this._tag, props)) {
606 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
607 markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
608 }
609 } else {
610 markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
611 }
612 if (markup) {
613 ret += ' ' + markup;
614 }
615 }
616 }
617
618 // For static pages, no need to put React ID and checksum. Saves lots of
619 // bytes.
620 if (transaction.renderToStaticMarkup) {
621 return ret;
622 }
623
624 if (!this._hostParent) {
625 ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
626 }
627 ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
628 return ret;
629 },
630
631 /**
632 * Creates markup for the content between the tags.
633 *
634 * @private
635 * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
636 * @param {object} props
637 * @param {object} context
638 * @return {string} Content markup.
639 */
640 _createContentMarkup: function (transaction, props, context) {
641 var ret = '';
642
643 // Intentional use of != to avoid catching zero/false.
644 var innerHTML = props.dangerouslySetInnerHTML;
645 if (innerHTML != null) {
646 if (innerHTML.__html != null) {
647 ret = innerHTML.__html;
648 }
649 } else {
650 var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
651 var childrenToUse = contentToUse != null ? null : props.children;
652 if (contentToUse != null) {
653 // TODO: Validate that text is allowed as a child of this node
654 ret = escapeTextContentForBrowser(contentToUse);
655 if (process.env.NODE_ENV !== 'production') {
656 setAndValidateContentChildDev.call(this, contentToUse);
657 }
658 } else if (childrenToUse != null) {
659 var mountImages = this.mountChildren(childrenToUse, transaction, context);
660 ret = mountImages.join('');
661 }
662 }
663 if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
664 // text/html ignores the first character in these tags if it's a newline
665 // Prefer to break application/xml over text/html (for now) by adding
666 // a newline specifically to get eaten by the parser. (Alternately for
667 // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
668 // \r is normalized out by HTMLTextAreaElement#value.)
669 // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
670 // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
671 // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
672 // See: Parsing of "textarea" "listing" and "pre" elements
673 // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
674 return '\n' + ret;
675 } else {
676 return ret;
677 }
678 },
679
680 _createInitialChildren: function (transaction, props, context, lazyTree) {
681 // Intentional use of != to avoid catching zero/false.
682 var innerHTML = props.dangerouslySetInnerHTML;
683 if (innerHTML != null) {
684 if (innerHTML.__html != null) {
685 DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
686 }
687 } else {
688 var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
689 var childrenToUse = contentToUse != null ? null : props.children;
690 // TODO: Validate that text is allowed as a child of this node
691 if (contentToUse != null) {
692 // Avoid setting textContent when the text is empty. In IE11 setting
693 // textContent on a text area will cause the placeholder to not
694 // show within the textarea until it has been focused and blurred again.
695 // https://github.com/facebook/react/issues/6731#issuecomment-254874553
696 if (contentToUse !== '') {
697 if (process.env.NODE_ENV !== 'production') {
698 setAndValidateContentChildDev.call(this, contentToUse);
699 }
700 DOMLazyTree.queueText(lazyTree, contentToUse);
701 }
702 } else if (childrenToUse != null) {
703 var mountImages = this.mountChildren(childrenToUse, transaction, context);
704 for (var i = 0; i < mountImages.length; i++) {
705 DOMLazyTree.queueChild(lazyTree, mountImages[i]);
706 }
707 }
708 }
709 },
710
711 /**
712 * Receives a next element and updates the component.
713 *
714 * @internal
715 * @param {ReactElement} nextElement
716 * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
717 * @param {object} context
718 */
719 receiveComponent: function (nextElement, transaction, context) {
720 var prevElement = this._currentElement;
721 this._currentElement = nextElement;
722 this.updateComponent(transaction, prevElement, nextElement, context);
723 },
724
725 /**
726 * Updates a DOM component after it has already been allocated and
727 * attached to the DOM. Reconciles the root DOM node, then recurses.
728 *
729 * @param {ReactReconcileTransaction} transaction
730 * @param {ReactElement} prevElement
731 * @param {ReactElement} nextElement
732 * @internal
733 * @overridable
734 */
735 updateComponent: function (transaction, prevElement, nextElement, context) {
736 var lastProps = prevElement.props;
737 var nextProps = this._currentElement.props;
738
739 switch (this._tag) {
740 case 'input':
741 lastProps = ReactDOMInput.getHostProps(this, lastProps);
742 nextProps = ReactDOMInput.getHostProps(this, nextProps);
743 break;
744 case 'option':
745 lastProps = ReactDOMOption.getHostProps(this, lastProps);
746 nextProps = ReactDOMOption.getHostProps(this, nextProps);
747 break;
748 case 'select':
749 lastProps = ReactDOMSelect.getHostProps(this, lastProps);
750 nextProps = ReactDOMSelect.getHostProps(this, nextProps);
751 break;
752 case 'textarea':
753 lastProps = ReactDOMTextarea.getHostProps(this, lastProps);
754 nextProps = ReactDOMTextarea.getHostProps(this, nextProps);
755 break;
756 }
757
758 assertValidProps(this, nextProps);
759 this._updateDOMProperties(lastProps, nextProps, transaction);
760 this._updateDOMChildren(lastProps, nextProps, transaction, context);
761
762 switch (this._tag) {
763 case 'input':
764 // Update the wrapper around inputs *after* updating props. This has to
765 // happen after `_updateDOMProperties`. Otherwise HTML5 input validations
766 // raise warnings and prevent the new value from being assigned.
767 ReactDOMInput.updateWrapper(this);
768 break;
769 case 'textarea':
770 ReactDOMTextarea.updateWrapper(this);
771 break;
772 case 'select':
773 // <select> value update needs to occur after <option> children
774 // reconciliation
775 transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
776 break;
777 }
778 },
779
780 /**
781 * Reconciles the properties by detecting differences in property values and
782 * updating the DOM as necessary. This function is probably the single most
783 * critical path for performance optimization.
784 *
785 * TODO: Benchmark whether checking for changed values in memory actually
786 * improves performance (especially statically positioned elements).
787 * TODO: Benchmark the effects of putting this at the top since 99% of props
788 * do not change for a given reconciliation.
789 * TODO: Benchmark areas that can be improved with caching.
790 *
791 * @private
792 * @param {object} lastProps
793 * @param {object} nextProps
794 * @param {?DOMElement} node
795 */
796 _updateDOMProperties: function (lastProps, nextProps, transaction) {
797 var propKey;
798 var styleName;
799 var styleUpdates;
800 for (propKey in lastProps) {
801 if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
802 continue;
803 }
804 if (propKey === STYLE) {
805 var lastStyle = this._previousStyleCopy;
806 for (styleName in lastStyle) {
807 if (lastStyle.hasOwnProperty(styleName)) {
808 styleUpdates = styleUpdates || {};
809 styleUpdates[styleName] = '';
810 }
811 }
812 this._previousStyleCopy = null;
813 } else if (registrationNameModules.hasOwnProperty(propKey)) {
814 if (lastProps[propKey]) {
815 // Only call deleteListener if there was a listener previously or
816 // else willDeleteListener gets called when there wasn't actually a
817 // listener (e.g., onClick={null})
818 deleteListener(this, propKey);
819 }
820 } else if (isCustomComponent(this._tag, lastProps)) {
821 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
822 DOMPropertyOperations.deleteValueForAttribute(getNode(this), propKey);
823 }
824 } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
825 DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
826 }
827 }
828 for (propKey in nextProps) {
829 var nextProp = nextProps[propKey];
830 var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
831 if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
832 continue;
833 }
834 if (propKey === STYLE) {
835 if (nextProp) {
836 if (process.env.NODE_ENV !== 'production') {
837 checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
838 this._previousStyle = nextProp;
839 }
840 nextProp = this._previousStyleCopy = _assign({}, nextProp);
841 } else {
842 this._previousStyleCopy = null;
843 }
844 if (lastProp) {
845 // Unset styles on `lastProp` but not on `nextProp`.
846 for (styleName in lastProp) {
847 if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
848 styleUpdates = styleUpdates || {};
849 styleUpdates[styleName] = '';
850 }
851 }
852 // Update styles that changed since `lastProp`.
853 for (styleName in nextProp) {
854 if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
855 styleUpdates = styleUpdates || {};
856 styleUpdates[styleName] = nextProp[styleName];
857 }
858 }
859 } else {
860 // Relies on `updateStylesByID` not mutating `styleUpdates`.
861 styleUpdates = nextProp;
862 }
863 } else if (registrationNameModules.hasOwnProperty(propKey)) {
864 if (nextProp) {
865 enqueuePutListener(this, propKey, nextProp, transaction);
866 } else if (lastProp) {
867 deleteListener(this, propKey);
868 }
869 } else if (isCustomComponent(this._tag, nextProps)) {
870 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
871 DOMPropertyOperations.setValueForAttribute(getNode(this), propKey, nextProp);
872 }
873 } else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
874 var node = getNode(this);
875 // If we're updating to null or undefined, we should remove the property
876 // from the DOM node instead of inadvertently setting to a string. This
877 // brings us in line with the same behavior we have on initial render.
878 if (nextProp != null) {
879 DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
880 } else {
881 DOMPropertyOperations.deleteValueForProperty(node, propKey);
882 }
883 }
884 }
885 if (styleUpdates) {
886 CSSPropertyOperations.setValueForStyles(getNode(this), styleUpdates, this);
887 }
888 },
889
890 /**
891 * Reconciles the children with the various properties that affect the
892 * children content.
893 *
894 * @param {object} lastProps
895 * @param {object} nextProps
896 * @param {ReactReconcileTransaction} transaction
897 * @param {object} context
898 */
899 _updateDOMChildren: function (lastProps, nextProps, transaction, context) {
900 var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
901 var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
902
903 var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html;
904 var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html;
905
906 // Note the use of `!=` which checks for null or undefined.
907 var lastChildren = lastContent != null ? null : lastProps.children;
908 var nextChildren = nextContent != null ? null : nextProps.children;
909
910 // If we're switching from children to content/html or vice versa, remove
911 // the old content
912 var lastHasContentOrHtml = lastContent != null || lastHtml != null;
913 var nextHasContentOrHtml = nextContent != null || nextHtml != null;
914 if (lastChildren != null && nextChildren == null) {
915 this.updateChildren(null, transaction, context);
916 } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
917 this.updateTextContent('');
918 if (process.env.NODE_ENV !== 'production') {
919 ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
920 }
921 }
922
923 if (nextContent != null) {
924 if (lastContent !== nextContent) {
925 this.updateTextContent('' + nextContent);
926 if (process.env.NODE_ENV !== 'production') {
927 setAndValidateContentChildDev.call(this, nextContent);
928 }
929 }
930 } else if (nextHtml != null) {
931 if (lastHtml !== nextHtml) {
932 this.updateMarkup('' + nextHtml);
933 }
934 if (process.env.NODE_ENV !== 'production') {
935 ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
936 }
937 } else if (nextChildren != null) {
938 if (process.env.NODE_ENV !== 'production') {
939 setAndValidateContentChildDev.call(this, null);
940 }
941
942 this.updateChildren(nextChildren, transaction, context);
943 }
944 },
945
946 getHostNode: function () {
947 return getNode(this);
948 },
949
950 /**
951 * Destroys all event registrations for this instance. Does not remove from
952 * the DOM. That must be done by the parent.
953 *
954 * @internal
955 */
956 unmountComponent: function (safely) {
957 switch (this._tag) {
958 case 'audio':
959 case 'form':
960 case 'iframe':
961 case 'img':
962 case 'link':
963 case 'object':
964 case 'source':
965 case 'video':
966 var listeners = this._wrapperState.listeners;
967 if (listeners) {
968 for (var i = 0; i < listeners.length; i++) {
969 listeners[i].remove();
970 }
971 }
972 break;
973 case 'input':
974 case 'textarea':
975 inputValueTracking.stopTracking(this);
976 break;
977 case 'html':
978 case 'head':
979 case 'body':
980 /**
981 * Components like <html> <head> and <body> can't be removed or added
982 * easily in a cross-browser way, however it's valuable to be able to
983 * take advantage of React's reconciliation for styling and <title>
984 * management. So we just document it and throw in dangerous cases.
985 */
986 !false ? process.env.NODE_ENV !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is impossible to unmount some top-level components (eg <html>, <head>, and <body>) reliably and efficiently. To fix this, have a single top-level component that never unmounts render these elements.', this._tag) : _prodInvariant('66', this._tag) : void 0;
987 break;
988 }
989
990 this.unmountChildren(safely);
991 ReactDOMComponentTree.uncacheNode(this);
992 EventPluginHub.deleteAllListeners(this);
993 this._rootNodeID = 0;
994 this._domID = 0;
995 this._wrapperState = null;
996
997 if (process.env.NODE_ENV !== 'production') {
998 setAndValidateContentChildDev.call(this, null);
999 }
1000 },
1001
1002 getPublicInstance: function () {
1003 return getNode(this);
1004 }
1005};
1006
1007_assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
1008
1009module.exports = ReactDOMComponent;
\No newline at end of file