UNPKG

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