react / react-0.13.3 / examples / basic-commonjs / node_modules / react / lib / ReactDOMComponent.js
81143 views/**1* Copyright 2013-2015, 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/* global hasOwnProperty:true */1314'use strict';1516var CSSPropertyOperations = require("./CSSPropertyOperations");17var DOMProperty = require("./DOMProperty");18var DOMPropertyOperations = require("./DOMPropertyOperations");19var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter");20var ReactComponentBrowserEnvironment =21require("./ReactComponentBrowserEnvironment");22var ReactMount = require("./ReactMount");23var ReactMultiChild = require("./ReactMultiChild");24var ReactPerf = require("./ReactPerf");2526var assign = require("./Object.assign");27var escapeTextContentForBrowser = require("./escapeTextContentForBrowser");28var invariant = require("./invariant");29var isEventSupported = require("./isEventSupported");30var keyOf = require("./keyOf");31var warning = require("./warning");3233var deleteListener = ReactBrowserEventEmitter.deleteListener;34var listenTo = ReactBrowserEventEmitter.listenTo;35var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;3637// For quickly matching children type, to test if can be treated as content.38var CONTENT_TYPES = {'string': true, 'number': true};3940var STYLE = keyOf({style: null});4142var ELEMENT_NODE_TYPE = 1;4344/**45* Optionally injectable operations for mutating the DOM46*/47var BackendIDOperations = null;4849/**50* @param {?object} props51*/52function assertValidProps(props) {53if (!props) {54return;55}56// Note the use of `==` which checks for null or undefined.57if (props.dangerouslySetInnerHTML != null) {58("production" !== process.env.NODE_ENV ? invariant(59props.children == null,60'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'61) : invariant(props.children == null));62("production" !== process.env.NODE_ENV ? invariant(63typeof props.dangerouslySetInnerHTML === 'object' &&64'__html' in props.dangerouslySetInnerHTML,65'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +66'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +67'for more information.'68) : invariant(typeof props.dangerouslySetInnerHTML === 'object' &&69'__html' in props.dangerouslySetInnerHTML));70}71if ("production" !== process.env.NODE_ENV) {72("production" !== process.env.NODE_ENV ? warning(73props.innerHTML == null,74'Directly setting property `innerHTML` is not permitted. ' +75'For more information, lookup documentation on `dangerouslySetInnerHTML`.'76) : null);77("production" !== process.env.NODE_ENV ? warning(78!props.contentEditable || props.children == null,79'A component is `contentEditable` and contains `children` managed by ' +80'React. It is now your responsibility to guarantee that none of ' +81'those nodes are unexpectedly modified or duplicated. This is ' +82'probably not intentional.'83) : null);84}85("production" !== process.env.NODE_ENV ? invariant(86props.style == null || typeof props.style === 'object',87'The `style` prop expects a mapping from style properties to values, ' +88'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' +89'using JSX.'90) : invariant(props.style == null || typeof props.style === 'object'));91}9293function putListener(id, registrationName, listener, transaction) {94if ("production" !== process.env.NODE_ENV) {95// IE8 has no API for event capturing and the `onScroll` event doesn't96// bubble.97("production" !== process.env.NODE_ENV ? warning(98registrationName !== 'onScroll' || isEventSupported('scroll', true),99'This browser doesn\'t support the `onScroll` event'100) : null);101}102var container = ReactMount.findReactContainerForID(id);103if (container) {104var doc = container.nodeType === ELEMENT_NODE_TYPE ?105container.ownerDocument :106container;107listenTo(registrationName, doc);108}109transaction.getPutListenerQueue().enqueuePutListener(110id,111registrationName,112listener113);114}115116// For HTML, certain tags should omit their close tag. We keep a whitelist for117// those special cased tags.118119var omittedCloseTags = {120'area': true,121'base': true,122'br': true,123'col': true,124'embed': true,125'hr': true,126'img': true,127'input': true,128'keygen': true,129'link': true,130'meta': true,131'param': true,132'source': true,133'track': true,134'wbr': true135// NOTE: menuitem's close tag should be omitted, but that causes problems.136};137138// We accept any tag to be rendered but since this gets injected into abitrary139// HTML, we want to make sure that it's a safe tag.140// http://www.w3.org/TR/REC-xml/#NT-Name141142var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset143var validatedTagCache = {};144var hasOwnProperty = {}.hasOwnProperty;145146function validateDangerousTag(tag) {147if (!hasOwnProperty.call(validatedTagCache, tag)) {148("production" !== process.env.NODE_ENV ? invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag) : invariant(VALID_TAG_REGEX.test(tag)));149validatedTagCache[tag] = true;150}151}152153/**154* Creates a new React class that is idempotent and capable of containing other155* React components. It accepts event listeners and DOM properties that are156* valid according to `DOMProperty`.157*158* - Event listeners: `onClick`, `onMouseDown`, etc.159* - DOM properties: `className`, `name`, `title`, etc.160*161* The `style` property functions differently from the DOM API. It accepts an162* object mapping of style properties to values.163*164* @constructor ReactDOMComponent165* @extends ReactMultiChild166*/167function ReactDOMComponent(tag) {168validateDangerousTag(tag);169this._tag = tag;170this._renderedChildren = null;171this._previousStyleCopy = null;172this._rootNodeID = null;173}174175ReactDOMComponent.displayName = 'ReactDOMComponent';176177ReactDOMComponent.Mixin = {178179construct: function(element) {180this._currentElement = element;181},182183/**184* Generates root tag markup then recurses. This method has side effects and185* is not idempotent.186*187* @internal188* @param {string} rootID The root DOM ID for this node.189* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction190* @return {string} The computed markup.191*/192mountComponent: function(rootID, transaction, context) {193this._rootNodeID = rootID;194assertValidProps(this._currentElement.props);195var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>';196return (197this._createOpenTagMarkupAndPutListeners(transaction) +198this._createContentMarkup(transaction, context) +199closeTag200);201},202203/**204* Creates markup for the open tag and all attributes.205*206* This method has side effects because events get registered.207*208* Iterating over object properties is faster than iterating over arrays.209* @see http://jsperf.com/obj-vs-arr-iteration210*211* @private212* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction213* @return {string} Markup of opening tag.214*/215_createOpenTagMarkupAndPutListeners: function(transaction) {216var props = this._currentElement.props;217var ret = '<' + this._tag;218219for (var propKey in props) {220if (!props.hasOwnProperty(propKey)) {221continue;222}223var propValue = props[propKey];224if (propValue == null) {225continue;226}227if (registrationNameModules.hasOwnProperty(propKey)) {228putListener(this._rootNodeID, propKey, propValue, transaction);229} else {230if (propKey === STYLE) {231if (propValue) {232propValue = this._previousStyleCopy = assign({}, props.style);233}234propValue = CSSPropertyOperations.createMarkupForStyles(propValue);235}236var markup =237DOMPropertyOperations.createMarkupForProperty(propKey, propValue);238if (markup) {239ret += ' ' + markup;240}241}242}243244// For static pages, no need to put React ID and checksum. Saves lots of245// bytes.246if (transaction.renderToStaticMarkup) {247return ret + '>';248}249250var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);251return ret + ' ' + markupForID + '>';252},253254/**255* Creates markup for the content between the tags.256*257* @private258* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction259* @param {object} context260* @return {string} Content markup.261*/262_createContentMarkup: function(transaction, context) {263var prefix = '';264if (this._tag === 'listing' ||265this._tag === 'pre' ||266this._tag === 'textarea') {267// Add an initial newline because browsers ignore the first newline in268// a <listing>, <pre>, or <textarea> as an "authoring convenience" -- see269// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody.270prefix = '\n';271}272273var props = this._currentElement.props;274275// Intentional use of != to avoid catching zero/false.276var innerHTML = props.dangerouslySetInnerHTML;277if (innerHTML != null) {278if (innerHTML.__html != null) {279return prefix + innerHTML.__html;280}281} else {282var contentToUse =283CONTENT_TYPES[typeof props.children] ? props.children : null;284var childrenToUse = contentToUse != null ? null : props.children;285if (contentToUse != null) {286return prefix + escapeTextContentForBrowser(contentToUse);287} else if (childrenToUse != null) {288var mountImages = this.mountChildren(289childrenToUse,290transaction,291context292);293return prefix + mountImages.join('');294}295}296return prefix;297},298299receiveComponent: function(nextElement, transaction, context) {300var prevElement = this._currentElement;301this._currentElement = nextElement;302this.updateComponent(transaction, prevElement, nextElement, context);303},304305/**306* Updates a native DOM component after it has already been allocated and307* attached to the DOM. Reconciles the root DOM node, then recurses.308*309* @param {ReactReconcileTransaction} transaction310* @param {ReactElement} prevElement311* @param {ReactElement} nextElement312* @internal313* @overridable314*/315updateComponent: function(transaction, prevElement, nextElement, context) {316assertValidProps(this._currentElement.props);317this._updateDOMProperties(prevElement.props, transaction);318this._updateDOMChildren(prevElement.props, transaction, context);319},320321/**322* Reconciles the properties by detecting differences in property values and323* updating the DOM as necessary. This function is probably the single most324* critical path for performance optimization.325*326* TODO: Benchmark whether checking for changed values in memory actually327* improves performance (especially statically positioned elements).328* TODO: Benchmark the effects of putting this at the top since 99% of props329* do not change for a given reconciliation.330* TODO: Benchmark areas that can be improved with caching.331*332* @private333* @param {object} lastProps334* @param {ReactReconcileTransaction} transaction335*/336_updateDOMProperties: function(lastProps, transaction) {337var nextProps = this._currentElement.props;338var propKey;339var styleName;340var styleUpdates;341for (propKey in lastProps) {342if (nextProps.hasOwnProperty(propKey) ||343!lastProps.hasOwnProperty(propKey)) {344continue;345}346if (propKey === STYLE) {347var lastStyle = this._previousStyleCopy;348for (styleName in lastStyle) {349if (lastStyle.hasOwnProperty(styleName)) {350styleUpdates = styleUpdates || {};351styleUpdates[styleName] = '';352}353}354this._previousStyleCopy = null;355} else if (registrationNameModules.hasOwnProperty(propKey)) {356deleteListener(this._rootNodeID, propKey);357} else if (358DOMProperty.isStandardName[propKey] ||359DOMProperty.isCustomAttribute(propKey)) {360BackendIDOperations.deletePropertyByID(361this._rootNodeID,362propKey363);364}365}366for (propKey in nextProps) {367var nextProp = nextProps[propKey];368var lastProp = propKey === STYLE ?369this._previousStyleCopy :370lastProps[propKey];371if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {372continue;373}374if (propKey === STYLE) {375if (nextProp) {376nextProp = this._previousStyleCopy = assign({}, nextProp);377} else {378this._previousStyleCopy = null;379}380if (lastProp) {381// Unset styles on `lastProp` but not on `nextProp`.382for (styleName in lastProp) {383if (lastProp.hasOwnProperty(styleName) &&384(!nextProp || !nextProp.hasOwnProperty(styleName))) {385styleUpdates = styleUpdates || {};386styleUpdates[styleName] = '';387}388}389// Update styles that changed since `lastProp`.390for (styleName in nextProp) {391if (nextProp.hasOwnProperty(styleName) &&392lastProp[styleName] !== nextProp[styleName]) {393styleUpdates = styleUpdates || {};394styleUpdates[styleName] = nextProp[styleName];395}396}397} else {398// Relies on `updateStylesByID` not mutating `styleUpdates`.399styleUpdates = nextProp;400}401} else if (registrationNameModules.hasOwnProperty(propKey)) {402putListener(this._rootNodeID, propKey, nextProp, transaction);403} else if (404DOMProperty.isStandardName[propKey] ||405DOMProperty.isCustomAttribute(propKey)) {406BackendIDOperations.updatePropertyByID(407this._rootNodeID,408propKey,409nextProp410);411}412}413if (styleUpdates) {414BackendIDOperations.updateStylesByID(415this._rootNodeID,416styleUpdates417);418}419},420421/**422* Reconciles the children with the various properties that affect the423* children content.424*425* @param {object} lastProps426* @param {ReactReconcileTransaction} transaction427*/428_updateDOMChildren: function(lastProps, transaction, context) {429var nextProps = this._currentElement.props;430431var lastContent =432CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;433var nextContent =434CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;435436var lastHtml =437lastProps.dangerouslySetInnerHTML &&438lastProps.dangerouslySetInnerHTML.__html;439var nextHtml =440nextProps.dangerouslySetInnerHTML &&441nextProps.dangerouslySetInnerHTML.__html;442443// Note the use of `!=` which checks for null or undefined.444var lastChildren = lastContent != null ? null : lastProps.children;445var nextChildren = nextContent != null ? null : nextProps.children;446447// If we're switching from children to content/html or vice versa, remove448// the old content449var lastHasContentOrHtml = lastContent != null || lastHtml != null;450var nextHasContentOrHtml = nextContent != null || nextHtml != null;451if (lastChildren != null && nextChildren == null) {452this.updateChildren(null, transaction, context);453} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {454this.updateTextContent('');455}456457if (nextContent != null) {458if (lastContent !== nextContent) {459this.updateTextContent('' + nextContent);460}461} else if (nextHtml != null) {462if (lastHtml !== nextHtml) {463BackendIDOperations.updateInnerHTMLByID(464this._rootNodeID,465nextHtml466);467}468} else if (nextChildren != null) {469this.updateChildren(nextChildren, transaction, context);470}471},472473/**474* Destroys all event registrations for this instance. Does not remove from475* the DOM. That must be done by the parent.476*477* @internal478*/479unmountComponent: function() {480this.unmountChildren();481ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);482ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);483this._rootNodeID = null;484}485486};487488ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', {489mountComponent: 'mountComponent',490updateComponent: 'updateComponent'491});492493assign(494ReactDOMComponent.prototype,495ReactDOMComponent.Mixin,496ReactMultiChild.Mixin497);498499ReactDOMComponent.injection = {500injectIDOperations: function(IDOperations) {501ReactDOMComponent.BackendIDOperations = BackendIDOperations = IDOperations;502}503};504505module.exports = ReactDOMComponent;506507508