From 808e625d9d74fb533ba8445e5d79c18b4fc3fb65 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Wed, 31 Jul 2013 16:31:53 -0700 Subject: [PATCH] Use `createNodesFromMarkup` Pulled out markup rendering logic for better reuse. --- src/core/__tests__/ReactMultiChild-test.js | 15 ++- src/dom/Danger.js | 117 +++++---------------- src/vendor/core/createArrayFrom.js | 56 ++++++++++ src/vendor/core/createNodesFromMarkup.js | 89 ++++++++++++++++ src/vendor/core/getMarkupWrap.js | 73 +++++++++++++ 5 files changed, 254 insertions(+), 96 deletions(-) create mode 100644 src/vendor/core/createArrayFrom.js create mode 100644 src/vendor/core/createNodesFromMarkup.js create mode 100644 src/vendor/core/getMarkupWrap.js diff --git a/src/core/__tests__/ReactMultiChild-test.js b/src/core/__tests__/ReactMultiChild-test.js index 2a6d25ef22..d8bb25a165 100644 --- a/src/core/__tests__/ReactMultiChild-test.js +++ b/src/core/__tests__/ReactMultiChild-test.js @@ -52,9 +52,7 @@ describe('ReactMultiChild', function() { , container ); - expect(setInnerHTML).toHaveBeenCalled(); - var callCountOnMount = setInnerHTML.callCount; - + // Warm the cache used by `getMarkupWrap`. React.renderComponent(

@@ -63,6 +61,17 @@ describe('ReactMultiChild', function() {
, container ); + expect(setInnerHTML).toHaveBeenCalled(); + var callCountOnMount = setInnerHTML.callCount; + + React.renderComponent( +
+

+

+

+
, + container + ); expect(setInnerHTML.callCount).toBe(callCountOnMount + 1); }); }); diff --git a/src/dom/Danger.js b/src/dom/Danger.js index 2c18695a2b..02615ea0e3 100644 --- a/src/dom/Danger.js +++ b/src/dom/Danger.js @@ -23,65 +23,13 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); +var createNodesFromMarkup = require('createNodesFromMarkup'); +var emptyFunction = require('emptyFunction'); +var getMarkupWrap = require('getMarkupWrap'); var invariant = require('invariant'); /** - * Dummy container used to render all markup. - */ -var dummyNode = ExecutionEnvironment.canUseDOM ? - document.createElement('div') : - null; - -/** - * Some browsers cannot use `innerHTML` to render certain elements standalone, - * so we wrap them, render the wrapped nodes, then extract the desired node. - */ -var markupWrap = { - 'option': [1, ''], - 'legend': [1, '
', '
'], - 'area': [1, '', ''], - 'param': [1, '', ''], - 'thead': [1, '', '
'], - 'tr': [2, '', '
'], - 'col': [2, '', '
'], - 'td': [3, '', '
'] -}; -markupWrap['optgroup'] = markupWrap['option']; -markupWrap['tbody'] = markupWrap['thead']; -markupWrap['tfoot'] = markupWrap['thead']; -markupWrap['colgroup'] = markupWrap['thead']; -markupWrap['caption'] = markupWrap['thead']; -markupWrap['th'] = markupWrap['td']; - -/** - * In IE8, certain elements cannot render alone, so wrap all elements. - */ -var defaultWrap = [1, '?
', '
']; - -/** - * Feature detection, remove wraps that are unnecessary for the current browser. - */ -if (dummyNode) { - for (var nodeName in markupWrap) { - if (!markupWrap.hasOwnProperty(nodeName)) { - continue; - } - dummyNode.innerHTML = '<' + nodeName + '>'; - if (dummyNode.firstChild) { - markupWrap[nodeName] = null; - } - } - dummyNode.innerHTML = ''; - if (dummyNode.firstChild) { - defaultWrap = null; - } -} - -/** - * Extracts the `nodeName` from a string of markup. This does not require a - * regular expression match because we make assumptions about React-generated - * markup (i.e. there are no spaces surrounding the opening tag and there is at - * least an ID attribute). + * Extracts the `nodeName` from a string of markup. * * NOTE: Extracting the `nodeName` does not require a regular expression match * because we make assumptions about React-generated markup (i.e. there are no @@ -95,31 +43,6 @@ function getNodeName(markup) { return markup.substring(1, markup.indexOf(' ')); } -/** - * Renders markup into nodes. The returned HTMLCollection is live and should be - * used immediately (or at least before the next invocation to `renderMarkup`). - * - * @param {string} markup Markup for one or more nodes with the same `nodeName`. - * @param {?string} nodeName Optional, the lowercase node name of the markup. - * @return {*} An HTMLCollection. - */ -function renderMarkup(markup, nodeName) { - nodeName = nodeName || getNodeName(markup); - var node = dummyNode; - var wrap = markupWrap[nodeName] || defaultWrap; - if (wrap) { - node.innerHTML = wrap[1] + markup + wrap[2]; - - var wrapDepth = wrap[0]; - while (wrapDepth--) { - node = node.lastChild; - } - } else { - node.innerHTML = markup; - } - return node.childNodes; -} - var Danger = { /** @@ -147,31 +70,39 @@ var Danger = { 'dangerouslyRenderMarkup(...): Missing markup.' ); nodeName = getNodeName(markupList[i]); - nodeName = markupWrap[nodeName] ? nodeName : '*'; + nodeName = getMarkupWrap(nodeName) ? nodeName : '*'; markupByNodeName[nodeName] = markupByNodeName[nodeName] || []; markupByNodeName[nodeName][i] = markupList[i]; } - var renderedMarkup = []; + var resultList = []; for (nodeName in markupByNodeName) { if (!markupByNodeName.hasOwnProperty(nodeName)) { continue; } var markupListByNodeName = markupByNodeName[nodeName]; var markup = markupListByNodeName.join(''); - // Render each group of markup. - var childNode = renderMarkup(markup, nodeName)[0]; + // Render each group of markup with similar `nodeName`. + var renderNodes = createNodesFromMarkup(markup, emptyFunction); // Restore the initial ordering. - for (var j = 0; j < markupListByNodeName.length; j++) { + var renderIndex = 0; + var resultIndex = 0; + while (resultIndex < markupListByNodeName.length) { // `markupListByNodeName` may be a sparse array. - if (markupListByNodeName[j]) { - invariant(childNode, 'dangerouslyRenderMarkup(...): Missing node.'); - renderedMarkup[j] = childNode; - childNode = childNode.nextSibling; + if (markupListByNodeName[resultIndex]) { + invariant( + resultList[resultIndex] = renderNodes[renderIndex], + 'dangerouslyRenderMarkup(...): Missing node.' + ); + renderIndex++; } + resultIndex++; } - invariant(!childNode, 'dangerouslyRenderMarkupO(...): Unexpected nodes.'); + invariant( + renderIndex === renderNodes.length, + 'dangerouslyRenderMarkupO(...): Unexpected nodes.' + ); } - return renderedMarkup; + return resultList; }, /** @@ -190,7 +121,7 @@ var Danger = { 'immediately.' ); invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.'); - var newChild = renderMarkup(markup)[0]; + var newChild = createNodesFromMarkup(markup, emptyFunction)[0]; oldChild.parentNode.replaceChild(newChild, oldChild); } diff --git a/src/vendor/core/createArrayFrom.js b/src/vendor/core/createArrayFrom.js new file mode 100644 index 0000000000..1924c0c241 --- /dev/null +++ b/src/vendor/core/createArrayFrom.js @@ -0,0 +1,56 @@ +/** + * Copyright 2013 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 createArrayFrom + * @typechecks + */ + +var hasArrayNature = require('hasArrayNature'); + +/** + * Ensure that the argument is an array by wrapping it in an array if it is not. + * Creates a copy of the argument if it is already an array. + * + * This is mostly useful idiomatically: + * + * var createArrayFrom = require('createArrayFrom'); + * + * function takesOneOrMoreThings(things) { + * things = createArrayFrom(things); + * ... + * } + * + * This allows you to treat `things' as an array, but accept scalars in the API. + * + * This is also good for converting certain pseudo-arrays, like `arguments` or + * HTMLCollections, into arrays. + * + * @param {*} obj + * @return {array} + */ +function createArrayFrom(obj) { + if (!hasArrayNature(obj)) { + return [obj]; + } + if (obj.item) { + // IE does not support Array#slice on HTMLCollections + var l = obj.length, ret = new Array(l); + while (l--) { ret[l] = obj[l]; } + return ret; + } + return Array.prototype.slice.call(obj); +} + +module.exports = createArrayFrom; diff --git a/src/vendor/core/createNodesFromMarkup.js b/src/vendor/core/createNodesFromMarkup.js new file mode 100644 index 0000000000..6ef538fa40 --- /dev/null +++ b/src/vendor/core/createNodesFromMarkup.js @@ -0,0 +1,89 @@ +/** + * Copyright 2013 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 createNodesFromMarkup + * @typechecks + */ + +/*jslint evil: true, sub: true */ + +var createArrayFrom = require('createArrayFrom'); +var getMarkupWrap = require('getMarkupWrap'); +var invariant = require('invariant'); + +/** + * Dummy container used to render all markup. + */ +var dummyNode = document.createElement('div'); + +/** + * Pattern used by `getNodeName`. + */ +var nodeNamePattern = /^\s*<(\w+)/; + +/** + * Extracts the `nodeName` of the first element in a string of markup. + * + * @param {string} markup String of markup. + * @return {?string} Node name of the supplied markup. + */ +function getNodeName(markup) { + var nodeNameMatch = markup.match(nodeNamePattern); + return nodeNameMatch && nodeNameMatch[1].toLowerCase(); +} + +/** + * Creates an array containing the nodes rendered from the supplied markup. The + * optionally supplied `handleScript` function will be invoked once for each + *