@samitouri / QOS-React-2 / commits / 808e625d9d

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;