react/src/browser/ui/ReactDOMComponent.js
Paul O’Shannessy c14b3a1b13 Merge pull request #1520 from syranide/ediblechildren
Warn against contentEditable with children props
2014-09-26 17:54:24 -07:00

440 lines
14 KiB
JavaScript

/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactDOMComponent
* @typechecks static-only
*/
"use strict";
var CSSPropertyOperations = require('CSSPropertyOperations');
var DOMProperty = require('DOMProperty');
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactComponent = require('ReactComponent');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactMount = require('ReactMount');
var ReactMultiChild = require('ReactMultiChild');
var ReactPerf = require('ReactPerf');
var escapeTextForBrowser = require('escapeTextForBrowser');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var keyOf = require('keyOf');
var merge = require('merge');
var mixInto = require('mixInto');
var monitorCodeUse = require('monitorCodeUse');
var deleteListener = ReactBrowserEventEmitter.deleteListener;
var listenTo = ReactBrowserEventEmitter.listenTo;
var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
// For quickly matching children type, to test if can be treated as content.
var CONTENT_TYPES = {'string': true, 'number': true};
var STYLE = keyOf({style: null});
var ELEMENT_NODE_TYPE = 1;
/**
* @param {?object} props
*/
function assertValidProps(props) {
if (!props) {
return;
}
// Note the use of `==` which checks for null or undefined.
invariant(
props.children == null || props.dangerouslySetInnerHTML == null,
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
);
if (__DEV__) {
if (props.contentEditable && props.children != null) {
console.warn(
'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.'
);
}
}
invariant(
props.style == null || typeof props.style === 'object',
'The `style` prop expects a mapping from style properties to values, ' +
'not a string.'
);
}
function putListener(id, registrationName, listener, transaction) {
if (__DEV__) {
// IE8 has no API for event capturing and the `onScroll` event doesn't
// bubble.
if (registrationName === 'onScroll' &&
!isEventSupported('scroll', true)) {
monitorCodeUse('react_no_scroll_event');
console.warn('This browser doesn\'t support the `onScroll` event');
}
}
var container = ReactMount.findReactContainerForID(id);
if (container) {
var doc = container.nodeType === ELEMENT_NODE_TYPE ?
container.ownerDocument :
container;
listenTo(registrationName, doc);
}
transaction.getPutListenerQueue().enqueuePutListener(
id,
registrationName,
listener
);
}
/**
* @constructor ReactDOMComponent
* @extends ReactComponent
* @extends ReactMultiChild
*/
function ReactDOMComponent(tag, omitClose) {
this._tagOpen = '<' + tag;
this._tagClose = omitClose ? '' : '</' + tag + '>';
this.tagName = tag.toUpperCase();
}
ReactDOMComponent.Mixin = {
/**
* Generates root tag markup then recurses. This method has side effects and
* is not idempotent.
*
* @internal
* @param {string} rootID The root DOM ID for this node.
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {number} mountDepth number of components in the owner hierarchy
* @return {string} The computed markup.
*/
mountComponent: ReactPerf.measure(
'ReactDOMComponent',
'mountComponent',
function(rootID, transaction, mountDepth) {
ReactComponent.Mixin.mountComponent.call(
this,
rootID,
transaction,
mountDepth
);
assertValidProps(this.props);
return (
this._createOpenTagMarkupAndPutListeners(transaction) +
this._createContentMarkup(transaction) +
this._tagClose
);
}
),
/**
* Creates markup for the open tag and all attributes.
*
* This method has side effects because events get registered.
*
* Iterating over object properties is faster than iterating over arrays.
* @see http://jsperf.com/obj-vs-arr-iteration
*
* @private
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @return {string} Markup of opening tag.
*/
_createOpenTagMarkupAndPutListeners: function(transaction) {
var props = this.props;
var ret = this._tagOpen;
for (var propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
var propValue = props[propKey];
if (propValue == null) {
continue;
}
if (registrationNameModules.hasOwnProperty(propKey)) {
putListener(this._rootNodeID, propKey, propValue, transaction);
} else {
if (propKey === STYLE) {
if (propValue) {
propValue = props.style = merge(props.style);
}
propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
}
var markup =
DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
if (markup) {
ret += ' ' + markup;
}
}
}
// For static pages, no need to put React ID and checksum. Saves lots of
// bytes.
if (transaction.renderToStaticMarkup) {
return ret + '>';
}
var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
return ret + ' ' + markupForID + '>';
},
/**
* Creates markup for the content between the tags.
*
* @private
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @return {string} Content markup.
*/
_createContentMarkup: function(transaction) {
// Intentional use of != to avoid catching zero/false.
var innerHTML = this.props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
return innerHTML.__html;
}
} else {
var contentToUse =
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
var childrenToUse = contentToUse != null ? null : this.props.children;
if (contentToUse != null) {
return escapeTextForBrowser(contentToUse);
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
childrenToUse,
transaction
);
return mountImages.join('');
}
}
return '';
},
receiveComponent: function(nextDescriptor, transaction) {
if (nextDescriptor === this._descriptor &&
nextDescriptor._owner != null) {
// Since descriptors are immutable after the owner is rendered,
// we can do a cheap identity compare here to determine if this is a
// superfluous reconcile. It's possible for state to be mutable but such
// change should trigger an update of the owner which would recreate
// the descriptor. We explicitly check for the existence of an owner since
// it's possible for a descriptor created outside a composite to be
// deeply mutated and reused.
return;
}
ReactComponent.Mixin.receiveComponent.call(
this,
nextDescriptor,
transaction
);
},
/**
* Updates a native DOM component after it has already been allocated and
* attached to the DOM. Reconciles the root DOM node, then recurses.
*
* @param {ReactReconcileTransaction} transaction
* @param {ReactDescriptor} prevDescriptor
* @internal
* @overridable
*/
updateComponent: ReactPerf.measure(
'ReactDOMComponent',
'updateComponent',
function(transaction, prevDescriptor) {
assertValidProps(this._descriptor.props);
ReactComponent.Mixin.updateComponent.call(
this,
transaction,
prevDescriptor
);
this._updateDOMProperties(prevDescriptor.props, transaction);
this._updateDOMChildren(prevDescriptor.props, transaction);
}
),
/**
* Reconciles the properties by detecting differences in property values and
* updating the DOM as necessary. This function is probably the single most
* critical path for performance optimization.
*
* TODO: Benchmark whether checking for changed values in memory actually
* improves performance (especially statically positioned elements).
* TODO: Benchmark the effects of putting this at the top since 99% of props
* do not change for a given reconciliation.
* TODO: Benchmark areas that can be improved with caching.
*
* @private
* @param {object} lastProps
* @param {ReactReconcileTransaction} transaction
*/
_updateDOMProperties: function(lastProps, transaction) {
var nextProps = this.props;
var propKey;
var styleName;
var styleUpdates;
for (propKey in lastProps) {
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey)) {
continue;
}
if (propKey === STYLE) {
var lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
deleteListener(this._rootNodeID, propKey);
} else if (
DOMProperty.isStandardName[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
ReactComponent.BackendIDOperations.deletePropertyByID(
this._rootNodeID,
propKey
);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps[propKey];
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
continue;
}
if (propKey === STYLE) {
if (nextProp) {
nextProp = nextProps.style = merge(nextProp);
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
putListener(this._rootNodeID, propKey, nextProp, transaction);
} else if (
DOMProperty.isStandardName[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
ReactComponent.BackendIDOperations.updatePropertyByID(
this._rootNodeID,
propKey,
nextProp
);
}
}
if (styleUpdates) {
ReactComponent.BackendIDOperations.updateStylesByID(
this._rootNodeID,
styleUpdates
);
}
},
/**
* Reconciles the children with the various properties that affect the
* children content.
*
* @param {object} lastProps
* @param {ReactReconcileTransaction} transaction
*/
_updateDOMChildren: function(lastProps, transaction) {
var nextProps = this.props;
var lastContent =
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
var nextContent =
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
var lastHtml =
lastProps.dangerouslySetInnerHTML &&
lastProps.dangerouslySetInnerHTML.__html;
var nextHtml =
nextProps.dangerouslySetInnerHTML &&
nextProps.dangerouslySetInnerHTML.__html;
// Note the use of `!=` which checks for null or undefined.
var lastChildren = lastContent != null ? null : lastProps.children;
var nextChildren = nextContent != null ? null : nextProps.children;
// If we're switching from children to content/html or vice versa, remove
// the old content
var lastHasContentOrHtml = lastContent != null || lastHtml != null;
var nextHasContentOrHtml = nextContent != null || nextHtml != null;
if (lastChildren != null && nextChildren == null) {
this.updateChildren(null, transaction);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
this.updateTextContent('');
}
if (nextContent != null) {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
}
} else if (nextHtml != null) {
if (lastHtml !== nextHtml) {
ReactComponent.BackendIDOperations.updateInnerHTMLByID(
this._rootNodeID,
nextHtml
);
}
} else if (nextChildren != null) {
this.updateChildren(nextChildren, transaction);
}
},
/**
* Destroys all event registrations for this instance. Does not remove from
* the DOM. That must be done by the parent.
*
* @internal
*/
unmountComponent: function() {
this.unmountChildren();
ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
ReactComponent.Mixin.unmountComponent.call(this);
}
};
mixInto(ReactDOMComponent, ReactComponent.Mixin);
mixInto(ReactDOMComponent, ReactDOMComponent.Mixin);
mixInto(ReactDOMComponent, ReactMultiChild.Mixin);
mixInto(ReactDOMComponent, ReactBrowserComponentMixin);
module.exports = ReactDOMComponent;