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

Support for custom elements #121

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
419 changes: 77 additions & 342 deletions lib/babel.js

Large diffs are not rendered by default.

77 changes: 2 additions & 75 deletions lib/browser.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,6 @@
var hyperx = require('hyperx')
var appendChild = require('./append-child')
var SVG_TAGS = require('./svg-tags')
var BOOL_PROPS = require('./bool-props')
var nanoHtmlCreateElement = require('./createElement')

var SVGNS = 'http://www.w3.org/2000/svg'
var XLINKNS = 'http://www.w3.org/1999/xlink'

var COMMENT_TAG = '!--'

function nanoHtmlCreateElement (tag, props, children) {
var el

// If an svg tag, it needs a namespace
if (SVG_TAGS.indexOf(tag) !== -1) {
props.namespace = SVGNS
}

// If we are using a namespace
var ns = false
if (props.namespace) {
ns = props.namespace
delete props.namespace
}

// Create the element
if (ns) {
el = document.createElementNS(ns, tag)
} else if (tag === COMMENT_TAG) {
return document.createComment(props.comment)
} else {
el = document.createElement(tag)
}

// Create the properties
for (var p in props) {
if (props.hasOwnProperty(p)) {
var key = p.toLowerCase()
var val = props[p]
// Normalize className
if (key === 'classname') {
key = 'class'
p = 'class'
}
// The for attribute gets transformed to htmlFor, but we just set as for
if (p === 'htmlFor') {
p = 'for'
}
// If a property is boolean, set itself to the key
if (BOOL_PROPS.indexOf(key) !== -1) {
if (val === 'true') val = key
else if (val === 'false') continue
}
// If a property prefers being set directly vs setAttribute
if (key.slice(0, 2) === 'on') {
el[p] = val
} else {
if (ns) {
if (p === 'xlink:href') {
el.setAttributeNS(XLINKNS, p, val)
} else if (/^xmlns($|:)/i.test(p)) {
// skip xmlns definitions
} else {
el.setAttributeNS(null, p, val)
}
} else {
el.setAttribute(p, val)
}
}
}
}

appendChild(el, children)
return el
}

module.exports = hyperx(nanoHtmlCreateElement, {comments: true})
module.exports = hyperx(nanoHtmlCreateElement, { comments: true })
module.exports.default = module.exports
module.exports.createElement = nanoHtmlCreateElement
245 changes: 41 additions & 204 deletions lib/browserify-transform.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
var convertSourceMap = require('convert-source-map')
var transformAst = require('transform-ast')
var through = require('through2')
var hyperx = require('hyperx')
var acorn = require('acorn')
var path = require('path')
var SVG_TAGS = require('./svg-tags')

var SUPPORTED_VIEWS = ['nanohtml', 'bel', 'yo-yo', 'choo', 'choo/html']
var DELIM = '~!@|@|@!~'
var VARNAME = 'nanohtml'
var SVGNS = 'http://www.w3.org/2000/svg'
var XLINKNS = '"http://www.w3.org/1999/xlink"'
var BOOL_PROPS = require('./bool-props').reduce(function (o, key) {
o[key] = 1
return o
}, {})
var transform = require('./transform')

module.exports = function yoYoify (file, opts) {
var SUPPORTED_VIEWS = ['nanohtml', 'bel', 'yo-yo', 'choo/html']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good, choo.view is long gone afaik 👍


module.exports = function (file, opts) {
if (/\.json$/.test(file)) return through()
var bufs = []
var viewVariables = []
var babelTemplateObjects = Object.create(null)
return through(write, end)

function write (buf, enc, next) {
bufs.push(buf)
next()
}

function end (cb) {
var src = Buffer.concat(bufs).toString('utf8')
var res
Expand All @@ -42,13 +36,16 @@ module.exports = function yoYoify (file, opts) {
this.push(res)
this.push(null)
}

function walk (node) {
var res

if (isSupportedView(node)) {
if (node.arguments[0].value === 'bel' ||
node.arguments[0].value === 'choo/html' ||
node.arguments[0].value === 'nanohtml') {
// html and choo/html have no other exports that may be used
node.edit.update('{}')
node.edit.update('{ createElement: require("' + path.join(node.arguments[0].value, '/lib/createElement") }'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to just do update('require("/path/to/createElement")') here and call that function directly as html() or createElement() (not renaming the variable is probably easier) instead of doing html.createElement?

choo/html is not going to have an export named choo/html/lib/createElement, and bel didn't have one either. maybe this should use require.resolve('./createElement') instead? or path.relative() from the input file

Copy link
Author

@diffcunha diffcunha Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was my first idea, but then I thought it wouldn't be a good idea to "reuse" an already existent api. I mean, one might want to:

var html = require('nanohtml')
var foo = function (strings, ...keys) {
  // do stuff
  return html(strings, ...keys)
}
var bar = html`<span>HELLO</span>`

You are absolutely right about bel and choo/html. Regarding choo/html, I could still replace with nanohtml/lib/createElement or something like it. Regarding bel, I'm not so sure but probably it won't be supported in this version on nanohtml given that it doesn't expose a createElement function

}
if (node.parent.type === 'VariableDeclarator') {
viewVariables.push(node.parent.id.name)
Expand All @@ -67,7 +64,8 @@ module.exports = function yoYoify (file, opts) {
if (node.type === 'TemplateLiteral' && node.parent.tag) {
var name = node.parent.tag.name || (node.parent.tag.object && node.parent.tag.object.name)
if (viewVariables.indexOf(name) !== -1) {
processNode(node.parent, [ node.quasis.map(cooked) ].concat(node.expressions.map(expr)))
res = apply(name, [ node.quasis.map(cooked) ].concat(node.expressions.map(expr)))
node.parent.update(res)
}
}
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && viewVariables.indexOf(node.callee.name) !== -1) {
Expand All @@ -78,15 +76,17 @@ module.exports = function yoYoify (file, opts) {
// Emitted by Buble.
template = node.arguments[0].elements.map(function (part) { return part.value })
expressions = node.arguments.slice(1).map(expr)
processNode(node, [ template ].concat(expressions))
res = apply(node.callee.name, [ template ].concat(expressions))
node.update(res)
} else if (node.arguments[0] && node.arguments[0].type === 'Identifier') {
// Detect transpiled template strings like:
// html(_templateObject, {id: "test"})
// Emitted by Babel.
var templateObject = babelTemplateObjects[node.arguments[0].name]
template = templateObject.elements.map(function (part) { return part.value })
expressions = node.arguments.slice(1).map(expr)
processNode(node, [ template ].concat(expressions))
res = apply(node.callee.name, [ template ].concat(expressions))
node.update(res)

// Remove the _taggedTemplateLiteral helper call
templateObject.parent.edit.update('0')
Expand All @@ -95,166 +95,31 @@ module.exports = function yoYoify (file, opts) {
}
}

function processNode (node, args) {
var resultArgs = []
var argCount = 0
var tagCount = 0

var needsAc = false
var needsSa = false

var hx = hyperx(function (tag, props, children) {
var res = []

var elname = VARNAME + tagCount
tagCount++

if (tag === '!--') {
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
}

// Create the element
if (namespace) {
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')')
} else {
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')')
}

function addAttr (to, key, val) {
// Normalize className
if (key.toLowerCase() === '"classname"') {
key = '"class"'
}
// The for attribute gets transformed to htmlFor, but we just set as for
if (key === '"htmlFor"') {
key = '"for"'
}
// If a property is boolean, set itself to the key
if (BOOL_PROPS[key.slice(1, -1)]) {
if (val.slice(0, 9) === 'arguments') {
if (namespace) {
res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + key + ')')
} else {
res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttribute(' + key + ', ' + key + ')')
}
return
} else {
if (val === 'true') val = key
else if (val === 'false') return
}
}
if (key.slice(1, 3) === 'on') {
res.push(to + '[' + key + '] = ' + val)
} else {
if (key === '"xlink:href"') {
res.push(to + '.setAttributeNS(' + XLINKNS + ', ' + key + ', ' + val + ')')
} else if (namespace && key.slice(0, 1) === '"') {
if (!/^xmlns($|:)/i.test(key.slice(1, -1))) {
// skip xmlns definitions
res.push(to + '.setAttributeNS(null, ' + key + ', ' + val + ')')
}
} else if (namespace) {
res.push('if (' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + val + ')')
} else if (key.slice(0, 1) === '"') {
res.push(to + '.setAttribute(' + key + ', ' + val + ')')
} else {
needsSa = true
res.push('sa(' + to + ', ' + key + ', ' + val + ')')
}
}
}

// Add properties to element
Object.keys(props).forEach(function (key) {
var prop = props[key]
var ksrcs = getSourceParts(key)
var srcs = getSourceParts(prop)
var k, val
if (srcs) {
val = ''
srcs.forEach(function (src, index) {
if (src.arg) {
if (index > 0) val += ' + '
if (src.before) val += JSON.stringify(src.before) + ' + '
val += 'arguments[' + argCount + ']'
if (src.after) val += ' + ' + JSON.stringify(src.after)
resultArgs.push(src.arg)
argCount++
}
})
} else {
val = JSON.stringify(prop)
}
if (ksrcs) {
k = ''
ksrcs.forEach(function (src, index) {
if (src.arg) {
if (index > 0) val += ' + '
if (src.before) val += JSON.stringify(src.before) + ' + '
k += 'arguments[' + argCount + ']'
if (src.after) k += ' + ' + JSON.stringify(src.after)
resultArgs.push(src.arg)
argCount++
}
})
} else {
k = JSON.stringify(key)
}
addAttr(elname, k, val)
})

if (Array.isArray(children)) {
var childs = []
children.forEach(function (child) {
var srcs = getSourceParts(child)
if (srcs) {
var src = srcs[0]
if (src.src) {
res.push(src.src)
}
if (src.name) {
childs.push(src.name)
}
if (src.arg) {
var argname = 'arguments[' + argCount + ']'
resultArgs.push(src.arg)
argCount++
childs.push(argname)
}
} else {
childs.push(JSON.stringify(child))
}
})
if (childs.length > 0) {
needsAc = true
res.push('ac(' + elname + ', [' + childs.join(',') + '])')
}
}

// Return delim'd parts as a child
return DELIM + [elname, res.join('\n'), null].join(DELIM) + DELIM
}, { comments: true })

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

// Pull out the final parts and wrap in a closure with arguments
var src = getSourceParts(res)
if (src && src[0].src) {
var params = resultArgs.join(',')

node.edit.update('(function () {' +
(needsAc ? '\n var ac = require(\'' + path.resolve(__dirname, 'append-child.js').replace(/\\/g, '\\\\') + '\')' : '') +
(needsSa ? '\n var sa = require(\'' + path.resolve(__dirname, 'set-attribute.js').replace(/\\/g, '\\\\') + '\')' : '') +
'\n ' + src[0].src + '\n return ' + src[0].name + '\n }(' + params + '))')
var apply = transform.factory({
arrayExpression: function (elements) {
return '[' + elements.join(',') + ']'
},
objectExpression: function (properties) {
return '{' + properties.join(',') + '}'
},
objectProperty: function (key, value, computed) {
return computed
? ('[' + key + ']' + ':' + value)
: (key + ':' + value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

choo aims to work in IE 11 + so we can't use ES6+ syntax in the transform output

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, will fix that

},
stringLiteral: function (value) {
return JSON.stringify(value)
},
callCreateElement (html, tag, props, children) {
return html + '.createElement(' + tag + ',' + props + ',' + children + ')'
},
callObjectAssign (objects) {
return 'Object.assign(' + objects.join(',') + ')'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.assign also doesn't exist yet in IE11 … choo uses the xtend module, that might be good (deduped = no extra bytes 🎉 ). maybe xtend can be exposed as nanohtml/lib/merge or so, and that can be required in the output, that way we don't add a peer dependency.

IE 11 will probably be dropped in choo 7 but not yet: choojs/choo#616

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, will use xtend instead

},
stringConcat: function (a, b) {
return a + '+' + b
}
}
})

function isSupportedView (node) {
return (node.type === 'CallExpression' &&
Expand All @@ -269,32 +134,4 @@ function BabelTemplateDefinition (node) {
}

function cooked (node) { return node.value.cooked }
function expr (ex, idx) {
return DELIM + [null, null, ex.source()].join(DELIM) + DELIM
}
function getSourceParts (str) {
if (typeof str !== 'string') return false
if (str.indexOf(DELIM) === -1) return false
var parts = str.split(DELIM)

var chunk = parts.splice(0, 5)
var arr = [{
before: chunk[0],
name: chunk[1],
src: chunk[2],
arg: chunk[3],
after: chunk[4]
}]
while (parts.length > 0) {
chunk = parts.splice(0, 4)
arr.push({
before: '',
name: chunk[0],
src: chunk[1],
arg: chunk[2],
after: chunk[3]
})
}

return arr
}
function expr (ex) { return transform.expr(ex.source()) }