Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple elements in root with Fragments #118

Merged
merged 7 commits into from
Dec 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})