Skip to content

Commit

Permalink
Allow multiple elements in root with Fragments (#118)
Browse files Browse the repository at this point in the history
* Use hyperx with createFragment support

* Add tests for multiple elements

* Add fragments to lib/browser

* Add fragments to lib/browserify-transform

* Add fragments to lib/babel

* Messed up during rebase

* Add docs for DocumentFragments
  • Loading branch information
finnp authored and goto-bus-stop committed Dec 13, 2018
1 parent fd4aa0d commit 39831aa
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 35 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<li>Chashu</li>
<li>Nori</li>
`

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.
Expand Down
43 changes: 29 additions & 14 deletions lib/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
14 changes: 13 additions & 1 deletion lib/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 31 additions & 19 deletions lib/browserify-transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions tests/browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ require('./api.js')
require('./elements.js')
require('./raw.js')
require('./events.js')
require('./multiple.js')
27 changes: 27 additions & 0 deletions tests/browser/multiple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var test = require('tape')
var html = require('../../')

test('multiple elements', function (t) {
var multiple = html`<li>Hamburg</li><li>Helsinki</li>haha<li>Berlin<div>test</div></li>`

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`<div>1</div>ab${html`cd<div>2</div>between<div>3</div>`}<div>4</div>`
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()
})
20 changes: 20 additions & 0 deletions tests/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<div>1</div><div>2</div>3<div>5</div>'
var result = html`<div>1</div><div>2</div>3<div>5</div>`.toString()

t.equal(expected, result)
t.end()
})

test('nested multiple root elements', function (t) {
t.plan(1)

var expected = '<div>1</div><div>2</div><div>3</div><div>4</div>'
var result = html`<div>1</div>${html`<div>2</div><div>3</div>`}<div>4</div>`.toString()

t.equal(expected, result)
t.end()
})

0 comments on commit 39831aa

Please sign in to comment.