@samitouri / QOS-React-2 / commits / 3aaccd2dc9

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.

Sebastian Markbage committed Oct 2, 2014 at 23:05 UTC 3aaccd2dc96cf229c0170d538fe81f7d22059a4e
16 files changed +329 -216
src/browser/ReactDOM.js
+134 -159
@@ -22,41 +22,23 @@
22 var ReactDescriptor = require('ReactDescriptor');
23 var ReactDescriptorValidator = require('ReactDescriptorValidator');
24 var ReactLegacyDescriptor = require('ReactLegacyDescriptor');
25 -var ReactDOMComponent = require('ReactDOMComponent');
25
27 -var mergeInto = require('mergeInto');
26 var mapObject = require('mapObject');
27
28 /**
31 - * Creates a new React class that is idempotent and capable of containing other
32 - * React components. It accepts event listeners and DOM properties that are
33 - * valid according to `DOMProperty`.
29 + * Create a factory that creates HTML tag descriptors.
30 *
35 - * - Event listeners: `onClick`, `onMouseDown`, etc.
36 - * - DOM properties: `className`, `name`, `title`, etc.
37 - *
38 - * The `style` property functions differently from the DOM API. It accepts an
39 - * object mapping of style properties to values.
40 - *
41 - * @param {boolean} omitClose True if the close tag should be omitted.
31 * @param {string} tag Tag name (e.g. `div`).
32 * @private
33 */
45 -function createDOMComponentClass(omitClose, tag) {
46 - var Constructor = function(props) {
47 - // This constructor and it's argument is currently used by mocks.
48 - };
49 - Constructor.prototype = new ReactDOMComponent(tag, omitClose);
50 - Constructor.prototype.constructor = Constructor;
51 - Constructor.displayName = tag;
52 -
34 +function createDOMFactory(tag) {
35 if (__DEV__) {
36 return ReactLegacyDescriptor.wrapFactory(
55 - ReactDescriptorValidator.createFactory(Constructor)
37 + ReactDescriptorValidator.createFactory(tag)
38 );
39 }
40 return ReactLegacyDescriptor.wrapFactory(
59 - ReactDescriptor.createFactory(Constructor)
41 + ReactDescriptor.createFactory(tag)
42 );
43 }
44
@@ -67,145 +49,138 @@ function createDOMComponentClass(omitClose, tag) {
49 * @public
50 */
51 var ReactDOM = mapObject({
70 - a: false,
71 - abbr: false,
72 - address: false,
73 - area: true,
74 - article: false,
75 - aside: false,
76 - audio: false,
77 - b: false,
78 - base: true,
79 - bdi: false,
80 - bdo: false,
81 - big: false,
82 - blockquote: false,
83 - body: false,
84 - br: true,
85 - button: false,
86 - canvas: false,
87 - caption: false,
88 - cite: false,
89 - code: false,
90 - col: true,
91 - colgroup: false,
92 - data: false,
93 - datalist: false,
94 - dd: false,
95 - del: false,
96 - details: false,
97 - dfn: false,
98 - dialog: false,
99 - div: false,
100 - dl: false,
101 - dt: false,
102 - em: false,
103 - embed: true,
104 - fieldset: false,
105 - figcaption: false,
106 - figure: false,
107 - footer: false,
108 - form: false, // NOTE: Injected, see `ReactDOMForm`.
109 - h1: false,
110 - h2: false,
111 - h3: false,
112 - h4: false,
113 - h5: false,
114 - h6: false,
115 - head: false,
116 - header: false,
117 - hr: true,
118 - html: false,
119 - i: false,
120 - iframe: false,
121 - img: true,
122 - input: true,
123 - ins: false,
124 - kbd: false,
125 - keygen: true,
126 - label: false,
127 - legend: false,
128 - li: false,
129 - link: true,
130 - main: false,
131 - map: false,
132 - mark: false,
133 - menu: false,
134 - menuitem: false, // NOTE: Close tag should be omitted, but causes problems.
135 - meta: true,
136 - meter: false,
137 - nav: false,
138 - noscript: false,
139 - object: false,
140 - ol: false,
141 - optgroup: false,
142 - option: false,
143 - output: false,
144 - p: false,
145 - param: true,
146 - picture: false,
147 - pre: false,
148 - progress: false,
149 - q: false,
150 - rp: false,
151 - rt: false,
152 - ruby: false,
153 - s: false,
154 - samp: false,
155 - script: false,
156 - section: false,
157 - select: false,
158 - small: false,
159 - source: true,
160 - span: false,
161 - strong: false,
162 - style: false,
163 - sub: false,
164 - summary: false,
165 - sup: false,
166 - table: false,
167 - tbody: false,
168 - td: false,
169 - textarea: false, // NOTE: Injected, see `ReactDOMTextarea`.
170 - tfoot: false,
171 - th: false,
172 - thead: false,
173 - time: false,
174 - title: false,
175 - tr: false,
176 - track: true,
177 - u: false,
178 - ul: false,
179 - 'var': false,
180 - video: false,
181 - wbr: true,
52 + a: 'a',
53 + abbr: 'abbr',
54 + address: 'address',
55 + area: 'area',
56 + article: 'article',
57 + aside: 'aside',
58 + audio: 'audio',
59 + b: 'b',
60 + base: 'base',
61 + bdi: 'bdi',
62 + bdo: 'bdo',
63 + big: 'big',
64 + blockquote: 'blockquote',
65 + body: 'body',
66 + br: 'br',
67 + button: 'button',
68 + canvas: 'canvas',
69 + caption: 'caption',
70 + cite: 'cite',
71 + code: 'code',
72 + col: 'col',
73 + colgroup: 'colgroup',
74 + data: 'data',
75 + datalist: 'datalist',
76 + dd: 'dd',
77 + del: 'del',
78 + details: 'details',
79 + dfn: 'dfn',
80 + dialog: 'dialog',
81 + div: 'div',
82 + dl: 'dl',
83 + dt: 'dt',
84 + em: 'em',
85 + embed: 'embed',
86 + fieldset: 'fieldset',
87 + figcaption: 'figcaption',
88 + figure: 'figure',
89 + footer: 'footer',
90 + form: 'form',
91 + h1: 'h1',
92 + h2: 'h2',
93 + h3: 'h3',
94 + h4: 'h4',
95 + h5: 'h5',
96 + h6: 'h6',
97 + head: 'head',
98 + header: 'header',
99 + hr: 'hr',
100 + html: 'html',
101 + i: 'i',
102 + iframe: 'iframe',
103 + img: 'img',
104 + input: 'input',
105 + ins: 'ins',
106 + kbd: 'kbd',
107 + keygen: 'keygen',
108 + label: 'label',
109 + legend: 'legend',
110 + li: 'li',
111 + link: 'link',
112 + main: 'main',
113 + map: 'map',
114 + mark: 'mark',
115 + menu: 'menu',
116 + menuitem: 'menuitem',
117 + meta: 'meta',
118 + meter: 'meter',
119 + nav: 'nav',
120 + noscript: 'noscript',
121 + object: 'object',
122 + ol: 'ol',
123 + optgroup: 'optgroup',
124 + option: 'option',
125 + output: 'output',
126 + p: 'p',
127 + param: 'param',
128 + picture: 'picture',
129 + pre: 'pre',
130 + progress: 'progress',
131 + q: 'q',
132 + rp: 'rp',
133 + rt: 'rt',
134 + ruby: 'ruby',
135 + s: 's',
136 + samp: 'samp',
137 + script: 'script',
138 + section: 'section',
139 + select: 'select',
140 + small: 'small',
141 + source: 'source',
142 + span: 'span',
143 + strong: 'strong',
144 + style: 'style',
145 + sub: 'sub',
146 + summary: 'summary',
147 + sup: 'sup',
148 + table: 'table',
149 + tbody: 'tbody',
150 + td: 'td',
151 + textarea: 'textarea',
152 + tfoot: 'tfoot',
153 + th: 'th',
154 + thead: 'thead',
155 + time: 'time',
156 + title: 'title',
157 + tr: 'tr',
158 + track: 'track',
159 + u: 'u',
160 + ul: 'ul',
161 + 'var': 'var',
162 + video: 'video',
163 + wbr: 'wbr',
164
165 // SVG
184 - circle: false,
185 - defs: false,
186 - ellipse: false,
187 - g: false,
188 - line: false,
189 - linearGradient: false,
190 - mask: false,
191 - path: false,
192 - pattern: false,
193 - polygon: false,
194 - polyline: false,
195 - radialGradient: false,
196 - rect: false,
197 - stop: false,
198 - svg: false,
199 - text: false,
200 - tspan: false
201 -}, createDOMComponentClass);
202 -
203 -var injection = {
204 - injectComponentClasses: function(componentClasses) {
205 - mergeInto(ReactDOM, componentClasses);
206 - }
207 -};
166 + circle: 'circle',
167 + defs: 'defs',
168 + ellipse: 'ellipse',
169 + g: 'g',
170 + line: 'line',
171 + linearGradient: 'linearGradient',
172 + mask: 'mask',
173 + path: 'path',
174 + pattern: 'pattern',
175 + polygon: 'polygon',
176 + polyline: 'polyline',
177 + radialGradient: 'radialGradient',
178 + rect: 'rect',
179 + stop: 'stop',
180 + svg: 'svg',
181 + text: 'text',
182 + tspan: 'tspan'
183
209 -ReactDOM.injection = injection;
184 +}, createDOMFactory);
185
186 module.exports = ReactDOM;
src/browser/server/ReactServerRendering.js
+2 -2
@@ -49,7 +49,7 @@ function renderComponentToString(component) {
49 transaction = ReactServerRenderingTransaction.getPooled(false);
50
51 return transaction.perform(function() {
52 - var componentInstance = instantiateReactComponent(component);
52 + var componentInstance = instantiateReactComponent(component, null);
53 var markup = componentInstance.mountComponent(id, transaction, 0);
54 return ReactMarkupChecksum.addChecksumToMarkup(markup);
55 }, null);
@@ -75,7 +75,7 @@ function renderComponentToStaticMarkup(component) {
75 transaction = ReactServerRenderingTransaction.getPooled(true);
76
77 return transaction.perform(function() {
78 - var componentInstance = instantiateReactComponent(component);
78 + var componentInstance = instantiateReactComponent(component, null);
79 return componentInstance.mountComponent(id, transaction, 0);
80 }, null);
81 } finally {
src/browser/ui/ReactDOMComponent.js
+39 -5
@@ -101,18 +101,51 @@ function putListener(id, registrationName, listener, transaction) {
101 );
102 }
103
104 +// For HTML, certain tags should omit their close tag. We keep a whitelist for
105 +// those special cased tags.
106 +
107 +var omittedCloseTags = {
108 + 'area': true,
109 + 'base': true,
110 + 'br': true,
111 + 'col': true,
112 + 'embed': true,
113 + 'hr': true,
114 + 'img': true,
115 + 'input': true,
116 + 'keygen': true,
117 + 'link': true,
118 + 'meta': true,
119 + 'param': true,
120 + 'source': true,
121 + 'track': true,
122 + 'wbr': true
123 + // NOTE: menuitem's close tag should be omitted, but that causes problems.
124 +};
125
126 /**
127 + * Creates a new React class that is idempotent and capable of containing other
128 + * React components. It accepts event listeners and DOM properties that are
129 + * valid according to `DOMProperty`.
130 + *
131 + * - Event listeners: `onClick`, `onMouseDown`, etc.
132 + * - DOM properties: `className`, `name`, `title`, etc.
133 + *
134 + * The `style` property functions differently from the DOM API. It accepts an
135 + * object mapping of style properties to values.
136 + *
137 * @constructor ReactDOMComponent
138 * @extends ReactComponent
139 * @extends ReactMultiChild
140 */
110 -function ReactDOMComponent(tag, omitClose) {
111 - this._tagOpen = '<' + tag;
112 - this._tagClose = omitClose ? '' : '</' + tag + '>';
141 +function ReactDOMComponent(tag) {
142 + // TODO: DANGEROUS this tag should be sanitized.
143 + this._tag = tag;
144 this.tagName = tag.toUpperCase();
145 }
146
147 +ReactDOMComponent.displayName = 'ReactDOMComponent';
148 +
149 ReactDOMComponent.Mixin = {
150
151 /**
@@ -136,10 +169,11 @@ ReactDOMComponent.Mixin = {
169 mountDepth
170 );
171 assertValidProps(this.props);
172 + var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>';
173 return (
174 this._createOpenTagMarkupAndPutListeners(transaction) +
175 this._createContentMarkup(transaction) +
142 - this._tagClose
176 + closeTag
177 );
178 }
179 ),
@@ -158,7 +192,7 @@ ReactDOMComponent.Mixin = {
192 */
193 _createOpenTagMarkupAndPutListeners: function(transaction) {
194 var props = this.props;
161 - var ret = this._tagOpen;
195 + var ret = '<' + this._tag;
196
197 for (var propKey in props) {
198 if (!props.hasOwnProperty(propKey)) {
src/browser/ui/ReactDefaultInjection.js
+18 -14
@@ -31,7 +31,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
31 var ReactComponentBrowserEnvironment =
32 require('ReactComponentBrowserEnvironment');
33 var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
34 -var ReactDOM = require('ReactDOM');
34 +var ReactDOMComponent = require('ReactDOMComponent');
35 var ReactDOMButton = require('ReactDOMButton');
36 var ReactDOMForm = require('ReactDOMForm');
37 var ReactDOMImg = require('ReactDOMImg');
@@ -76,18 +76,22 @@ function inject() {
76 BeforeInputEventPlugin: BeforeInputEventPlugin
77 });
78
79 - ReactInjection.DOM.injectComponentClasses({
80 - button: ReactDOMButton,
81 - form: ReactDOMForm,
82 - img: ReactDOMImg,
83 - input: ReactDOMInput,
84 - option: ReactDOMOption,
85 - select: ReactDOMSelect,
86 - textarea: ReactDOMTextarea,
87 -
88 - html: createFullPageComponent(ReactDOM.html),
89 - head: createFullPageComponent(ReactDOM.head),
90 - body: createFullPageComponent(ReactDOM.body)
79 + ReactInjection.NativeComponent.injectGenericComponentClass(
80 + ReactDOMComponent
81 + );
82 +
83 + ReactInjection.NativeComponent.injectComponentClasses({
84 + 'button': ReactDOMButton,
85 + 'form': ReactDOMForm,
86 + 'img': ReactDOMImg,
87 + 'input': ReactDOMInput,
88 + 'option': ReactDOMOption,
89 + 'select': ReactDOMSelect,
90 + 'textarea': ReactDOMTextarea,
91 +
92 + 'html': createFullPageComponent('html'),
93 + 'head': createFullPageComponent('head'),
94 + 'body': createFullPageComponent('body')
95 });
96
97 // This needs to happen after createFullPageComponent() otherwise the mixin
@@ -97,7 +101,7 @@ function inject() {
101 ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
102 ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
103
100 - ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.noscript);
104 + ReactInjection.EmptyComponent.injectEmptyComponent('noscript');
105
106 ReactInjection.Updates.injectReconcileTransaction(
107 ReactComponentBrowserEnvironment.ReactReconcileTransaction
src/browser/ui/ReactInjection.js
+2 -2
@@ -22,9 +22,9 @@ var DOMProperty = require('DOMProperty');
22 var EventPluginHub = require('EventPluginHub');
23 var ReactComponent = require('ReactComponent');
24 var ReactCompositeComponent = require('ReactCompositeComponent');
25 -var ReactDOM = require('ReactDOM');
25 var ReactEmptyComponent = require('ReactEmptyComponent');
26 var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
27 +var ReactNativeComponent = require('ReactNativeComponent');
28 var ReactPerf = require('ReactPerf');
29 var ReactRootIndex = require('ReactRootIndex');
30 var ReactUpdates = require('ReactUpdates');
@@ -35,8 +35,8 @@ var ReactInjection = {
35 DOMProperty: DOMProperty.injection,
36 EmptyComponent: ReactEmptyComponent.injection,
37 EventPluginHub: EventPluginHub.injection,
38 - DOM: ReactDOM.injection,
38 EventEmitter: ReactBrowserEventEmitter.injection,
39 + NativeComponent: ReactNativeComponent.injection,
40 Perf: ReactPerf.injection,
41 RootIndex: ReactRootIndex.injection,
42 Updates: ReactUpdates.injection
src/browser/ui/ReactMount.js
+1 -1
@@ -307,7 +307,7 @@ var ReactMount = {
307 'componentDidUpdate.'
308 );
309
310 - var componentInstance = instantiateReactComponent(nextComponent);
310 + var componentInstance = instantiateReactComponent(nextComponent, null);
311 var reactRootID = ReactMount._registerComponent(
312 componentInstance,
313 container
src/browser/ui/dom/components/createFullPageComponent.js
+4 -6
@@ -33,16 +33,14 @@ var invariant = require('invariant');
33 * take advantage of React's reconciliation for styling and <title>
34 * management. So we just document it and throw in dangerous cases.
35 *
36 - * @param {function} componentClass convenience constructor to wrap
36 + * @param {string} tag The tag to wrap
37 * @return {function} convenience constructor of new component
38 */
39 -function createFullPageComponent(componentClass) {
40 - var elementFactory = ReactDescriptor.createFactory(componentClass.type);
39 +function createFullPageComponent(tag) {
40 + var elementFactory = ReactDescriptor.createFactory(tag);
41
42 var FullPageComponent = ReactCompositeComponent.createClass({
43 - displayName: 'ReactFullPageComponent' + (
44 - componentClass.type.displayName || ''
45 - ),
43 + displayName: 'ReactFullPageComponent' + tag,
44
45 componentWillUnmount: function() {
46 invariant(
src/core/ReactCompositeComponent.js
+6 -2
@@ -793,7 +793,8 @@ var ReactCompositeComponentMixin = {
793 }
794
795 this._renderedComponent = instantiateReactComponent(
796 - this._renderValidatedComponent()
796 + this._renderValidatedComponent(),
797 + this._descriptor.type // The wrapping type
798 );
799
800 // Done with mounting, `setState` will now trigger UI changes.
@@ -1185,7 +1186,10 @@ var ReactCompositeComponentMixin = {
1186 var thisID = this._rootNodeID;
1187 var prevComponentID = prevComponentInstance._rootNodeID;
1188 prevComponentInstance.unmountComponent();
1188 - this._renderedComponent = instantiateReactComponent(nextDescriptor);
1189 + this._renderedComponent = instantiateReactComponent(
1190 + nextDescriptor,
1191 + this._descriptor.type
1192 + );
1193 var nextMarkup = this._renderedComponent.mountComponent(
1194 thisID,
1195 transaction,
src/core/ReactDescriptor.js
+7 -4
@@ -222,10 +222,13 @@ ReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) {
222 * @public
223 */
224 ReactDescriptor.isValidFactory = function(factory) {
225 - return typeof factory === 'function' &&
226 - typeof factory.type === 'function' &&
227 - typeof factory.type.prototype.mountComponent === 'function' &&
228 - typeof factory.type.prototype.receiveComponent === 'function';
225 + return typeof factory === 'function' && (
226 + typeof factory.type === 'string' || (
227 + typeof factory.type === 'function' &&
228 + typeof factory.type.prototype.mountComponent === 'function' &&
229 + typeof factory.type.prototype.receiveComponent === 'function'
230 + )
231 + );
232 };
233
234 /**
src/core/ReactEmptyComponent.js
+1 -1
@@ -29,7 +29,7 @@ var nullComponentIdsRegistry = {};
29
30 var ReactEmptyComponentInjection = {
31 injectEmptyComponent: function(emptyComponent) {
32 - component = ReactDescriptor.createFactory(emptyComponent.type);
32 + component = ReactDescriptor.createFactory(emptyComponent);
33 }
34 };
35
src/core/ReactMultiChild.js
+5 -2
@@ -195,7 +195,7 @@ var ReactMultiChild = {
195 if (children.hasOwnProperty(name)) {
196 // The rendered children must be turned into instances as they're
197 // mounted.
198 - var childInstance = instantiateReactComponent(child);
198 + var childInstance = instantiateReactComponent(child, null);
199 children[name] = childInstance;
200 // Inlined for performance, see `ReactInstanceHandles.createReactID`.
201 var rootID = this._rootNodeID + name;
@@ -300,7 +300,10 @@ var ReactMultiChild = {
300 this._unmountChildByName(prevChild, name);
301 }
302 // The child must be instantiated before it's mounted.
303 - var nextChildInstance = instantiateReactComponent(nextDescriptor);
303 + var nextChildInstance = instantiateReactComponent(
304 + nextDescriptor,
305 + null
306 + );
307 this._mountChildByNameAtIndex(
308 nextChildInstance, name, nextIndex, transaction
309 );
src/core/ReactNativeComponent.js new
+76
@@ -0,0 +1,76 @@
1 +/**
2 + * Copyright 2014 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactNativeComponent
17 + */
18 +
19 +"use strict";
20 +
21 +var invariant = require('invariant');
22 +var mergeInto = require('mergeInto');
23 +
24 +var genericComponentClass = null;
25 +// This registry keeps track of wrapper classes around native tags
26 +var tagToComponentClass = {};
27 +
28 +var ReactNativeComponentInjection = {
29 + // This accepts a class that receives the tag string. This is a catch all
30 + // that can render any kind of tag.
31 + injectGenericComponentClass: function(componentClass) {
32 + genericComponentClass = componentClass;
33 + },
34 + // This accepts a keyed object with classes as values. Each key represents a
35 + // tag. That particular tag will use this class instead of the generic one.
36 + injectComponentClasses: function(componentClasses) {
37 + mergeInto(tagToComponentClass, componentClasses);
38 + }
39 +};
40 +
41 +/**
42 + * Create an internal class for a specific tag.
43 + *
44 + * @param {string} tag The tag for which to create an internal instance.
45 + * @param {any} props The props passed to the instance constructor.
46 + * @return {ReactComponent} component The injected empty component.
47 + */
48 +function createInstanceForTag(tag, props, parentType) {
49 + var componentClass = tagToComponentClass[tag];
50 + if (componentClass == null) {
51 + invariant(
52 + genericComponentClass,
53 + 'There is no registered component for the tag %s',
54 + tag
55 + );
56 + return new genericComponentClass(tag, props);
57 + }
58 + if (parentType === tag) {
59 + // Avoid recursion
60 + invariant(
61 + genericComponentClass,
62 + 'There is no registered component for the tag %s',
63 + tag
64 + );
65 + return new genericComponentClass(tag, props);
66 + }
67 + // Unwrap legacy factories
68 + return new componentClass.type(props);
69 +}
70 +
71 +var ReactNativeComponent = {
72 + createInstanceForTag: createInstanceForTag,
73 + injection: ReactNativeComponentInjection,
74 +};
75 +
76 +module.exports = ReactNativeComponent;
src/core/ReactPropTransferer.js
+2
@@ -141,6 +141,8 @@ var ReactPropTransferer = {
141 'don\'t own, %s. This usually means you are calling ' +
142 'transferPropsTo() on a component passed in as props or children.',
143 this.constructor.displayName,
144 + typeof descriptor.type === 'string' ?
145 + descriptor.type :
146 descriptor.type.displayName
147 );
148
src/core/__tests__/ReactDescriptor-test.js
+6
@@ -297,4 +297,10 @@ describe('ReactDescriptor', function() {
297 expect(typeof Component.specialType.isRequired).toBe("function");
298 });
299
300 + it('allows a DOM descriptor to be used with a string', function() {
301 + var descriptor = React.createDescriptor('div', { className: 'foo' });
302 + var instance = ReactTestUtils.renderIntoDocument(descriptor);
303 + expect(instance.getDOMNode().tagName).toBe('DIV');
304 + });
305 +
306 });
src/core/instantiateReactComponent.js
+24 -16
@@ -23,6 +23,7 @@ var warning = require('warning');
23
24 var ReactDescriptor = require('ReactDescriptor');
25 var ReactLegacyDescriptor = require('ReactLegacyDescriptor');
26 +var ReactNativeComponent = require('ReactNativeComponent');
27 var ReactEmptyComponent = require('ReactEmptyComponent');
28
29 /**
@@ -30,10 +31,11 @@ var ReactEmptyComponent = require('ReactEmptyComponent');
31 * mounted.
32 *
33 * @param {object} descriptor
33 - * @return {object} A new instance of componentDescriptor's constructor.
34 + * @param {*} parentCompositeType The composite type that resolved this.
35 + * @return {object} A new instance of the descriptor's constructor.
36 * @protected
37 */
36 -function instantiateReactComponent(descriptor) {
38 +function instantiateReactComponent(descriptor, parentCompositeType) {
39 var instance;
40
41 if (__DEV__) {
@@ -41,8 +43,6 @@ function instantiateReactComponent(descriptor) {
43 descriptor && (typeof descriptor.type === 'function' ||
44 typeof descriptor.type === 'string'),
45 'Only functions or strings can be mounted as React components.'
44 - // Not really strings yet, but as soon as I solve the cyclic dep, they
45 - // will be allowed here.
46 );
47
48 // Resolve mock instances
@@ -72,24 +72,32 @@ function instantiateReactComponent(descriptor) {
72 // there is no render function on the instance. We replace the whole
73 // component with an empty component instance instead.
74 descriptor = ReactEmptyComponent.getEmptyComponent();
75 - instance = new descriptor.type(descriptor.props);
75 + } else {
76 + if (render._isMockFunction && !render._getMockImplementation()) {
77 + // Auto-mocked components may have a prototype with a mocked render
78 + // function. For those, we'll need to mock the result of the render
79 + // since we consider undefined to be invalid results from render.
80 + render.mockImplementation(
81 + ReactEmptyComponent.getEmptyComponent
82 + );
83 + }
84 instance.construct(descriptor);
85 return instance;
78 - } else if (render._isMockFunction && !render._getMockImplementation()) {
79 - // Auto-mocked components may have a prototype with a mocked render
80 - // function. For those, we'll need to mock the result of the render
81 - // since we consider undefined to be invalid results from render.
82 - render.mockImplementation(
83 - ReactEmptyComponent.getEmptyComponent
84 - );
86 }
86 - instance.construct(descriptor);
87 - return instance;
87 }
88 }
89
91 - // Normal case for non-mocks
92 - instance = new descriptor.type(descriptor.props);
90 + // Special case string values
91 + if (typeof descriptor.type === 'string') {
92 + instance = ReactNativeComponent.createInstanceForTag(
93 + descriptor.type,
94 + descriptor.props,
95 + parentCompositeType
96 + );
97 + } else {
98 + // Normal case for non-mocks and non-strings
99 + instance = new descriptor.type(descriptor.props);
100 + }
101
102 if (__DEV__) {
103 warning(
src/test/reactComponentExpect.js
+2 -2
@@ -106,7 +106,7 @@ mergeInto(reactComponentExpect.prototype, {
106
107 toBeComponentOfType: function(convenienceConstructor) {
108 expect(
109 - this.instance().constructor === convenienceConstructor.type
109 + this.instance()._descriptor.type === convenienceConstructor.type
110 ).toBe(true);
111 return this;
112 },
@@ -126,7 +126,7 @@ mergeInto(reactComponentExpect.prototype, {
126 toBeCompositeComponentWithType: function(convenienceConstructor) {
127 this.toBeCompositeComponent();
128 expect(
129 - this.instance().constructor === convenienceConstructor.type
129 + this.instance()._descriptor.type === convenienceConstructor.type
130 ).toBe(true);
131 return this;
132 },