From 3aaccd2dc96cf229c0170d538fe81f7d22059a4e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 2 Oct 2014 23:05:11 -0700 Subject: [PATCH] Use strings as the type for DOM elements This makes ReactDOM a simple helper for creating ReactElements with the string tag as the type. The actual class is internal and created by instantiateReactComponent. Configurable using injection. There's not a separate class for each tag. There's just a generic ReactDOMComponent which could take any tag name. Invididual tags can be wrapped. When wrapping happens you can return the same tag again. If the wrapper returns the same string, then we fall back to the generic component. This avoids recursion in a single level wrapper. --- src/browser/ReactDOM.js | 293 ++++++++---------- src/browser/server/ReactServerRendering.js | 4 +- src/browser/ui/ReactDOMComponent.js | 44 ++- src/browser/ui/ReactDefaultInjection.js | 32 +- src/browser/ui/ReactInjection.js | 4 +- src/browser/ui/ReactMount.js | 2 +- .../dom/components/createFullPageComponent.js | 10 +- src/core/ReactCompositeComponent.js | 8 +- src/core/ReactDescriptor.js | 11 +- src/core/ReactEmptyComponent.js | 2 +- src/core/ReactMultiChild.js | 7 +- src/core/ReactNativeComponent.js | 76 +++++ src/core/ReactPropTransferer.js | 2 + src/core/__tests__/ReactDescriptor-test.js | 6 + src/core/instantiateReactComponent.js | 40 ++- src/test/reactComponentExpect.js | 4 +- 16 files changed, 329 insertions(+), 216 deletions(-) create mode 100644 src/core/ReactNativeComponent.js diff --git a/src/browser/ReactDOM.js b/src/browser/ReactDOM.js index c6158c19b4a..8d3bc87df8e 100644 --- a/src/browser/ReactDOM.js +++ b/src/browser/ReactDOM.js @@ -22,41 +22,23 @@ var ReactDescriptor = require('ReactDescriptor'); var ReactDescriptorValidator = require('ReactDescriptorValidator'); var ReactLegacyDescriptor = require('ReactLegacyDescriptor'); -var ReactDOMComponent = require('ReactDOMComponent'); -var mergeInto = require('mergeInto'); var mapObject = require('mapObject'); /** - * Creates a new React class that is idempotent and capable of containing other - * React components. It accepts event listeners and DOM properties that are - * valid according to `DOMProperty`. + * Create a factory that creates HTML tag descriptors. * - * - Event listeners: `onClick`, `onMouseDown`, etc. - * - DOM properties: `className`, `name`, `title`, etc. - * - * The `style` property functions differently from the DOM API. It accepts an - * object mapping of style properties to values. - * - * @param {boolean} omitClose True if the close tag should be omitted. * @param {string} tag Tag name (e.g. `div`). * @private */ -function createDOMComponentClass(omitClose, tag) { - var Constructor = function(props) { - // This constructor and it's argument is currently used by mocks. - }; - Constructor.prototype = new ReactDOMComponent(tag, omitClose); - Constructor.prototype.constructor = Constructor; - Constructor.displayName = tag; - +function createDOMFactory(tag) { if (__DEV__) { return ReactLegacyDescriptor.wrapFactory( - ReactDescriptorValidator.createFactory(Constructor) + ReactDescriptorValidator.createFactory(tag) ); } return ReactLegacyDescriptor.wrapFactory( - ReactDescriptor.createFactory(Constructor) + ReactDescriptor.createFactory(tag) ); } @@ -67,145 +49,138 @@ function createDOMComponentClass(omitClose, tag) { * @public */ var ReactDOM = mapObject({ - a: false, - abbr: false, - address: false, - area: true, - article: false, - aside: false, - audio: false, - b: false, - base: true, - bdi: false, - bdo: false, - big: false, - blockquote: false, - body: false, - br: true, - button: false, - canvas: false, - caption: false, - cite: false, - code: false, - col: true, - colgroup: false, - data: false, - datalist: false, - dd: false, - del: false, - details: false, - dfn: false, - dialog: false, - div: false, - dl: false, - dt: false, - em: false, - embed: true, - fieldset: false, - figcaption: false, - figure: false, - footer: false, - form: false, // NOTE: Injected, see `ReactDOMForm`. - h1: false, - h2: false, - h3: false, - h4: false, - h5: false, - h6: false, - head: false, - header: false, - hr: true, - html: false, - i: false, - iframe: false, - img: true, - input: true, - ins: false, - kbd: false, - keygen: true, - label: false, - legend: false, - li: false, - link: true, - main: false, - map: false, - mark: false, - menu: false, - menuitem: false, // NOTE: Close tag should be omitted, but causes problems. - meta: true, - meter: false, - nav: false, - noscript: false, - object: false, - ol: false, - optgroup: false, - option: false, - output: false, - p: false, - param: true, - picture: false, - pre: false, - progress: false, - q: false, - rp: false, - rt: false, - ruby: false, - s: false, - samp: false, - script: false, - section: false, - select: false, - small: false, - source: true, - span: false, - strong: false, - style: false, - sub: false, - summary: false, - sup: false, - table: false, - tbody: false, - td: false, - textarea: false, // NOTE: Injected, see `ReactDOMTextarea`. - tfoot: false, - th: false, - thead: false, - time: false, - title: false, - tr: false, - track: true, - u: false, - ul: false, - 'var': false, - video: false, - wbr: true, + a: 'a', + abbr: 'abbr', + address: 'address', + area: 'area', + article: 'article', + aside: 'aside', + audio: 'audio', + b: 'b', + base: 'base', + bdi: 'bdi', + bdo: 'bdo', + big: 'big', + blockquote: 'blockquote', + body: 'body', + br: 'br', + button: 'button', + canvas: 'canvas', + caption: 'caption', + cite: 'cite', + code: 'code', + col: 'col', + colgroup: 'colgroup', + data: 'data', + datalist: 'datalist', + dd: 'dd', + del: 'del', + details: 'details', + dfn: 'dfn', + dialog: 'dialog', + div: 'div', + dl: 'dl', + dt: 'dt', + em: 'em', + embed: 'embed', + fieldset: 'fieldset', + figcaption: 'figcaption', + figure: 'figure', + footer: 'footer', + form: 'form', + h1: 'h1', + h2: 'h2', + h3: 'h3', + h4: 'h4', + h5: 'h5', + h6: 'h6', + head: 'head', + header: 'header', + hr: 'hr', + html: 'html', + i: 'i', + iframe: 'iframe', + img: 'img', + input: 'input', + ins: 'ins', + kbd: 'kbd', + keygen: 'keygen', + label: 'label', + legend: 'legend', + li: 'li', + link: 'link', + main: 'main', + map: 'map', + mark: 'mark', + menu: 'menu', + menuitem: 'menuitem', + meta: 'meta', + meter: 'meter', + nav: 'nav', + noscript: 'noscript', + object: 'object', + ol: 'ol', + optgroup: 'optgroup', + option: 'option', + output: 'output', + p: 'p', + param: 'param', + picture: 'picture', + pre: 'pre', + progress: 'progress', + q: 'q', + rp: 'rp', + rt: 'rt', + ruby: 'ruby', + s: 's', + samp: 'samp', + script: 'script', + section: 'section', + select: 'select', + small: 'small', + source: 'source', + span: 'span', + strong: 'strong', + style: 'style', + sub: 'sub', + summary: 'summary', + sup: 'sup', + table: 'table', + tbody: 'tbody', + td: 'td', + textarea: 'textarea', + tfoot: 'tfoot', + th: 'th', + thead: 'thead', + time: 'time', + title: 'title', + tr: 'tr', + track: 'track', + u: 'u', + ul: 'ul', + 'var': 'var', + video: 'video', + wbr: 'wbr', // SVG - circle: false, - defs: false, - ellipse: false, - g: false, - line: false, - linearGradient: false, - mask: false, - path: false, - pattern: false, - polygon: false, - polyline: false, - radialGradient: false, - rect: false, - stop: false, - svg: false, - text: false, - tspan: false -}, createDOMComponentClass); - -var injection = { - injectComponentClasses: function(componentClasses) { - mergeInto(ReactDOM, componentClasses); - } -}; + circle: 'circle', + defs: 'defs', + ellipse: 'ellipse', + g: 'g', + line: 'line', + linearGradient: 'linearGradient', + mask: 'mask', + path: 'path', + pattern: 'pattern', + polygon: 'polygon', + polyline: 'polyline', + radialGradient: 'radialGradient', + rect: 'rect', + stop: 'stop', + svg: 'svg', + text: 'text', + tspan: 'tspan' -ReactDOM.injection = injection; +}, createDOMFactory); module.exports = ReactDOM; diff --git a/src/browser/server/ReactServerRendering.js b/src/browser/server/ReactServerRendering.js index cf61cc4aaab..d0beb51d41d 100644 --- a/src/browser/server/ReactServerRendering.js +++ b/src/browser/server/ReactServerRendering.js @@ -49,7 +49,7 @@ function renderComponentToString(component) { transaction = ReactServerRenderingTransaction.getPooled(false); return transaction.perform(function() { - var componentInstance = instantiateReactComponent(component); + var componentInstance = instantiateReactComponent(component, null); var markup = componentInstance.mountComponent(id, transaction, 0); return ReactMarkupChecksum.addChecksumToMarkup(markup); }, null); @@ -75,7 +75,7 @@ function renderComponentToStaticMarkup(component) { transaction = ReactServerRenderingTransaction.getPooled(true); return transaction.perform(function() { - var componentInstance = instantiateReactComponent(component); + var componentInstance = instantiateReactComponent(component, null); return componentInstance.mountComponent(id, transaction, 0); }, null); } finally { diff --git a/src/browser/ui/ReactDOMComponent.js b/src/browser/ui/ReactDOMComponent.js index 9b528def272..3436e4c9791 100644 --- a/src/browser/ui/ReactDOMComponent.js +++ b/src/browser/ui/ReactDOMComponent.js @@ -101,18 +101,51 @@ function putListener(id, registrationName, listener, transaction) { ); } +// For HTML, certain tags should omit their close tag. We keep a whitelist for +// those special cased tags. + +var omittedCloseTags = { + 'area': true, + 'base': true, + 'br': true, + 'col': true, + 'embed': true, + 'hr': true, + 'img': true, + 'input': true, + 'keygen': true, + 'link': true, + 'meta': true, + 'param': true, + 'source': true, + 'track': true, + 'wbr': true + // NOTE: menuitem's close tag should be omitted, but that causes problems. +}; /** + * Creates a new React class that is idempotent and capable of containing other + * React components. It accepts event listeners and DOM properties that are + * valid according to `DOMProperty`. + * + * - Event listeners: `onClick`, `onMouseDown`, etc. + * - DOM properties: `className`, `name`, `title`, etc. + * + * The `style` property functions differently from the DOM API. It accepts an + * object mapping of style properties to values. + * * @constructor ReactDOMComponent * @extends ReactComponent * @extends ReactMultiChild */ -function ReactDOMComponent(tag, omitClose) { - this._tagOpen = '<' + tag; - this._tagClose = omitClose ? '' : ''; +function ReactDOMComponent(tag) { + // TODO: DANGEROUS this tag should be sanitized. + this._tag = tag; this.tagName = tag.toUpperCase(); } +ReactDOMComponent.displayName = 'ReactDOMComponent'; + ReactDOMComponent.Mixin = { /** @@ -136,10 +169,11 @@ ReactDOMComponent.Mixin = { mountDepth ); assertValidProps(this.props); + var closeTag = omittedCloseTags[this._tag] ? '' : ''; return ( this._createOpenTagMarkupAndPutListeners(transaction) + this._createContentMarkup(transaction) + - this._tagClose + closeTag ); } ), @@ -158,7 +192,7 @@ ReactDOMComponent.Mixin = { */ _createOpenTagMarkupAndPutListeners: function(transaction) { var props = this.props; - var ret = this._tagOpen; + var ret = '<' + this._tag; for (var propKey in props) { if (!props.hasOwnProperty(propKey)) { diff --git a/src/browser/ui/ReactDefaultInjection.js b/src/browser/ui/ReactDefaultInjection.js index 2bac15de63e..21eb9d948e9 100644 --- a/src/browser/ui/ReactDefaultInjection.js +++ b/src/browser/ui/ReactDefaultInjection.js @@ -31,7 +31,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); -var ReactDOM = require('ReactDOM'); +var ReactDOMComponent = require('ReactDOMComponent'); var ReactDOMButton = require('ReactDOMButton'); var ReactDOMForm = require('ReactDOMForm'); var ReactDOMImg = require('ReactDOMImg'); @@ -76,18 +76,22 @@ function inject() { BeforeInputEventPlugin: BeforeInputEventPlugin }); - ReactInjection.DOM.injectComponentClasses({ - button: ReactDOMButton, - form: ReactDOMForm, - img: ReactDOMImg, - input: ReactDOMInput, - option: ReactDOMOption, - select: ReactDOMSelect, - textarea: ReactDOMTextarea, - - html: createFullPageComponent(ReactDOM.html), - head: createFullPageComponent(ReactDOM.head), - body: createFullPageComponent(ReactDOM.body) + ReactInjection.NativeComponent.injectGenericComponentClass( + ReactDOMComponent + ); + + ReactInjection.NativeComponent.injectComponentClasses({ + 'button': ReactDOMButton, + 'form': ReactDOMForm, + 'img': ReactDOMImg, + 'input': ReactDOMInput, + 'option': ReactDOMOption, + 'select': ReactDOMSelect, + 'textarea': ReactDOMTextarea, + + 'html': createFullPageComponent('html'), + 'head': createFullPageComponent('head'), + 'body': createFullPageComponent('body') }); // This needs to happen after createFullPageComponent() otherwise the mixin @@ -97,7 +101,7 @@ function inject() { ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig); ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig); - ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.noscript); + ReactInjection.EmptyComponent.injectEmptyComponent('noscript'); ReactInjection.Updates.injectReconcileTransaction( ReactComponentBrowserEnvironment.ReactReconcileTransaction diff --git a/src/browser/ui/ReactInjection.js b/src/browser/ui/ReactInjection.js index b1132388dda..9545c039e0a 100644 --- a/src/browser/ui/ReactInjection.js +++ b/src/browser/ui/ReactInjection.js @@ -22,9 +22,9 @@ var DOMProperty = require('DOMProperty'); var EventPluginHub = require('EventPluginHub'); var ReactComponent = require('ReactComponent'); var ReactCompositeComponent = require('ReactCompositeComponent'); -var ReactDOM = require('ReactDOM'); var ReactEmptyComponent = require('ReactEmptyComponent'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); +var ReactNativeComponent = require('ReactNativeComponent'); var ReactPerf = require('ReactPerf'); var ReactRootIndex = require('ReactRootIndex'); var ReactUpdates = require('ReactUpdates'); @@ -35,8 +35,8 @@ var ReactInjection = { DOMProperty: DOMProperty.injection, EmptyComponent: ReactEmptyComponent.injection, EventPluginHub: EventPluginHub.injection, - DOM: ReactDOM.injection, EventEmitter: ReactBrowserEventEmitter.injection, + NativeComponent: ReactNativeComponent.injection, Perf: ReactPerf.injection, RootIndex: ReactRootIndex.injection, Updates: ReactUpdates.injection diff --git a/src/browser/ui/ReactMount.js b/src/browser/ui/ReactMount.js index 65eb9902cc9..3dc095b6aab 100644 --- a/src/browser/ui/ReactMount.js +++ b/src/browser/ui/ReactMount.js @@ -307,7 +307,7 @@ var ReactMount = { 'componentDidUpdate.' ); - var componentInstance = instantiateReactComponent(nextComponent); + var componentInstance = instantiateReactComponent(nextComponent, null); var reactRootID = ReactMount._registerComponent( componentInstance, container diff --git a/src/browser/ui/dom/components/createFullPageComponent.js b/src/browser/ui/dom/components/createFullPageComponent.js index 4f8beafd288..4568db3b81f 100644 --- a/src/browser/ui/dom/components/createFullPageComponent.js +++ b/src/browser/ui/dom/components/createFullPageComponent.js @@ -33,16 +33,14 @@ var invariant = require('invariant'); * take advantage of React's reconciliation for styling and * management. So we just document it and throw in dangerous cases. * - * @param {function} componentClass convenience constructor to wrap + * @param {string} tag The tag to wrap * @return {function} convenience constructor of new component */ -function createFullPageComponent(componentClass) { - var elementFactory = ReactDescriptor.createFactory(componentClass.type); +function createFullPageComponent(tag) { + var elementFactory = ReactDescriptor.createFactory(tag); var FullPageComponent = ReactCompositeComponent.createClass({ - displayName: 'ReactFullPageComponent' + ( - componentClass.type.displayName || '' - ), + displayName: 'ReactFullPageComponent' + tag, componentWillUnmount: function() { invariant( diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index bc71b84b1ab..7d792619525 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -793,7 +793,8 @@ var ReactCompositeComponentMixin = { } this._renderedComponent = instantiateReactComponent( - this._renderValidatedComponent() + this._renderValidatedComponent(), + this._descriptor.type // The wrapping type ); // Done with mounting, `setState` will now trigger UI changes. @@ -1185,7 +1186,10 @@ var ReactCompositeComponentMixin = { var thisID = this._rootNodeID; var prevComponentID = prevComponentInstance._rootNodeID; prevComponentInstance.unmountComponent(); - this._renderedComponent = instantiateReactComponent(nextDescriptor); + this._renderedComponent = instantiateReactComponent( + nextDescriptor, + this._descriptor.type + ); var nextMarkup = this._renderedComponent.mountComponent( thisID, transaction, diff --git a/src/core/ReactDescriptor.js b/src/core/ReactDescriptor.js index 1818736460f..784e32cfad3 100644 --- a/src/core/ReactDescriptor.js +++ b/src/core/ReactDescriptor.js @@ -222,10 +222,13 @@ ReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) { * @public */ ReactDescriptor.isValidFactory = function(factory) { - return typeof factory === 'function' && - typeof factory.type === 'function' && - typeof factory.type.prototype.mountComponent === 'function' && - typeof factory.type.prototype.receiveComponent === 'function'; + return typeof factory === 'function' && ( + typeof factory.type === 'string' || ( + typeof factory.type === 'function' && + typeof factory.type.prototype.mountComponent === 'function' && + typeof factory.type.prototype.receiveComponent === 'function' + ) + ); }; /** diff --git a/src/core/ReactEmptyComponent.js b/src/core/ReactEmptyComponent.js index 64a8c22807d..0d2414ac2ec 100644 --- a/src/core/ReactEmptyComponent.js +++ b/src/core/ReactEmptyComponent.js @@ -29,7 +29,7 @@ var nullComponentIdsRegistry = {}; var ReactEmptyComponentInjection = { injectEmptyComponent: function(emptyComponent) { - component = ReactDescriptor.createFactory(emptyComponent.type); + component = ReactDescriptor.createFactory(emptyComponent); } }; diff --git a/src/core/ReactMultiChild.js b/src/core/ReactMultiChild.js index 608fe62d846..782338a852e 100644 --- a/src/core/ReactMultiChild.js +++ b/src/core/ReactMultiChild.js @@ -195,7 +195,7 @@ var ReactMultiChild = { if (children.hasOwnProperty(name)) { // The rendered children must be turned into instances as they're // mounted. - var childInstance = instantiateReactComponent(child); + var childInstance = instantiateReactComponent(child, null); children[name] = childInstance; // Inlined for performance, see `ReactInstanceHandles.createReactID`. var rootID = this._rootNodeID + name; @@ -300,7 +300,10 @@ var ReactMultiChild = { this._unmountChildByName(prevChild, name); } // The child must be instantiated before it's mounted. - var nextChildInstance = instantiateReactComponent(nextDescriptor); + var nextChildInstance = instantiateReactComponent( + nextDescriptor, + null + ); this._mountChildByNameAtIndex( nextChildInstance, name, nextIndex, transaction ); diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js new file mode 100644 index 00000000000..0735e2050a1 --- /dev/null +++ b/src/core/ReactNativeComponent.js @@ -0,0 +1,76 @@ +/** + * Copyright 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 ReactNativeComponent + */ + +"use strict"; + +var invariant = require('invariant'); +var mergeInto = require('mergeInto'); + +var genericComponentClass = null; +// This registry keeps track of wrapper classes around native tags +var tagToComponentClass = {}; + +var ReactNativeComponentInjection = { + // This accepts a class that receives the tag string. This is a catch all + // that can render any kind of tag. + injectGenericComponentClass: function(componentClass) { + genericComponentClass = componentClass; + }, + // This accepts a keyed object with classes as values. Each key represents a + // tag. That particular tag will use this class instead of the generic one. + injectComponentClasses: function(componentClasses) { + mergeInto(tagToComponentClass, componentClasses); + } +}; + +/** + * Create an internal class for a specific tag. + * + * @param {string} tag The tag for which to create an internal instance. + * @param {any} props The props passed to the instance constructor. + * @return {ReactComponent} component The injected empty component. + */ +function createInstanceForTag(tag, props, parentType) { + var componentClass = tagToComponentClass[tag]; + if (componentClass == null) { + invariant( + genericComponentClass, + 'There is no registered component for the tag %s', + tag + ); + return new genericComponentClass(tag, props); + } + if (parentType === tag) { + // Avoid recursion + invariant( + genericComponentClass, + 'There is no registered component for the tag %s', + tag + ); + return new genericComponentClass(tag, props); + } + // Unwrap legacy factories + return new componentClass.type(props); +} + +var ReactNativeComponent = { + createInstanceForTag: createInstanceForTag, + injection: ReactNativeComponentInjection, +}; + +module.exports = ReactNativeComponent; diff --git a/src/core/ReactPropTransferer.js b/src/core/ReactPropTransferer.js index a4f1ec5d94b..d9bb05ff369 100644 --- a/src/core/ReactPropTransferer.js +++ b/src/core/ReactPropTransferer.js @@ -141,6 +141,8 @@ var ReactPropTransferer = { 'don\'t own, %s. This usually means you are calling ' + 'transferPropsTo() on a component passed in as props or children.', this.constructor.displayName, + typeof descriptor.type === 'string' ? + descriptor.type : descriptor.type.displayName ); diff --git a/src/core/__tests__/ReactDescriptor-test.js b/src/core/__tests__/ReactDescriptor-test.js index 782ee91e3a6..f963c6029b1 100644 --- a/src/core/__tests__/ReactDescriptor-test.js +++ b/src/core/__tests__/ReactDescriptor-test.js @@ -297,4 +297,10 @@ describe('ReactDescriptor', function() { expect(typeof Component.specialType.isRequired).toBe("function"); }); + it('allows a DOM descriptor to be used with a string', function() { + var descriptor = React.createDescriptor('div', { className: 'foo' }); + var instance = ReactTestUtils.renderIntoDocument(descriptor); + expect(instance.getDOMNode().tagName).toBe('DIV'); + }); + }); diff --git a/src/core/instantiateReactComponent.js b/src/core/instantiateReactComponent.js index 2460c138a7b..086035393e1 100644 --- a/src/core/instantiateReactComponent.js +++ b/src/core/instantiateReactComponent.js @@ -23,6 +23,7 @@ var warning = require('warning'); var ReactDescriptor = require('ReactDescriptor'); var ReactLegacyDescriptor = require('ReactLegacyDescriptor'); +var ReactNativeComponent = require('ReactNativeComponent'); var ReactEmptyComponent = require('ReactEmptyComponent'); /** @@ -30,10 +31,11 @@ var ReactEmptyComponent = require('ReactEmptyComponent'); * mounted. * * @param {object} descriptor - * @return {object} A new instance of componentDescriptor's constructor. + * @param {*} parentCompositeType The composite type that resolved this. + * @return {object} A new instance of the descriptor's constructor. * @protected */ -function instantiateReactComponent(descriptor) { +function instantiateReactComponent(descriptor, parentCompositeType) { var instance; if (__DEV__) { @@ -41,8 +43,6 @@ function instantiateReactComponent(descriptor) { descriptor && (typeof descriptor.type === 'function' || typeof descriptor.type === 'string'), 'Only functions or strings can be mounted as React components.' - // Not really strings yet, but as soon as I solve the cyclic dep, they - // will be allowed here. ); // Resolve mock instances @@ -72,24 +72,32 @@ function instantiateReactComponent(descriptor) { // there is no render function on the instance. We replace the whole // component with an empty component instance instead. descriptor = ReactEmptyComponent.getEmptyComponent(); - instance = new descriptor.type(descriptor.props); + } else { + if (render._isMockFunction && !render._getMockImplementation()) { + // Auto-mocked components may have a prototype with a mocked render + // function. For those, we'll need to mock the result of the render + // since we consider undefined to be invalid results from render. + render.mockImplementation( + ReactEmptyComponent.getEmptyComponent + ); + } instance.construct(descriptor); return instance; - } else if (render._isMockFunction && !render._getMockImplementation()) { - // Auto-mocked components may have a prototype with a mocked render - // function. For those, we'll need to mock the result of the render - // since we consider undefined to be invalid results from render. - render.mockImplementation( - ReactEmptyComponent.getEmptyComponent - ); } - instance.construct(descriptor); - return instance; } } - // Normal case for non-mocks - instance = new descriptor.type(descriptor.props); + // Special case string values + if (typeof descriptor.type === 'string') { + instance = ReactNativeComponent.createInstanceForTag( + descriptor.type, + descriptor.props, + parentCompositeType + ); + } else { + // Normal case for non-mocks and non-strings + instance = new descriptor.type(descriptor.props); + } if (__DEV__) { warning( diff --git a/src/test/reactComponentExpect.js b/src/test/reactComponentExpect.js index 89cc8178053..56815ecd424 100644 --- a/src/test/reactComponentExpect.js +++ b/src/test/reactComponentExpect.js @@ -106,7 +106,7 @@ mergeInto(reactComponentExpect.prototype, { toBeComponentOfType: function(convenienceConstructor) { expect( - this.instance().constructor === convenienceConstructor.type + this.instance()._descriptor.type === convenienceConstructor.type ).toBe(true); return this; }, @@ -126,7 +126,7 @@ mergeInto(reactComponentExpect.prototype, { toBeCompositeComponentWithType: function(convenienceConstructor) { this.toBeCompositeComponent(); expect( - this.instance().constructor === convenienceConstructor.type + this.instance()._descriptor.type === convenienceConstructor.type ).toBe(true); return this; },