diff --git a/README.md b/README.md index f2c9705..0132d1c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,21 @@ function onclick (e) { } ``` +### Multiple root elements + +If you have more than one root element they will be combined with a [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment). + +```js +var html = require('nanohtml') + +var el = html` +
  • Chashu
  • +
  • Nori
  • +` + +document.querySelector('ul').appendChild(el) +``` + ## Static optimizations Parsing HTML has significant overhead. Being able to parse HTML statically, ahead of time can speed up rendering to be about twice as fast. diff --git a/lib/babel.js b/lib/babel.js index c4a217e..9c44105 100644 --- a/lib/babel.js +++ b/lib/babel.js @@ -102,6 +102,15 @@ module.exports = (babel) => { [t.stringLiteral(text)] ) + /** + * Returns a node that creates a fragment. + */ + const createFragment = (text) => + t.callExpression( + t.memberExpression(t.identifier('document'), t.identifier('createDocumentFragment')), + [] + ) + /** * Returns a node that sets a DOM property. */ @@ -185,8 +194,10 @@ module.exports = (babel) => { const expressions = path.node.expressions const expressionPlaceholders = expressions.map((expr, i) => getPlaceholder(i)) - const root = hyperx(transform, { comments: true }).apply(null, - [quasis].concat(expressionPlaceholders)) + const root = hyperx(transform, { + comments: true, + createFragment: children => transform('nanohtml-fragment', {}, children) + }).apply(null, [quasis].concat(expressionPlaceholders)) /** * Convert placeholders used in the template string back to the AST nodes @@ -219,22 +230,26 @@ module.exports = (babel) => { const result = [] - var isCustomElement = props.is - delete props.is + if (tag === 'nanohtml-fragment') { + result.push(t.assignmentExpression('=', id, createFragment())) + } else { + var isCustomElement = props.is + delete props.is - // Use the SVG namespace for svg elements. - if (SVG_TAGS.includes(tag)) { - state.svgNamespaceId.used = true + // Use the SVG namespace for svg elements. + if (SVG_TAGS.includes(tag)) { + state.svgNamespaceId.used = true - if (isCustomElement) { - result.push(t.assignmentExpression('=', id, createNsCustomBuiltIn(state.svgNamespaceId, tag, isCustomElement))) + if (isCustomElement) { + result.push(t.assignmentExpression('=', id, createNsCustomBuiltIn(state.svgNamespaceId, tag, isCustomElement))) + } else { + result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag))) + } + } else if (isCustomElement) { + result.push(t.assignmentExpression('=', id, createCustomBuiltIn(tag, isCustomElement))) } else { - result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag))) + result.push(t.assignmentExpression('=', id, createElement(tag))) } - } else if (isCustomElement) { - result.push(t.assignmentExpression('=', id, createCustomBuiltIn(tag, isCustomElement))) - } else { - result.push(t.assignmentExpression('=', id, createElement(tag))) } Object.keys(props).forEach((propName) => { diff --git a/lib/browser.js b/lib/browser.js index f9b34f1..3960de5 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -89,6 +89,18 @@ function nanoHtmlCreateElement (tag, props, children) { return el } -module.exports = hyperx(nanoHtmlCreateElement, {comments: true}) +function createFragment (nodes) { + var fragment = document.createDocumentFragment() + for (var i = 0; i < nodes.length; i++) { + if (typeof nodes[i] === 'string') nodes[i] = document.createTextNode(nodes[i]) + fragment.appendChild(nodes[i]) + } + return fragment +} + +module.exports = hyperx(nanoHtmlCreateElement, { + comments: true, + createFragment: createFragment +}) module.exports.default = module.exports module.exports.createElement = nanoHtmlCreateElement diff --git a/lib/browserify-transform.js b/lib/browserify-transform.js index 3990855..7602553 100644 --- a/lib/browserify-transform.js +++ b/lib/browserify-transform.js @@ -124,7 +124,7 @@ function processNode (node, args) { var needsAc = false var needsSa = false - var hx = hyperx(function (tag, props, children) { + function createElement (tag, props, children) { var res = [] var elname = VARNAME + tagCount @@ -134,27 +134,31 @@ function processNode (node, args) { return DELIM + [elname, 'var ' + elname + ' = document.createComment(' + JSON.stringify(props.comment) + ')', null].join(DELIM) + DELIM } - // Whether this element needs a namespace - var namespace = props.namespace - if (!namespace && SVG_TAGS.indexOf(tag) !== -1) { - namespace = SVGNS - } + if (tag === 'nanohtml-fragment') { + res.push('var ' + elname + ' = document.createDocumentFragment()') + } else { + // Whether this element needs a namespace + var namespace = props.namespace + if (!namespace && SVG_TAGS.indexOf(tag) !== -1) { + namespace = SVGNS + } - // Whether this element is extended - var isCustomElement = props.is - delete props.is + // Whether this element is extended + var isCustomElement = props.is + delete props.is - // Create the element - if (namespace) { - if (isCustomElement) { - res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') + // Create the element + if (namespace) { + if (isCustomElement) { + res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') + } else { + res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')') + } + } else if (isCustomElement) { + res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') } else { - res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')') + res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')') } - } else if (isCustomElement) { - res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') - } else { - res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')') } function addAttr (to, key, val) { @@ -272,8 +276,16 @@ function processNode (node, args) { } // Return delim'd parts as a child + // return [elname, res] return DELIM + [elname, res.join('\n'), null].join(DELIM) + DELIM - }, { comments: true }) + } + + var hx = hyperx(createElement, { + comments: true, + createFragment: function (nodes) { + return createElement('nanohtml-fragment', {}, nodes) + } + }) // Run through hyperx var res = hx.apply(null, args) diff --git a/package.json b/package.json index 2c1f703..856a7cc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "camel-case": "^3.0.0", "convert-source-map": "^1.5.1", "estree-is-member-expression": "^1.0.0", - "hyperx": "^2.3.2", + "hyperx": "^2.5.0", "is-boolean-attribute": "0.0.1", "nanoassert": "^1.1.0", "nanobench": "^2.1.0", diff --git a/tests/browser/index.js b/tests/browser/index.js index 6cb2f2b..422f7dd 100644 --- a/tests/browser/index.js +++ b/tests/browser/index.js @@ -2,3 +2,4 @@ require('./api.js') require('./elements.js') require('./raw.js') require('./events.js') +require('./multiple.js') diff --git a/tests/browser/multiple.js b/tests/browser/multiple.js new file mode 100644 index 0000000..603662c --- /dev/null +++ b/tests/browser/multiple.js @@ -0,0 +1,27 @@ +var test = require('tape') +var html = require('../../') + +test('multiple elements', function (t) { + var multiple = html`
  • Hamburg
  • Helsinki
  • haha
  • Berlin
    test
  • ` + + var list = document.createElement('ul') + list.appendChild(multiple) + t.equal(list.children.length, 3, '3 children') + t.equal(list.childNodes.length, 4, '4 childNodes') + t.equal(list.children[0].tagName, 'LI', 'list tag name') + t.equal(list.children[0].textContent, 'Hamburg') + t.equal(list.children[1].textContent, 'Helsinki') + t.equal(list.children[2].textContent, 'Berlintest') + t.equal(list.querySelector('div').textContent, 'test', 'created sub-element') + t.equal(list.childNodes[2].nodeValue, 'haha') + t.end() +}) + +test('nested fragments', function (t) { + var fragments = html`
    1
    ab${html`cd
    2
    between
    3
    `}
    4
    ` + t.equals(fragments.textContent, '1abcd2between34') + t.equals(fragments.children.length, 4) + t.equals(fragments.childNodes[4].textContent, 'between') + t.equals(fragments.childNodes.length, 7) + t.end() +}) diff --git a/tests/server/index.js b/tests/server/index.js index ee31e46..9f80eef 100644 --- a/tests/server/index.js +++ b/tests/server/index.js @@ -86,3 +86,23 @@ test('spread attributes', function (t) { t.equal(result, expected) t.end() }) + +test('multiple root elements', function (t) { + t.plan(1) + + var expected = '
    1
    2
    3
    5
    ' + var result = html`
    1
    2
    3
    5
    `.toString() + + t.equal(expected, result) + t.end() +}) + +test('nested multiple root elements', function (t) { + t.plan(1) + + var expected = '
    1
    2
    3
    4
    ' + var result = html`
    1
    ${html`
    2
    3
    `}
    4
    `.toString() + + t.equal(expected, result) + t.end() +})