Skip to content

Commit 39831aa

Browse files
finnpgoto-bus-stop
authored andcommitted
Allow multiple elements in root with Fragments (#118)
* 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
1 parent fd4aa0d commit 39831aa

File tree

8 files changed

+137
-35
lines changed

8 files changed

+137
-35
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ function onclick (e) {
8080
}
8181
```
8282

83+
### Multiple root elements
84+
85+
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).
86+
87+
```js
88+
var html = require('nanohtml')
89+
90+
var el = html`
91+
<li>Chashu</li>
92+
<li>Nori</li>
93+
`
94+
95+
document.querySelector('ul').appendChild(el)
96+
```
97+
8398
## Static optimizations
8499
Parsing HTML has significant overhead. Being able to parse HTML statically,
85100
ahead of time can speed up rendering to be about twice as fast.

lib/babel.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ module.exports = (babel) => {
102102
[t.stringLiteral(text)]
103103
)
104104

105+
/**
106+
* Returns a node that creates a fragment.
107+
*/
108+
const createFragment = (text) =>
109+
t.callExpression(
110+
t.memberExpression(t.identifier('document'), t.identifier('createDocumentFragment')),
111+
[]
112+
)
113+
105114
/**
106115
* Returns a node that sets a DOM property.
107116
*/
@@ -185,8 +194,10 @@ module.exports = (babel) => {
185194
const expressions = path.node.expressions
186195
const expressionPlaceholders = expressions.map((expr, i) => getPlaceholder(i))
187196

188-
const root = hyperx(transform, { comments: true }).apply(null,
189-
[quasis].concat(expressionPlaceholders))
197+
const root = hyperx(transform, {
198+
comments: true,
199+
createFragment: children => transform('nanohtml-fragment', {}, children)
200+
}).apply(null, [quasis].concat(expressionPlaceholders))
190201

191202
/**
192203
* Convert placeholders used in the template string back to the AST nodes
@@ -219,22 +230,26 @@ module.exports = (babel) => {
219230

220231
const result = []
221232

222-
var isCustomElement = props.is
223-
delete props.is
233+
if (tag === 'nanohtml-fragment') {
234+
result.push(t.assignmentExpression('=', id, createFragment()))
235+
} else {
236+
var isCustomElement = props.is
237+
delete props.is
224238

225-
// Use the SVG namespace for svg elements.
226-
if (SVG_TAGS.includes(tag)) {
227-
state.svgNamespaceId.used = true
239+
// Use the SVG namespace for svg elements.
240+
if (SVG_TAGS.includes(tag)) {
241+
state.svgNamespaceId.used = true
228242

229-
if (isCustomElement) {
230-
result.push(t.assignmentExpression('=', id, createNsCustomBuiltIn(state.svgNamespaceId, tag, isCustomElement)))
243+
if (isCustomElement) {
244+
result.push(t.assignmentExpression('=', id, createNsCustomBuiltIn(state.svgNamespaceId, tag, isCustomElement)))
245+
} else {
246+
result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag)))
247+
}
248+
} else if (isCustomElement) {
249+
result.push(t.assignmentExpression('=', id, createCustomBuiltIn(tag, isCustomElement)))
231250
} else {
232-
result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag)))
251+
result.push(t.assignmentExpression('=', id, createElement(tag)))
233252
}
234-
} else if (isCustomElement) {
235-
result.push(t.assignmentExpression('=', id, createCustomBuiltIn(tag, isCustomElement)))
236-
} else {
237-
result.push(t.assignmentExpression('=', id, createElement(tag)))
238253
}
239254

240255
Object.keys(props).forEach((propName) => {

lib/browser.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ function nanoHtmlCreateElement (tag, props, children) {
8989
return el
9090
}
9191

92-
module.exports = hyperx(nanoHtmlCreateElement, {comments: true})
92+
function createFragment (nodes) {
93+
var fragment = document.createDocumentFragment()
94+
for (var i = 0; i < nodes.length; i++) {
95+
if (typeof nodes[i] === 'string') nodes[i] = document.createTextNode(nodes[i])
96+
fragment.appendChild(nodes[i])
97+
}
98+
return fragment
99+
}
100+
101+
module.exports = hyperx(nanoHtmlCreateElement, {
102+
comments: true,
103+
createFragment: createFragment
104+
})
93105
module.exports.default = module.exports
94106
module.exports.createElement = nanoHtmlCreateElement

lib/browserify-transform.js

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ function processNode (node, args) {
124124
var needsAc = false
125125
var needsSa = false
126126

127-
var hx = hyperx(function (tag, props, children) {
127+
function createElement (tag, props, children) {
128128
var res = []
129129

130130
var elname = VARNAME + tagCount
@@ -134,27 +134,31 @@ function processNode (node, args) {
134134
return DELIM + [elname, 'var ' + elname + ' = document.createComment(' + JSON.stringify(props.comment) + ')', null].join(DELIM) + DELIM
135135
}
136136

137-
// Whether this element needs a namespace
138-
var namespace = props.namespace
139-
if (!namespace && SVG_TAGS.indexOf(tag) !== -1) {
140-
namespace = SVGNS
141-
}
137+
if (tag === 'nanohtml-fragment') {
138+
res.push('var ' + elname + ' = document.createDocumentFragment()')
139+
} else {
140+
// Whether this element needs a namespace
141+
var namespace = props.namespace
142+
if (!namespace && SVG_TAGS.indexOf(tag) !== -1) {
143+
namespace = SVGNS
144+
}
142145

143-
// Whether this element is extended
144-
var isCustomElement = props.is
145-
delete props.is
146+
// Whether this element is extended
147+
var isCustomElement = props.is
148+
delete props.is
146149

147-
// Create the element
148-
if (namespace) {
149-
if (isCustomElement) {
150-
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })')
150+
// Create the element
151+
if (namespace) {
152+
if (isCustomElement) {
153+
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })')
154+
} else {
155+
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')')
156+
}
157+
} else if (isCustomElement) {
158+
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })')
151159
} else {
152-
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')')
160+
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')')
153161
}
154-
} else if (isCustomElement) {
155-
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })')
156-
} else {
157-
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')')
158162
}
159163

160164
function addAttr (to, key, val) {
@@ -272,8 +276,16 @@ function processNode (node, args) {
272276
}
273277

274278
// Return delim'd parts as a child
279+
// return [elname, res]
275280
return DELIM + [elname, res.join('\n'), null].join(DELIM) + DELIM
276-
}, { comments: true })
281+
}
282+
283+
var hx = hyperx(createElement, {
284+
comments: true,
285+
createFragment: function (nodes) {
286+
return createElement('nanohtml-fragment', {}, nodes)
287+
}
288+
})
277289

278290
// Run through hyperx
279291
var res = hx.apply(null, args)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"camel-case": "^3.0.0",
2727
"convert-source-map": "^1.5.1",
2828
"estree-is-member-expression": "^1.0.0",
29-
"hyperx": "^2.3.2",
29+
"hyperx": "^2.5.0",
3030
"is-boolean-attribute": "0.0.1",
3131
"nanoassert": "^1.1.0",
3232
"nanobench": "^2.1.0",

tests/browser/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ require('./api.js')
22
require('./elements.js')
33
require('./raw.js')
44
require('./events.js')
5+
require('./multiple.js')

tests/browser/multiple.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
var test = require('tape')
2+
var html = require('../../')
3+
4+
test('multiple elements', function (t) {
5+
var multiple = html`<li>Hamburg</li><li>Helsinki</li>haha<li>Berlin<div>test</div></li>`
6+
7+
var list = document.createElement('ul')
8+
list.appendChild(multiple)
9+
t.equal(list.children.length, 3, '3 children')
10+
t.equal(list.childNodes.length, 4, '4 childNodes')
11+
t.equal(list.children[0].tagName, 'LI', 'list tag name')
12+
t.equal(list.children[0].textContent, 'Hamburg')
13+
t.equal(list.children[1].textContent, 'Helsinki')
14+
t.equal(list.children[2].textContent, 'Berlintest')
15+
t.equal(list.querySelector('div').textContent, 'test', 'created sub-element')
16+
t.equal(list.childNodes[2].nodeValue, 'haha')
17+
t.end()
18+
})
19+
20+
test('nested fragments', function (t) {
21+
var fragments = html`<div>1</div>ab${html`cd<div>2</div>between<div>3</div>`}<div>4</div>`
22+
t.equals(fragments.textContent, '1abcd2between34')
23+
t.equals(fragments.children.length, 4)
24+
t.equals(fragments.childNodes[4].textContent, 'between')
25+
t.equals(fragments.childNodes.length, 7)
26+
t.end()
27+
})

tests/server/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,23 @@ test('spread attributes', function (t) {
8686
t.equal(result, expected)
8787
t.end()
8888
})
89+
90+
test('multiple root elements', function (t) {
91+
t.plan(1)
92+
93+
var expected = '<div>1</div><div>2</div>3<div>5</div>'
94+
var result = html`<div>1</div><div>2</div>3<div>5</div>`.toString()
95+
96+
t.equal(expected, result)
97+
t.end()
98+
})
99+
100+
test('nested multiple root elements', function (t) {
101+
t.plan(1)
102+
103+
var expected = '<div>1</div><div>2</div><div>3</div><div>4</div>'
104+
var result = html`<div>1</div>${html`<div>2</div><div>3</div>`}<div>4</div>`.toString()
105+
106+
t.equal(expected, result)
107+
t.end()
108+
})

0 commit comments

Comments
 (0)