UNPKG

d2-ui

Version:
694 lines (608 loc) 27.5 kB
/** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactCompositeComponent */ 'use strict'; var ReactComponentEnvironment = require('./ReactComponentEnvironment'); var ReactCurrentOwner = require('./ReactCurrentOwner'); var ReactElement = require('./ReactElement'); var ReactInstanceMap = require('./ReactInstanceMap'); var ReactPerf = require('./ReactPerf'); var ReactPropTypeLocations = require('./ReactPropTypeLocations'); var ReactPropTypeLocationNames = require('./ReactPropTypeLocationNames'); var ReactReconciler = require('./ReactReconciler'); var ReactUpdateQueue = require('./ReactUpdateQueue'); var assign = require('./Object.assign'); var emptyObject = require('fbjs/lib/emptyObject'); var invariant = require('fbjs/lib/invariant'); var shouldUpdateReactComponent = require('./shouldUpdateReactComponent'); var warning = require('fbjs/lib/warning'); function getDeclarationErrorAddendum(component) { var owner = component._currentElement._owner || null; if (owner) { var name = owner.getName(); if (name) { return ' Check the render method of `' + name + '`.'; } } return ''; } function StatelessComponent(Component) {} StatelessComponent.prototype.render = function () { var Component = ReactInstanceMap.get(this)._currentElement.type; return Component(this.props, this.context, this.updater); }; /** * ------------------ The Life-Cycle of a Composite Component ------------------ * * - constructor: Initialization of state. The instance is now retained. * - componentWillMount * - render * - [children's constructors] * - [children's componentWillMount and render] * - [children's componentDidMount] * - componentDidMount * * Update Phases: * - componentWillReceiveProps (only called if parent updated) * - shouldComponentUpdate * - componentWillUpdate * - render * - [children's constructors or receive props phases] * - componentDidUpdate * * - componentWillUnmount * - [children's componentWillUnmount] * - [children destroyed] * - (destroyed): The instance is now blank, released by React and ready for GC. * * ----------------------------------------------------------------------------- */ /** * An incrementing ID assigned to each component when it is mounted. This is * used to enforce the order in which `ReactUpdates` updates dirty components. * * @private */ var nextMountID = 1; /** * @lends {ReactCompositeComponent.prototype} */ var ReactCompositeComponentMixin = { /** * Base constructor for all composite component. * * @param {ReactElement} element * @final * @internal */ construct: function (element) { this._currentElement = element; this._rootNodeID = null; this._instance = null; // See ReactUpdateQueue this._pendingElement = null; this._pendingStateQueue = null; this._pendingReplaceState = false; this._pendingForceUpdate = false; this._renderedComponent = null; this._context = null; this._mountOrder = 0; this._topLevelWrapper = null; // See ReactUpdates and ReactUpdateQueue. this._pendingCallbacks = null; }, /** * Initializes the component, renders markup, and registers event listeners. * * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction * @return {?string} Rendered markup to be inserted into the DOM. * @final * @internal */ mountComponent: function (rootID, transaction, context) { this._context = context; this._mountOrder = nextMountID++; this._rootNodeID = rootID; var publicProps = this._processProps(this._currentElement.props); var publicContext = this._processContext(context); var Component = this._currentElement.type; // Initialize the public class var inst; var renderedElement; // This is a way to detect if Component is a stateless arrow function // component, which is not newable. It might not be 100% reliable but is // something we can do until we start detecting that Component extends // React.Component. We already assume that typeof Component === 'function'. var canInstantiate = ('prototype' in Component); if (canInstantiate) { if (process.env.NODE_ENV !== 'production') { ReactCurrentOwner.current = this; try { inst = new Component(publicProps, publicContext, ReactUpdateQueue); } finally { ReactCurrentOwner.current = null; } } else { inst = new Component(publicProps, publicContext, ReactUpdateQueue); } } if (!canInstantiate || inst === null || inst === false || ReactElement.isValidElement(inst)) { renderedElement = inst; inst = new StatelessComponent(Component); } if (process.env.NODE_ENV !== 'production') { // This will throw later in _renderValidatedComponent, but add an early // warning now to help debugging if (inst.render == null) { process.env.NODE_ENV !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`, returned ' + 'null/false from a stateless component, or tried to render an ' + 'element whose type is a function that isn\'t a React component.', Component.displayName || Component.name || 'Component') : undefined; } else { // We support ES6 inheriting from React.Component, the module pattern, // and stateless components, but not ES6 classes that don't extend process.env.NODE_ENV !== 'production' ? warning(Component.prototype && Component.prototype.isReactComponent || !canInstantiate || !(inst instanceof Component), '%s(...): React component classes must extend React.Component.', Component.displayName || Component.name || 'Component') : undefined; } } // These should be set up in the constructor, but as a convenience for // simpler class abstractions, we set them up after the fact. inst.props = publicProps; inst.context = publicContext; inst.refs = emptyObject; inst.updater = ReactUpdateQueue; this._instance = inst; // Store a reference from the instance back to the internal representation ReactInstanceMap.set(inst, this); if (process.env.NODE_ENV !== 'production') { // Since plain JS classes are defined without any special initialization // logic, we can not catch common errors early. Therefore, we have to // catch them here, at initialization time, instead. process.env.NODE_ENV !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : undefined; process.env.NODE_ENV !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : undefined; process.env.NODE_ENV !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : undefined; process.env.NODE_ENV !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : undefined; process.env.NODE_ENV !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : undefined; process.env.NODE_ENV !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : undefined; process.env.NODE_ENV !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : undefined; } var initialState = inst.state; if (initialState === undefined) { inst.state = initialState = null; } !(typeof initialState === 'object' && !Array.isArray(initialState)) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined; this._pendingStateQueue = null; this._pendingReplaceState = false; this._pendingForceUpdate = false; if (inst.componentWillMount) { inst.componentWillMount(); // When mounting, calls to `setState` by `componentWillMount` will set // `this._pendingStateQueue` without triggering a re-render. if (this._pendingStateQueue) { inst.state = this._processPendingState(inst.props, inst.context); } } // If not a stateless component, we now render if (renderedElement === undefined) { renderedElement = this._renderValidatedComponent(); } this._renderedComponent = this._instantiateReactComponent(renderedElement); var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context)); if (inst.componentDidMount) { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } return markup; }, /** * Releases any resources allocated by `mountComponent`. * * @final * @internal */ unmountComponent: function () { var inst = this._instance; if (inst.componentWillUnmount) { inst.componentWillUnmount(); } ReactReconciler.unmountComponent(this._renderedComponent); this._renderedComponent = null; this._instance = null; // Reset pending fields // Even if this component is scheduled for another update in ReactUpdates, // it would still be ignored because these fields are reset. this._pendingStateQueue = null; this._pendingReplaceState = false; this._pendingForceUpdate = false; this._pendingCallbacks = null; this._pendingElement = null; // These fields do not really need to be reset since this object is no // longer accessible. this._context = null; this._rootNodeID = null; this._topLevelWrapper = null; // Delete the reference from the instance to this internal representation // which allow the internals to be properly cleaned up even if the user // leaks a reference to the public instance. ReactInstanceMap.remove(inst); // Some existing components rely on inst.props even after they've been // destroyed (in event handlers). // TODO: inst.props = null; // TODO: inst.state = null; // TODO: inst.context = null; }, /** * Filters the context object to only contain keys specified in * `contextTypes` * * @param {object} context * @return {?object} * @private */ _maskContext: function (context) { var maskedContext = null; var Component = this._currentElement.type; var contextTypes = Component.contextTypes; if (!contextTypes) { return emptyObject; } maskedContext = {}; for (var contextName in contextTypes) { maskedContext[contextName] = context[contextName]; } return maskedContext; }, /** * Filters the context object to only contain keys specified in * `contextTypes`, and asserts that they are valid. * * @param {object} context * @return {?object} * @private */ _processContext: function (context) { var maskedContext = this._maskContext(context); if (process.env.NODE_ENV !== 'production') { var Component = this._currentElement.type; if (Component.contextTypes) { this._checkPropTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context); } } return maskedContext; }, /** * @param {object} currentContext * @return {object} * @private */ _processChildContext: function (currentContext) { var Component = this._currentElement.type; var inst = this._instance; var childContext = inst.getChildContext && inst.getChildContext(); if (childContext) { !(typeof Component.childContextTypes === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined; if (process.env.NODE_ENV !== 'production') { this._checkPropTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext); } for (var name in childContext) { !(name in Component.childContextTypes) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : invariant(false) : undefined; } return assign({}, currentContext, childContext); } return currentContext; }, /** * Processes props by setting default values for unspecified props and * asserting that the props are valid. Does not mutate its argument; returns * a new props object with defaults merged in. * * @param {object} newProps * @return {object} * @private */ _processProps: function (newProps) { if (process.env.NODE_ENV !== 'production') { var Component = this._currentElement.type; if (Component.propTypes) { this._checkPropTypes(Component.propTypes, newProps, ReactPropTypeLocations.prop); } } return newProps; }, /** * Assert that the props are valid * * @param {object} propTypes Map of prop name to a ReactPropType * @param {object} props * @param {string} location e.g. "prop", "context", "child context" * @private */ _checkPropTypes: function (propTypes, props, location) { // TODO: Stop validating prop types here and only use the element // validation. var componentName = this.getName(); for (var propName in propTypes) { if (propTypes.hasOwnProperty(propName)) { var error; try { // This is intentionally an invariant that gets caught. It's the same // behavior as without this statement except with a better message. !(typeof propTypes[propName] === 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s: %s type `%s` is invalid; it must be a function, usually ' + 'from React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName) : invariant(false) : undefined; error = propTypes[propName](props, propName, componentName, location); } catch (ex) { error = ex; } if (error instanceof Error) { // We may want to extend this logic for similar errors in // top-level render calls, so I'm abstracting it away into // a function to minimize refactoring in the future var addendum = getDeclarationErrorAddendum(this); if (location === ReactPropTypeLocations.prop) { // Preface gives us something to blacklist in warning module process.env.NODE_ENV !== 'production' ? warning(false, 'Failed Composite propType: %s%s', error.message, addendum) : undefined; } else { process.env.NODE_ENV !== 'production' ? warning(false, 'Failed Context Types: %s%s', error.message, addendum) : undefined; } } } } }, receiveComponent: function (nextElement, transaction, nextContext) { var prevElement = this._currentElement; var prevContext = this._context; this._pendingElement = null; this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext); }, /** * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` * is set, update the component. * * @param {ReactReconcileTransaction} transaction * @internal */ performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context); } if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } }, /** * Perform an update to a mounted component. The componentWillReceiveProps and * shouldComponentUpdate methods are called, then (assuming the update isn't * skipped) the remaining update lifecycle methods are called and the DOM * representation is updated. * * By default, this implements React's rendering and reconciliation algorithm. * Sophisticated clients may wish to override this. * * @param {ReactReconcileTransaction} transaction * @param {ReactElement} prevParentElement * @param {ReactElement} nextParentElement * @internal * @overridable */ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { var inst = this._instance; var nextContext = this._context === nextUnmaskedContext ? inst.context : this._processContext(nextUnmaskedContext); var nextProps; // Distinguish between a props update versus a simple state update if (prevParentElement === nextParentElement) { // Skip checking prop types again -- we don't read inst.props to avoid // warning for DOM component props in this upgrade nextProps = nextParentElement.props; } else { nextProps = this._processProps(nextParentElement.props); // An update here will schedule an update but immediately set // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } } var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext); if (process.env.NODE_ENV !== 'production') { process.env.NODE_ENV !== 'production' ? warning(typeof shouldUpdate !== 'undefined', '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : undefined; } if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext); } else { // If it's determined that a component should not update, we still want // to set props and state but we shortcut the rest of the update. this._currentElement = nextParentElement; this._context = nextUnmaskedContext; inst.props = nextProps; inst.state = nextState; inst.context = nextContext; } }, _processPendingState: function (props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; this._pendingReplaceState = false; this._pendingStateQueue = null; if (!queue) { return inst.state; } if (replace && queue.length === 1) { return queue[0]; } var nextState = assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i < queue.length; i++) { var partial = queue[i]; assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); } return nextState; }, /** * Merges new props and state, notifies delegate methods of update and * performs update. * * @param {ReactElement} nextElement Next element * @param {object} nextProps Next public object to set as properties. * @param {?object} nextState Next object to set as state. * @param {?object} nextContext Next public object to set as context. * @param {ReactReconcileTransaction} transaction * @param {?object} unmaskedContext * @private */ _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) { var inst = this._instance; var hasComponentDidUpdate = Boolean(inst.componentDidUpdate); var prevProps; var prevState; var prevContext; if (hasComponentDidUpdate) { prevProps = inst.props; prevState = inst.state; prevContext = inst.context; } if (inst.componentWillUpdate) { inst.componentWillUpdate(nextProps, nextState, nextContext); } this._currentElement = nextElement; this._context = unmaskedContext; inst.props = nextProps; inst.state = nextState; inst.context = nextContext; this._updateRenderedComponent(transaction, unmaskedContext); if (hasComponentDidUpdate) { transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst); } }, /** * Call the component's `render` method and update the DOM accordingly. * * @param {ReactReconcileTransaction} transaction * @internal */ _updateRenderedComponent: function (transaction, context) { var prevComponentInstance = this._renderedComponent; var prevRenderedElement = prevComponentInstance._currentElement; var nextRenderedElement = this._renderValidatedComponent(); if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context)); } else { // These two IDs are actually the same! But nothing should rely on that. var thisID = this._rootNodeID; var prevComponentID = prevComponentInstance._rootNodeID; ReactReconciler.unmountComponent(prevComponentInstance); this._renderedComponent = this._instantiateReactComponent(nextRenderedElement); var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, thisID, transaction, this._processChildContext(context)); this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup); } }, /** * @protected */ _replaceNodeWithMarkupByID: function (prevComponentID, nextMarkup) { ReactComponentEnvironment.replaceNodeWithMarkupByID(prevComponentID, nextMarkup); }, /** * @protected */ _renderValidatedComponentWithoutOwnerOrContext: function () { var inst = this._instance; var renderedComponent = inst.render(); if (process.env.NODE_ENV !== 'production') { // We allow auto-mocks to proceed as if they're returning null. if (typeof renderedComponent === 'undefined' && inst.render._isMockFunction) { // This is probably bad practice. Consider warning here and // deprecating this convenience. renderedComponent = null; } } return renderedComponent; }, /** * @private */ _renderValidatedComponent: function () { var renderedComponent; ReactCurrentOwner.current = this; try { renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext(); } finally { ReactCurrentOwner.current = null; } !( // TODO: An `isValidNode` function would probably be more appropriate renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.render(): A valid ReactComponent must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : invariant(false) : undefined; return renderedComponent; }, /** * Lazily allocates the refs object and stores `component` as `ref`. * * @param {string} ref Reference name. * @param {component} component Component to store as `ref`. * @final * @private */ attachRef: function (ref, component) { var inst = this.getPublicInstance(); !(inst != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : invariant(false) : undefined; var publicComponentInstance = component.getPublicInstance(); if (process.env.NODE_ENV !== 'production') { var componentName = component && component.getName ? component.getName() : 'a component'; process.env.NODE_ENV !== 'production' ? warning(publicComponentInstance != null, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : undefined; } var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs; refs[ref] = publicComponentInstance; }, /** * Detaches a reference name. * * @param {string} ref Name to dereference. * @final * @private */ detachRef: function (ref) { var refs = this.getPublicInstance().refs; delete refs[ref]; }, /** * Get a text description of the component that can be used to identify it * in error messages. * @return {string} The name or null. * @internal */ getName: function () { var type = this._currentElement.type; var constructor = this._instance && this._instance.constructor; return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null; }, /** * Get the publicly accessible representation of this component - i.e. what * is exposed by refs and returned by render. Can be null for stateless * components. * * @return {ReactComponent} the public component instance. * @internal */ getPublicInstance: function () { var inst = this._instance; if (inst instanceof StatelessComponent) { return null; } return inst; }, // Stub _instantiateReactComponent: null }; ReactPerf.measureMethods(ReactCompositeComponentMixin, 'ReactCompositeComponent', { mountComponent: 'mountComponent', updateComponent: 'updateComponent', _renderValidatedComponent: '_renderValidatedComponent' }); var ReactCompositeComponent = { Mixin: ReactCompositeComponentMixin }; module.exports = ReactCompositeComponent;