Use `createNodesFromMarkup`
Pulled out markup rendering logic for better reuse.
Tim Yung committed
Jul 31, 2013 at 16:31 UTC
808e625d9d74fb533ba8445e5d79c18b4fc3fb65
5 files changed
+254
-96
src/core/__tests__/ReactMultiChild-test.js
+12
-3
@@ -52,9 +52,7 @@ describe('ReactMultiChild', function() {
52
</div>,
53
container
54
);
55
- expect(setInnerHTML).toHaveBeenCalled();
56
- var callCountOnMount = setInnerHTML.callCount;
57
-
55
+ // Warm the cache used by `getMarkupWrap`.
56
React.renderComponent(
57
<div>
58
<p><span /><span /></p>
@@ -63,6 +61,17 @@ describe('ReactMultiChild', function() {
61
</div>,
62
container
63
);
64
+ expect(setInnerHTML).toHaveBeenCalled();
65
+ var callCountOnMount = setInnerHTML.callCount;
66
+
67
+ React.renderComponent(
68
+ <div>
69
+ <p><span /><span /><span /></p>
70
+ <p><span /><span /><span /></p>
71
+ <p><span /><span /><span /></p>
72
+ </div>,
73
+ container
74
+ );
75
expect(setInnerHTML.callCount).toBe(callCountOnMount + 1);
76
});
77
});
src/dom/Danger.js
+24
-93
@@ -23,65 +23,13 @@
23
24
var ExecutionEnvironment = require('ExecutionEnvironment');
25
26
+var createNodesFromMarkup = require('createNodesFromMarkup');
27
+var emptyFunction = require('emptyFunction');
28
+var getMarkupWrap = require('getMarkupWrap');
29
var invariant = require('invariant');
30
31
/**
29
- * Dummy container used to render all markup.
30
- */
31
-var dummyNode = ExecutionEnvironment.canUseDOM ?
32
- document.createElement('div') :
33
- null;
34
-
35
-/**
36
- * Some browsers cannot use `innerHTML` to render certain elements standalone,
37
- * so we wrap them, render the wrapped nodes, then extract the desired node.
38
- */
39
-var markupWrap = {
40
- 'option': [1, '<select multiple="true">', '</select>'],
41
- 'legend': [1, '<fieldset>', '</fieldset>'],
42
- 'area': [1, '<map>', '</map>'],
43
- 'param': [1, '<object>', '</object>'],
44
- 'thead': [1, '<table>', '</table>'],
45
- 'tr': [2, '<table><tbody>', '</tbody></table>'],
46
- 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
47
- 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>']
48
-};
49
-markupWrap['optgroup'] = markupWrap['option'];
50
-markupWrap['tbody'] = markupWrap['thead'];
51
-markupWrap['tfoot'] = markupWrap['thead'];
52
-markupWrap['colgroup'] = markupWrap['thead'];
53
-markupWrap['caption'] = markupWrap['thead'];
54
-markupWrap['th'] = markupWrap['td'];
55
-
56
-/**
57
- * In IE8, certain elements cannot render alone, so wrap all elements.
58
- */
59
-var defaultWrap = [1, '?<div>', '</div>'];
60
-
61
-/**
62
- * Feature detection, remove wraps that are unnecessary for the current browser.
63
- */
64
-if (dummyNode) {
65
- for (var nodeName in markupWrap) {
66
- if (!markupWrap.hasOwnProperty(nodeName)) {
67
- continue;
68
- }
69
- dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
70
- if (dummyNode.firstChild) {
71
- markupWrap[nodeName] = null;
72
- }
73
- }
74
- dummyNode.innerHTML = '<link />';
75
- if (dummyNode.firstChild) {
76
- defaultWrap = null;
77
- }
78
-}
79
-
80
-/**
81
- * Extracts the `nodeName` from a string of markup. This does not require a
82
- * regular expression match because we make assumptions about React-generated
83
- * markup (i.e. there are no spaces surrounding the opening tag and there is at
84
- * least an ID attribute).
32
+ * Extracts the `nodeName` from a string of markup.
33
*
34
* NOTE: Extracting the `nodeName` does not require a regular expression match
35
* because we make assumptions about React-generated markup (i.e. there are no
@@ -95,31 +43,6 @@ function getNodeName(markup) {
43
return markup.substring(1, markup.indexOf(' '));
44
}
45
98
-/**
99
- * Renders markup into nodes. The returned HTMLCollection is live and should be
100
- * used immediately (or at least before the next invocation to `renderMarkup`).
101
- *
102
- * @param {string} markup Markup for one or more nodes with the same `nodeName`.
103
- * @param {?string} nodeName Optional, the lowercase node name of the markup.
104
- * @return {*} An HTMLCollection.
105
- */
106
-function renderMarkup(markup, nodeName) {
107
- nodeName = nodeName || getNodeName(markup);
108
- var node = dummyNode;
109
- var wrap = markupWrap[nodeName] || defaultWrap;
110
- if (wrap) {
111
- node.innerHTML = wrap[1] + markup + wrap[2];
112
-
113
- var wrapDepth = wrap[0];
114
- while (wrapDepth--) {
115
- node = node.lastChild;
116
- }
117
- } else {
118
- node.innerHTML = markup;
119
- }
120
- return node.childNodes;
121
-}
122
-
46
var Danger = {
47
48
/**
@@ -147,31 +70,39 @@ var Danger = {
70
'dangerouslyRenderMarkup(...): Missing markup.'
71
);
72
nodeName = getNodeName(markupList[i]);
150
- nodeName = markupWrap[nodeName] ? nodeName : '*';
73
+ nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
74
markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
75
markupByNodeName[nodeName][i] = markupList[i];
76
}
154
- var renderedMarkup = [];
77
+ var resultList = [];
78
for (nodeName in markupByNodeName) {
79
if (!markupByNodeName.hasOwnProperty(nodeName)) {
80
continue;
81
}
82
var markupListByNodeName = markupByNodeName[nodeName];
83
var markup = markupListByNodeName.join('');
161
- // Render each group of markup.
162
- var childNode = renderMarkup(markup, nodeName)[0];
84
+ // Render each group of markup with similar `nodeName`.
85
+ var renderNodes = createNodesFromMarkup(markup, emptyFunction);
86
// Restore the initial ordering.
164
- for (var j = 0; j < markupListByNodeName.length; j++) {
87
+ var renderIndex = 0;
88
+ var resultIndex = 0;
89
+ while (resultIndex < markupListByNodeName.length) {
90
// `markupListByNodeName` may be a sparse array.
166
- if (markupListByNodeName[j]) {
167
- invariant(childNode, 'dangerouslyRenderMarkup(...): Missing node.');
168
- renderedMarkup[j] = childNode;
169
- childNode = childNode.nextSibling;
91
+ if (markupListByNodeName[resultIndex]) {
92
+ invariant(
93
+ resultList[resultIndex] = renderNodes[renderIndex],
94
+ 'dangerouslyRenderMarkup(...): Missing node.'
95
+ );
96
+ renderIndex++;
97
}
98
+ resultIndex++;
99
}
172
- invariant(!childNode, 'dangerouslyRenderMarkupO(...): Unexpected nodes.');
100
+ invariant(
101
+ renderIndex === renderNodes.length,
102
+ 'dangerouslyRenderMarkupO(...): Unexpected nodes.'
103
+ );
104
}
174
- return renderedMarkup;
105
+ return resultList;
106
},
107
108
/**
@@ -190,7 +121,7 @@ var Danger = {
121
'immediately.'
122
);
123
invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.');
193
- var newChild = renderMarkup(markup)[0];
124
+ var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
125
oldChild.parentNode.replaceChild(newChild, oldChild);
126
}
127
src/vendor/core/createArrayFrom.js
new
+56
@@ -0,0 +1,56 @@
1
+/**
2
+ * Copyright 2013 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 createArrayFrom
17
+ * @typechecks
18
+ */
19
+
20
+var hasArrayNature = require('hasArrayNature');
21
+
22
+/**
23
+ * Ensure that the argument is an array by wrapping it in an array if it is not.
24
+ * Creates a copy of the argument if it is already an array.
25
+ *
26
+ * This is mostly useful idiomatically:
27
+ *
28
+ * var createArrayFrom = require('createArrayFrom');
29
+ *
30
+ * function takesOneOrMoreThings(things) {
31
+ * things = createArrayFrom(things);
32
+ * ...
33
+ * }
34
+ *
35
+ * This allows you to treat `things' as an array, but accept scalars in the API.
36
+ *
37
+ * This is also good for converting certain pseudo-arrays, like `arguments` or
38
+ * HTMLCollections, into arrays.
39
+ *
40
+ * @param {*} obj
41
+ * @return {array}
42
+ */
43
+function createArrayFrom(obj) {
44
+ if (!hasArrayNature(obj)) {
45
+ return [obj];
46
+ }
47
+ if (obj.item) {
48
+ // IE does not support Array#slice on HTMLCollections
49
+ var l = obj.length, ret = new Array(l);
50
+ while (l--) { ret[l] = obj[l]; }
51
+ return ret;
52
+ }
53
+ return Array.prototype.slice.call(obj);
54
+}
55
+
56
+module.exports = createArrayFrom;
src/vendor/core/createNodesFromMarkup.js
new
+89
@@ -0,0 +1,89 @@
1
+/**
2
+ * Copyright 2013 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 createNodesFromMarkup
17
+ * @typechecks
18
+ */
19
+
20
+/*jslint evil: true, sub: true */
21
+
22
+var createArrayFrom = require('createArrayFrom');
23
+var getMarkupWrap = require('getMarkupWrap');
24
+var invariant = require('invariant');
25
+
26
+/**
27
+ * Dummy container used to render all markup.
28
+ */
29
+var dummyNode = document.createElement('div');
30
+
31
+/**
32
+ * Pattern used by `getNodeName`.
33
+ */
34
+var nodeNamePattern = /^\s*<(\w+)/;
35
+
36
+/**
37
+ * Extracts the `nodeName` of the first element in a string of markup.
38
+ *
39
+ * @param {string} markup String of markup.
40
+ * @return {?string} Node name of the supplied markup.
41
+ */
42
+function getNodeName(markup) {
43
+ var nodeNameMatch = markup.match(nodeNamePattern);
44
+ return nodeNameMatch && nodeNameMatch[1].toLowerCase();
45
+}
46
+
47
+/**
48
+ * Creates an array containing the nodes rendered from the supplied markup. The
49
+ * optionally supplied `handleScript` function will be invoked once for each
50
+ * <script> element that is rendered. If no `handleScript` function is supplied,
51
+ * an exception is thrown if any <script> elements are rendered.
52
+ *
53
+ * @param {string} markup A string of valid HTML markup.
54
+ * @param {?function} handleScript Invoked once for each rendered <script>.
55
+ * @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
56
+ */
57
+function createNodesFromMarkup(markup, handleScript) {
58
+ var node = dummyNode;
59
+ var nodeName = getNodeName(markup);
60
+
61
+ var wrap = nodeName && getMarkupWrap(nodeName);
62
+ if (wrap) {
63
+ node.innerHTML = wrap[1] + markup + wrap[2];
64
+
65
+ var wrapDepth = wrap[0];
66
+ while (wrapDepth--) {
67
+ node = node.lastChild;
68
+ }
69
+ } else {
70
+ node.innerHTML = markup;
71
+ }
72
+
73
+ var scripts = node.getElementsByTagName('script');
74
+ if (scripts.length) {
75
+ invariant(
76
+ handleScript,
77
+ 'createNodesFromMarkup(...): Unexpected <script> element rendered.'
78
+ );
79
+ createArrayFrom(scripts).forEach(handleScript);
80
+ }
81
+
82
+ var nodes = createArrayFrom(node.childNodes);
83
+ while (node.lastChild) {
84
+ node.removeChild(node.lastChild);
85
+ }
86
+ return nodes;
87
+}
88
+
89
+module.exports = createNodesFromMarkup;
src/vendor/core/getMarkupWrap.js
new
+73
@@ -0,0 +1,73 @@
1
+/**
2
+ * Copyright 2013 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 getMarkupWrap
17
+ */
18
+
19
+/**
20
+ * Dummy container used to detect which wraps are necessary.
21
+ */
22
+var dummyNode = document.createElement('div');
23
+
24
+/**
25
+ * Some browsers cannot use `innerHTML` to render certain elements standalone,
26
+ * so we wrap them, render the wrapped nodes, then extract the desired node.
27
+ *
28
+ * In IE8, certain elements cannot render alone, so wrap all elements ('*').
29
+ */
30
+var shouldWrap = {};
31
+var markupWrap = {
32
+ 'area': [1, '<map>', '</map>'],
33
+ 'caption': [1, '<table>', '</table>'],
34
+ 'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
35
+ 'colgroup': [1, '<table>', '</table>'],
36
+ 'legend': [1, '<fieldset>', '</fieldset>'],
37
+ 'optgroup': [1, '<select multiple="true">', '</select>'],
38
+ 'option': [1, '<select multiple="true">', '</select>'],
39
+ 'param': [1, '<object>', '</object>'],
40
+ 'tbody': [1, '<table>', '</table>'],
41
+ 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
42
+ 'tfoot': [1, '<table>', '</table>'],
43
+ 'th': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
44
+ 'thead': [1, '<table>', '</table>'],
45
+ 'tr': [2, '<table><tbody>', '</tbody></table>'],
46
+ '*': [1, '?<div>', '</div>']
47
+};
48
+
49
+/**
50
+ * Gets the markup wrap configuration for the supplied `nodeName`.
51
+ *
52
+ * NOTE: This lazily detects which wraps are necessary for the current browser.
53
+ *
54
+ * @param {string} nodeName Lowercase `nodeName`.
55
+ * @return {?array} Markup wrap configuration, if applicable.
56
+ */
57
+function getMarkupWrap(nodeName) {
58
+ if (!markupWrap.hasOwnProperty(nodeName)) {
59
+ nodeName = '*';
60
+ }
61
+ if (!shouldWrap.hasOwnProperty(nodeName)) {
62
+ if (nodeName === '*') {
63
+ dummyNode.innerHTML = '<link />';
64
+ } else {
65
+ dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
66
+ }
67
+ shouldWrap[nodeName] = !dummyNode.firstChild;
68
+ }
69
+ return shouldWrap[nodeName] ? markupWrap[nodeName] : null;
70
+}
71
+
72
+
73
+module.exports = getMarkupWrap;