UNPKG

react-addons

Version:

Simple packaging of react addons to avoid fiddly 'react/addons' npm module.

551 lines (508 loc) 18.5 kB
/** * 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 ReactComponent */ "use strict"; var ReactComponentEnvironment = require("./ReactComponentEnvironment"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactOwner = require("./ReactOwner"); var ReactUpdates = require("./ReactUpdates"); var invariant = require("./invariant"); var keyMirror = require("./keyMirror"); var merge = require("./merge"); /** * Every React component is in one of these life cycles. */ var ComponentLifeCycle = keyMirror({ /** * Mounted components have a DOM node representation and are capable of * receiving new props. */ MOUNTED: null, /** * Unmounted components are inactive and cannot receive new props. */ UNMOUNTED: null }); /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between * updates. */ var ownerHasExplicitKeyWarning = {}; var ownerHasPropertyWarning = {}; var NUMERIC_PROPERTY_REGEX = /^\d+$/; /** * Warn if the component doesn't have an explicit key assigned to it. * This component is in an array. The array could grow and shrink or be * reordered. All children that haven't already been validated are required to * have a "key" property assigned to it. * * @internal * @param {ReactComponent} component Component that requires a key. */ function validateExplicitKey(component) { if (component.__keyValidated__ || component.props.key != null) { return; } component.__keyValidated__ = true; // We can't provide friendly warnings for top level components. if (!ReactCurrentOwner.current) { return; } // Name of the component whose render method tried to pass children. var currentName = ReactCurrentOwner.current.constructor.displayName; if (ownerHasExplicitKeyWarning.hasOwnProperty(currentName)) { return; } ownerHasExplicitKeyWarning[currentName] = true; var message = 'Each child in an array should have a unique "key" prop. ' + 'Check the render method of ' + currentName + '.'; if (!component.isOwnedBy(ReactCurrentOwner.current)) { // Name of the component that originally created this child. var childOwnerName = component._owner && component._owner.constructor.displayName; // Usually the current owner is the offender, but if it accepts // children as a property, it may be the creator of the child that's // responsible for assigning it a key. message += ' It was passed a child from ' + childOwnerName + '.'; } message += ' See http://fb.me/react-warning-keys for more information.'; console.warn(message); } /** * Warn if the key is being defined as an object property but has an incorrect * value. * * @internal * @param {string} name Property name of the key. * @param {ReactComponent} component Component that requires a key. */ function validatePropertyKey(name) { if (NUMERIC_PROPERTY_REGEX.test(name)) { // Name of the component whose render method tried to pass children. var currentName = ReactCurrentOwner.current.constructor.displayName; if (ownerHasPropertyWarning.hasOwnProperty(currentName)) { return; } ownerHasPropertyWarning[currentName] = true; console.warn( 'Child objects should have non-numeric keys so ordering is preserved. ' + 'Check the render method of ' + currentName + '. ' + 'See http://fb.me/react-warning-keys for more information.' ); } } /** * Ensure that every component either is passed in a static location, in an * array with an explicit keys property defined, or in an object literal * with valid key property. * * @internal * @param {*} component Statically passed child of any type. * @return {boolean} */ function validateChildKeys(component) { if (Array.isArray(component)) { for (var i = 0; i < component.length; i++) { var child = component[i]; if (ReactComponent.isValidComponent(child)) { validateExplicitKey(child); } } } else if (ReactComponent.isValidComponent(component)) { // This component was passed in a valid location. component.__keyValidated__ = true; } else if (component && typeof component === 'object') { for (var name in component) { validatePropertyKey(name, component); } } } /** * Components are the basic units of composition in React. * * Every component accepts a set of keyed input parameters known as "props" that * are initialized by the constructor. Once a component is mounted, the props * can be mutated using `setProps` or `replaceProps`. * * Every component is capable of the following operations: * * `mountComponent` * Initializes the component, renders markup, and registers event listeners. * * `receiveComponent` * Updates the rendered DOM nodes to match the given component. * * `unmountComponent` * Releases any resources allocated by this component. * * Components can also be "owned" by other components. Being owned by another * component means being constructed by that component. This is different from * being the child of a component, which means having a DOM representation that * is a child of the DOM representation of that component. * * @class ReactComponent */ var ReactComponent = { /** * @param {?object} object * @return {boolean} True if `object` is a valid component. * @final */ isValidComponent: function(object) { if (!object || !object.type || !object.type.prototype) { return false; } // This is the safer way of duck checking the type of instance this is. // The object can be a generic descriptor but the type property refers to // the constructor and it's prototype can be used to inspect the type that // will actually get mounted. var prototype = object.type.prototype; return ( typeof prototype.mountComponentIntoNode === 'function' && typeof prototype.receiveComponent === 'function' ); }, /** * @internal */ LifeCycle: ComponentLifeCycle, /** * Injected module that provides ability to mutate individual properties. * Injected into the base class because many different subclasses need access * to this. * * @internal */ BackendIDOperations: ReactComponentEnvironment.BackendIDOperations, /** * Optionally injectable environment dependent cleanup hook. (server vs. * browser etc). Example: A browser system caches DOM nodes based on component * ID and must remove that cache entry when this instance is unmounted. * * @private */ unmountIDFromEnvironment: ReactComponentEnvironment.unmountIDFromEnvironment, /** * The "image" of a component tree, is the platform specific (typically * serialized) data that represents a tree of lower level UI building blocks. * On the web, this "image" is HTML markup which describes a construction of * low level `div` and `span` nodes. Other platforms may have different * encoding of this "image". This must be injected. * * @private */ mountImageIntoNode: ReactComponentEnvironment.mountImageIntoNode, /** * React references `ReactReconcileTransaction` using this property in order * to allow dependency injection. * * @internal */ ReactReconcileTransaction: ReactComponentEnvironment.ReactReconcileTransaction, /** * Base functionality for every ReactComponent constructor. Mixed into the * `ReactComponent` prototype, but exposed statically for easy access. * * @lends {ReactComponent.prototype} */ Mixin: merge(ReactComponentEnvironment.Mixin, { /** * Checks whether or not this component is mounted. * * @return {boolean} True if mounted, false otherwise. * @final * @protected */ isMounted: function() { return this._lifeCycleState === ComponentLifeCycle.MOUNTED; }, /** * Sets a subset of the props. * * @param {object} partialProps Subset of the next props. * @param {?function} callback Called after props are updated. * @final * @public */ setProps: function(partialProps, callback) { // Merge with `_pendingProps` if it exists, otherwise with existing props. this.replaceProps( merge(this._pendingProps || this.props, partialProps), callback ); }, /** * Replaces all of the props. * * @param {object} props New props. * @param {?function} callback Called after props are updated. * @final * @public */ replaceProps: function(props, callback) { ("production" !== process.env.NODE_ENV ? invariant( this.isMounted(), 'replaceProps(...): Can only update a mounted component.' ) : invariant(this.isMounted())); ("production" !== process.env.NODE_ENV ? invariant( this._mountDepth === 0, 'replaceProps(...): You called `setProps` or `replaceProps` on a ' + 'component with a parent. This is an anti-pattern since props will ' + 'get reactively updated when rendered. Instead, change the owner\'s ' + '`render` method to pass the correct value as props to the component ' + 'where it is created.' ) : invariant(this._mountDepth === 0)); this._pendingProps = props; ReactUpdates.enqueueUpdate(this, callback); }, /** * Base constructor for all React components. * * Subclasses that override this method should make sure to invoke * `ReactComponent.Mixin.construct.call(this, ...)`. * * @param {?object} initialProps * @param {*} children * @internal */ construct: function(initialProps, children) { this.props = initialProps || {}; // Record the component responsible for creating this component. this._owner = ReactCurrentOwner.current; // All components start unmounted. this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; this._pendingProps = null; this._pendingCallbacks = null; // Unlike _pendingProps and _pendingCallbacks, we won't use null to // indicate that nothing is pending because it's possible for a component // to have a null owner. Instead, an owner change is pending when // this._owner !== this._pendingOwner. this._pendingOwner = this._owner; // Children can be more than one argument var childrenLength = arguments.length - 1; if (childrenLength === 1) { if ("production" !== process.env.NODE_ENV) { validateChildKeys(children); } this.props.children = children; } else if (childrenLength > 1) { var childArray = Array(childrenLength); for (var i = 0; i < childrenLength; i++) { if ("production" !== process.env.NODE_ENV) { validateChildKeys(arguments[i + 1]); } childArray[i] = arguments[i + 1]; } this.props.children = childArray; } }, /** * Initializes the component, renders markup, and registers event listeners. * * NOTE: This does not insert any nodes into the DOM. * * Subclasses that override this method should make sure to invoke * `ReactComponent.Mixin.mountComponent.call(this, ...)`. * * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction} transaction * @param {number} mountDepth number of components in the owner hierarchy. * @return {?string} Rendered markup to be inserted into the DOM. * @internal */ mountComponent: function(rootID, transaction, mountDepth) { ("production" !== process.env.NODE_ENV ? invariant( !this.isMounted(), 'mountComponent(%s, ...): Can only mount an unmounted component. ' + 'Make sure to avoid storing components between renders or reusing a ' + 'single component instance in multiple places.', rootID ) : invariant(!this.isMounted())); var props = this.props; if (props.ref != null) { ReactOwner.addComponentAsRefTo(this, props.ref, this._owner); } this._rootNodeID = rootID; this._lifeCycleState = ComponentLifeCycle.MOUNTED; this._mountDepth = mountDepth; // Effectively: return ''; }, /** * Releases any resources allocated by `mountComponent`. * * NOTE: This does not remove any nodes from the DOM. * * Subclasses that override this method should make sure to invoke * `ReactComponent.Mixin.unmountComponent.call(this)`. * * @internal */ unmountComponent: function() { ("production" !== process.env.NODE_ENV ? invariant( this.isMounted(), 'unmountComponent(): Can only unmount a mounted component.' ) : invariant(this.isMounted())); var props = this.props; if (props.ref != null) { ReactOwner.removeComponentAsRefFrom(this, props.ref, this._owner); } ReactComponent.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null; this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; }, /** * Given a new instance of this component, updates the rendered DOM nodes * as if that instance was rendered instead. * * Subclasses that override this method should make sure to invoke * `ReactComponent.Mixin.receiveComponent.call(this, ...)`. * * @param {object} nextComponent Next set of properties. * @param {ReactReconcileTransaction} transaction * @internal */ receiveComponent: function(nextComponent, transaction) { ("production" !== process.env.NODE_ENV ? invariant( this.isMounted(), 'receiveComponent(...): Can only update a mounted component.' ) : invariant(this.isMounted())); this._pendingOwner = nextComponent._owner; this._pendingProps = nextComponent.props; this._performUpdateIfNecessary(transaction); }, /** * Call `_performUpdateIfNecessary` within a new transaction. * * @param {ReactReconcileTransaction} transaction * @internal */ performUpdateIfNecessary: function() { var transaction = ReactComponent.ReactReconcileTransaction.getPooled(); transaction.perform(this._performUpdateIfNecessary, this, transaction); ReactComponent.ReactReconcileTransaction.release(transaction); }, /** * If `_pendingProps` is set, update the component. * * @param {ReactReconcileTransaction} transaction * @internal */ _performUpdateIfNecessary: function(transaction) { if (this._pendingProps == null) { return; } var prevProps = this.props; var prevOwner = this._owner; this.props = this._pendingProps; this._owner = this._pendingOwner; this._pendingProps = null; this.updateComponent(transaction, prevProps, prevOwner); }, /** * Updates the component's currently mounted representation. * * @param {ReactReconcileTransaction} transaction * @param {object} prevProps * @internal */ updateComponent: function(transaction, prevProps, prevOwner) { var props = this.props; // If either the owner or a `ref` has changed, make sure the newest owner // has stored a reference to `this`, and the previous owner (if different) // has forgotten the reference to `this`. if (this._owner !== prevOwner || props.ref !== prevProps.ref) { if (prevProps.ref != null) { ReactOwner.removeComponentAsRefFrom( this, prevProps.ref, prevOwner ); } // Correct, even if the owner is the same, and only the ref has changed. if (props.ref != null) { ReactOwner.addComponentAsRefTo(this, props.ref, this._owner); } } }, /** * Mounts this component and inserts it into the DOM. * * @param {string} rootID DOM ID of the root node. * @param {DOMElement} container DOM element to mount into. * @param {boolean} shouldReuseMarkup If true, do not insert markup * @final * @internal * @see {ReactMount.renderComponent} */ mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) { var transaction = ReactComponent.ReactReconcileTransaction.getPooled(); transaction.perform( this._mountComponentIntoNode, this, rootID, container, transaction, shouldReuseMarkup ); ReactComponent.ReactReconcileTransaction.release(transaction); }, /** * @param {string} rootID DOM ID of the root node. * @param {DOMElement} container DOM element to mount into. * @param {ReactReconcileTransaction} transaction * @param {boolean} shouldReuseMarkup If true, do not insert markup * @final * @private */ _mountComponentIntoNode: function( rootID, container, transaction, shouldReuseMarkup) { var markup = this.mountComponent(rootID, transaction, 0); ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup); }, /** * Checks if this component is owned by the supplied `owner` component. * * @param {ReactComponent} owner Component to check. * @return {boolean} True if `owners` owns this component. * @final * @internal */ isOwnedBy: function(owner) { return this._owner === owner; }, /** * Gets another component, that shares the same owner as this one, by ref. * * @param {string} ref of a sibling Component. * @return {?ReactComponent} the actual sibling Component. * @final * @internal */ getSiblingByRef: function(ref) { var owner = this._owner; if (!owner || !owner.refs) { return null; } return owner.refs[ref]; } }) }; module.exports = ReactComponent;