diff --git a/lib/babel.js b/lib/babel.js index 9871c39..293f534 100644 --- a/lib/babel.js +++ b/lib/babel.js @@ -49,6 +49,12 @@ module.exports = (babel) => { const t = babel.types const nanohtmlModuleNames = ['nanohtml', 'bel', 'yo-yo', 'choo/html'] + /** + * Returns an object which specifies the custom elements by which a built-in is extended. + */ + const createExtendsObjectExpression = (is) => + t.objectExpression([t.objectProperty(t.identifier('is'), t.stringLiteral(is))]) + /** * Returns a node that creates a namespaced HTML element. */ @@ -58,6 +64,15 @@ module.exports = (babel) => { [ns, t.stringLiteral(tag)] ) + /** + * Returns a node that creates a extended namespaced HTML element. + */ + const createNsCustomBuiltIn = (ns, tag, is) => + t.callExpression( + t.memberExpression(t.identifier('document'), t.identifier('createElementNS')), + [ns, t.stringLiteral(tag), createExtendsObjectExpression(is)] + ) + /** * Returns a node that creates an element. */ @@ -67,6 +82,15 @@ module.exports = (babel) => { [t.stringLiteral(tag)] ) + /** + * Returns a node that creates an extended element. + */ + const createCustomBuiltIn = (tag, is) => + t.callExpression( + t.memberExpression(t.identifier('document'), t.identifier('createElement')), + [t.stringLiteral(tag), createExtendsObjectExpression(is)] + ) + /** * Returns a node that creates a comment. */ @@ -188,10 +212,20 @@ module.exports = (babel) => { const result = [] + var isCustomElement = props.is + delete props.is + // Use the SVG namespace for svg elements. if (SVG_TAGS.includes(tag)) { state.svgNamespaceId.used = true - result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag))) + + 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, createElement(tag))) } diff --git a/lib/browser.js b/lib/browser.js index 0bbf1d0..a6c69d0 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -23,11 +23,24 @@ function nanoHtmlCreateElement (tag, props, children) { delete props.namespace } + // If we are extending a builtin element + var isCustomElement = false + if (props.is) { + isCustomElement = props.is + delete props.is + } + // Create the element if (ns) { - el = document.createElementNS(ns, tag) + if (isCustomElement) { + el = document.createElementNS(ns, tag, { is: isCustomElement }) + } else { + el = document.createElementNS(ns, tag) + } } else if (tag === COMMENT_TAG) { return document.createComment(props.comment) + } else if (isCustomElement) { + el = document.createElement(tag, { is: isCustomElement }) } else { el = document.createElement(tag) } diff --git a/lib/browserify-transform.js b/lib/browserify-transform.js index 0b3af48..4c2f4af 100644 --- a/lib/browserify-transform.js +++ b/lib/browserify-transform.js @@ -119,9 +119,19 @@ function processNode (node, args) { namespace = SVGNS } + // Whether this element is extended + var isCustomElement = props.is + delete props.is + // Create the element if (namespace) { - res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')') + 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.createElement(' + JSON.stringify(tag) + ')') } diff --git a/tests/babel/fixtures/custom-build-in.expected.js b/tests/babel/fixtures/custom-build-in.expected.js new file mode 100644 index 0000000..ab168c8 --- /dev/null +++ b/tests/babel/fixtures/custom-build-in.expected.js @@ -0,0 +1,6 @@ +var _div, + _appendChild = require('nanohtml/lib/append-child'); + +_div = document.createElement('div', { + is: 'my-div' +}), _appendChild(_div, ['\n Hello world\n ']), _div; \ No newline at end of file diff --git a/tests/babel/fixtures/custom-build-in.js b/tests/babel/fixtures/custom-build-in.js new file mode 100644 index 0000000..7c86685 --- /dev/null +++ b/tests/babel/fixtures/custom-build-in.js @@ -0,0 +1,7 @@ +import html from 'nanohtml' + +html` +
+ Hello world +
+` diff --git a/tests/babel/index.js b/tests/babel/index.js index 7439a1a..088ef60 100644 --- a/tests/babel/index.js +++ b/tests/babel/index.js @@ -37,6 +37,7 @@ function testFixture (name, opts) { } testFixture('simple') +testFixture('custom-build-in') testFixture('empty') testFixture('this') testFixture('variableNames') diff --git a/tests/browser/elements.js b/tests/browser/elements.js index 8b6ec4b..74a32e9 100644 --- a/tests/browser/elements.js +++ b/tests/browser/elements.js @@ -180,3 +180,27 @@ test('allow objects to be passed', function (t) { t.ok(result.outerHTML.indexOf('
hey
') !== -1, 'contains foo="bar"') t.end() }) + +test('supports extended build-in elements', function (t) { + t.plan(1) + + var originalCreateElement = document.createElement + var optionsArg + + // this iife is a must to avoid illegal invocation type errors, caused by transformed nanohtml tests + (function () { + document.createElement = function () { + optionsArg = arguments[1] + return originalCreateElement.apply(this, arguments) + } + })() + + ;html`
` + + t.ok(typeof optionsArg === 'object' && optionsArg.is === 'my-div', 'properly passes optional extends object') + + // revert to original prototype method + delete document.createElement + + t.end() +}) diff --git a/tests/transform/index.js b/tests/transform/index.js index 7284d10..c775c41 100644 --- a/tests/transform/index.js +++ b/tests/transform/index.js @@ -6,8 +6,8 @@ var path = require('path') var FIXTURE = path.join(__dirname, 'fixture.js') test('works', function (t) { - t.plan(4) - var src = 'var html = require(\'nanohtml\')\n module.exports = function (data) {\n var className = \'test\'\n return html`
\n

${data}

\n
`\n }' // eslint-disable-line + t.plan(5) + var src = 'var html = require(\'nanohtml\')\n module.exports = function (data) {\n var className = \'test\'\n return html`
\n

${data}

\n
`\n }' // eslint-disable-line fs.writeFileSync(FIXTURE, src) var b = browserify(FIXTURE, { browserField: false, @@ -19,6 +19,7 @@ test('works', function (t) { var result = src.toString() t.ok(result.indexOf('var html = {}') !== -1, 'replaced html dependency with {}') t.ok(result.indexOf('document.createElement("h1")') !== -1, 'created an h1 tag') + t.ok(result.indexOf('document.createElement("div", { is: "my-div" })') !== -1, 'created an extended build-in element') t.ok(result.indexOf('setAttribute("class", arguments[1])') !== -1, 'set a class attribute') t.end() })