react / react-0.13.3 / examples / basic-commonjs / node_modules / reactify / node_modules / react-tools / src / browser / ui / ReactDOMComponent.js
81155 views/**1* Copyright 2013-2014, Facebook, Inc.2* All rights reserved.3*4* This source code is licensed under the BSD-style license found in the5* LICENSE file in the root directory of this source tree. An additional grant6* of patent rights can be found in the PATENTS file in the same directory.7*8* @providesModule ReactDOMComponent9* @typechecks static-only10*/1112"use strict";1314var CSSPropertyOperations = require('CSSPropertyOperations');15var DOMProperty = require('DOMProperty');16var DOMPropertyOperations = require('DOMPropertyOperations');17var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');18var ReactComponent = require('ReactComponent');19var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');20var ReactMount = require('ReactMount');21var ReactMultiChild = require('ReactMultiChild');22var ReactPerf = require('ReactPerf');2324var assign = require('Object.assign');25var escapeTextForBrowser = require('escapeTextForBrowser');26var invariant = require('invariant');27var isEventSupported = require('isEventSupported');28var keyOf = require('keyOf');29var monitorCodeUse = require('monitorCodeUse');3031var deleteListener = ReactBrowserEventEmitter.deleteListener;32var listenTo = ReactBrowserEventEmitter.listenTo;33var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;3435// For quickly matching children type, to test if can be treated as content.36var CONTENT_TYPES = {'string': true, 'number': true};3738var STYLE = keyOf({style: null});3940var ELEMENT_NODE_TYPE = 1;4142/**43* @param {?object} props44*/45function assertValidProps(props) {46if (!props) {47return;48}49// Note the use of `==` which checks for null or undefined.50invariant(51props.children == null || props.dangerouslySetInnerHTML == null,52'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'53);54if (__DEV__) {55if (props.contentEditable && props.children != null) {56console.warn(57'A component is `contentEditable` and contains `children` managed by ' +58'React. It is now your responsibility to guarantee that none of those '+59'nodes are unexpectedly modified or duplicated. This is probably not ' +60'intentional.'61);62}63}64invariant(65props.style == null || typeof props.style === 'object',66'The `style` prop expects a mapping from style properties to values, ' +67'not a string.'68);69}7071function putListener(id, registrationName, listener, transaction) {72if (__DEV__) {73// IE8 has no API for event capturing and the `onScroll` event doesn't74// bubble.75if (registrationName === 'onScroll' &&76!isEventSupported('scroll', true)) {77monitorCodeUse('react_no_scroll_event');78console.warn('This browser doesn\'t support the `onScroll` event');79}80}81var container = ReactMount.findReactContainerForID(id);82if (container) {83var doc = container.nodeType === ELEMENT_NODE_TYPE ?84container.ownerDocument :85container;86listenTo(registrationName, doc);87}88transaction.getPutListenerQueue().enqueuePutListener(89id,90registrationName,91listener92);93}9495// For HTML, certain tags should omit their close tag. We keep a whitelist for96// those special cased tags.9798var omittedCloseTags = {99'area': true,100'base': true,101'br': true,102'col': true,103'embed': true,104'hr': true,105'img': true,106'input': true,107'keygen': true,108'link': true,109'meta': true,110'param': true,111'source': true,112'track': true,113'wbr': true114// NOTE: menuitem's close tag should be omitted, but that causes problems.115};116117// We accept any tag to be rendered but since this gets injected into abitrary118// HTML, we want to make sure that it's a safe tag.119// http://www.w3.org/TR/REC-xml/#NT-Name120121var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset122var validatedTagCache = {};123var hasOwnProperty = {}.hasOwnProperty;124125function validateDangerousTag(tag) {126if (!hasOwnProperty.call(validatedTagCache, tag)) {127invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag);128validatedTagCache[tag] = true;129}130}131132/**133* Creates a new React class that is idempotent and capable of containing other134* React components. It accepts event listeners and DOM properties that are135* valid according to `DOMProperty`.136*137* - Event listeners: `onClick`, `onMouseDown`, etc.138* - DOM properties: `className`, `name`, `title`, etc.139*140* The `style` property functions differently from the DOM API. It accepts an141* object mapping of style properties to values.142*143* @constructor ReactDOMComponent144* @extends ReactComponent145* @extends ReactMultiChild146*/147function ReactDOMComponent(tag) {148validateDangerousTag(tag);149this._tag = tag;150this.tagName = tag.toUpperCase();151}152153ReactDOMComponent.displayName = 'ReactDOMComponent';154155ReactDOMComponent.Mixin = {156157/**158* Generates root tag markup then recurses. This method has side effects and159* is not idempotent.160*161* @internal162* @param {string} rootID The root DOM ID for this node.163* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction164* @param {number} mountDepth number of components in the owner hierarchy165* @return {string} The computed markup.166*/167mountComponent: ReactPerf.measure(168'ReactDOMComponent',169'mountComponent',170function(rootID, transaction, mountDepth) {171ReactComponent.Mixin.mountComponent.call(172this,173rootID,174transaction,175mountDepth176);177assertValidProps(this.props);178var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>';179return (180this._createOpenTagMarkupAndPutListeners(transaction) +181this._createContentMarkup(transaction) +182closeTag183);184}185),186187/**188* Creates markup for the open tag and all attributes.189*190* This method has side effects because events get registered.191*192* Iterating over object properties is faster than iterating over arrays.193* @see http://jsperf.com/obj-vs-arr-iteration194*195* @private196* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction197* @return {string} Markup of opening tag.198*/199_createOpenTagMarkupAndPutListeners: function(transaction) {200var props = this.props;201var ret = '<' + this._tag;202203for (var propKey in props) {204if (!props.hasOwnProperty(propKey)) {205continue;206}207var propValue = props[propKey];208if (propValue == null) {209continue;210}211if (registrationNameModules.hasOwnProperty(propKey)) {212putListener(this._rootNodeID, propKey, propValue, transaction);213} else {214if (propKey === STYLE) {215if (propValue) {216propValue = props.style = assign({}, props.style);217}218propValue = CSSPropertyOperations.createMarkupForStyles(propValue);219}220var markup =221DOMPropertyOperations.createMarkupForProperty(propKey, propValue);222if (markup) {223ret += ' ' + markup;224}225}226}227228// For static pages, no need to put React ID and checksum. Saves lots of229// bytes.230if (transaction.renderToStaticMarkup) {231return ret + '>';232}233234var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);235return ret + ' ' + markupForID + '>';236},237238/**239* Creates markup for the content between the tags.240*241* @private242* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction243* @return {string} Content markup.244*/245_createContentMarkup: function(transaction) {246// Intentional use of != to avoid catching zero/false.247var innerHTML = this.props.dangerouslySetInnerHTML;248if (innerHTML != null) {249if (innerHTML.__html != null) {250return innerHTML.__html;251}252} else {253var contentToUse =254CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;255var childrenToUse = contentToUse != null ? null : this.props.children;256if (contentToUse != null) {257return escapeTextForBrowser(contentToUse);258} else if (childrenToUse != null) {259var mountImages = this.mountChildren(260childrenToUse,261transaction262);263return mountImages.join('');264}265}266return '';267},268269receiveComponent: function(nextElement, transaction) {270if (nextElement === this._currentElement &&271nextElement._owner != null) {272// Since elements are immutable after the owner is rendered,273// we can do a cheap identity compare here to determine if this is a274// superfluous reconcile. It's possible for state to be mutable but such275// change should trigger an update of the owner which would recreate276// the element. We explicitly check for the existence of an owner since277// it's possible for a element created outside a composite to be278// deeply mutated and reused.279return;280}281282ReactComponent.Mixin.receiveComponent.call(283this,284nextElement,285transaction286);287},288289/**290* Updates a native DOM component after it has already been allocated and291* attached to the DOM. Reconciles the root DOM node, then recurses.292*293* @param {ReactReconcileTransaction} transaction294* @param {ReactElement} prevElement295* @internal296* @overridable297*/298updateComponent: ReactPerf.measure(299'ReactDOMComponent',300'updateComponent',301function(transaction, prevElement) {302assertValidProps(this._currentElement.props);303ReactComponent.Mixin.updateComponent.call(304this,305transaction,306prevElement307);308this._updateDOMProperties(prevElement.props, transaction);309this._updateDOMChildren(prevElement.props, transaction);310}311),312313/**314* Reconciles the properties by detecting differences in property values and315* updating the DOM as necessary. This function is probably the single most316* critical path for performance optimization.317*318* TODO: Benchmark whether checking for changed values in memory actually319* improves performance (especially statically positioned elements).320* TODO: Benchmark the effects of putting this at the top since 99% of props321* do not change for a given reconciliation.322* TODO: Benchmark areas that can be improved with caching.323*324* @private325* @param {object} lastProps326* @param {ReactReconcileTransaction} transaction327*/328_updateDOMProperties: function(lastProps, transaction) {329var nextProps = this.props;330var propKey;331var styleName;332var styleUpdates;333for (propKey in lastProps) {334if (nextProps.hasOwnProperty(propKey) ||335!lastProps.hasOwnProperty(propKey)) {336continue;337}338if (propKey === STYLE) {339var lastStyle = lastProps[propKey];340for (styleName in lastStyle) {341if (lastStyle.hasOwnProperty(styleName)) {342styleUpdates = styleUpdates || {};343styleUpdates[styleName] = '';344}345}346} else if (registrationNameModules.hasOwnProperty(propKey)) {347deleteListener(this._rootNodeID, propKey);348} else if (349DOMProperty.isStandardName[propKey] ||350DOMProperty.isCustomAttribute(propKey)) {351ReactComponent.BackendIDOperations.deletePropertyByID(352this._rootNodeID,353propKey354);355}356}357for (propKey in nextProps) {358var nextProp = nextProps[propKey];359var lastProp = lastProps[propKey];360if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {361continue;362}363if (propKey === STYLE) {364if (nextProp) {365nextProp = nextProps.style = assign({}, nextProp);366}367if (lastProp) {368// Unset styles on `lastProp` but not on `nextProp`.369for (styleName in lastProp) {370if (lastProp.hasOwnProperty(styleName) &&371(!nextProp || !nextProp.hasOwnProperty(styleName))) {372styleUpdates = styleUpdates || {};373styleUpdates[styleName] = '';374}375}376// Update styles that changed since `lastProp`.377for (styleName in nextProp) {378if (nextProp.hasOwnProperty(styleName) &&379lastProp[styleName] !== nextProp[styleName]) {380styleUpdates = styleUpdates || {};381styleUpdates[styleName] = nextProp[styleName];382}383}384} else {385// Relies on `updateStylesByID` not mutating `styleUpdates`.386styleUpdates = nextProp;387}388} else if (registrationNameModules.hasOwnProperty(propKey)) {389putListener(this._rootNodeID, propKey, nextProp, transaction);390} else if (391DOMProperty.isStandardName[propKey] ||392DOMProperty.isCustomAttribute(propKey)) {393ReactComponent.BackendIDOperations.updatePropertyByID(394this._rootNodeID,395propKey,396nextProp397);398}399}400if (styleUpdates) {401ReactComponent.BackendIDOperations.updateStylesByID(402this._rootNodeID,403styleUpdates404);405}406},407408/**409* Reconciles the children with the various properties that affect the410* children content.411*412* @param {object} lastProps413* @param {ReactReconcileTransaction} transaction414*/415_updateDOMChildren: function(lastProps, transaction) {416var nextProps = this.props;417418var lastContent =419CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;420var nextContent =421CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;422423var lastHtml =424lastProps.dangerouslySetInnerHTML &&425lastProps.dangerouslySetInnerHTML.__html;426var nextHtml =427nextProps.dangerouslySetInnerHTML &&428nextProps.dangerouslySetInnerHTML.__html;429430// Note the use of `!=` which checks for null or undefined.431var lastChildren = lastContent != null ? null : lastProps.children;432var nextChildren = nextContent != null ? null : nextProps.children;433434// If we're switching from children to content/html or vice versa, remove435// the old content436var lastHasContentOrHtml = lastContent != null || lastHtml != null;437var nextHasContentOrHtml = nextContent != null || nextHtml != null;438if (lastChildren != null && nextChildren == null) {439this.updateChildren(null, transaction);440} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {441this.updateTextContent('');442}443444if (nextContent != null) {445if (lastContent !== nextContent) {446this.updateTextContent('' + nextContent);447}448} else if (nextHtml != null) {449if (lastHtml !== nextHtml) {450ReactComponent.BackendIDOperations.updateInnerHTMLByID(451this._rootNodeID,452nextHtml453);454}455} else if (nextChildren != null) {456this.updateChildren(nextChildren, transaction);457}458},459460/**461* Destroys all event registrations for this instance. Does not remove from462* the DOM. That must be done by the parent.463*464* @internal465*/466unmountComponent: function() {467this.unmountChildren();468ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);469ReactComponent.Mixin.unmountComponent.call(this);470}471472};473474assign(475ReactDOMComponent.prototype,476ReactComponent.Mixin,477ReactDOMComponent.Mixin,478ReactMultiChild.Mixin,479ReactBrowserComponentMixin480);481482module.exports = ReactDOMComponent;483484485