├── .npmignore ├── logo.png ├── .gitignore ├── .travis.yml ├── package.json ├── license ├── index.js ├── logo.svg ├── test.js └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | yarn* 3 | coverage -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/queckezz/elementx/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | npm-debug.log* 4 | yarn* 5 | dist.js 6 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | install: 6 | - npm install 7 | - npm install -g codecov 8 | script: 9 | - npm test 10 | - nyc report --reporter=lcov > coverage.lcov && codecov -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elementx", 3 | "version": "3.1.2", 4 | "description": "Create complex DOM elements/trees using a functional approach", 5 | "main": "dist.js", 6 | "license": "MIT", 7 | "repository": "queckezz/elementx", 8 | "author": "queckezz ", 9 | "scripts": { 10 | "precommit": "npm test", 11 | "test": "standard && nyc ava", 12 | "prepublish": "buble index.js > dist.js" 13 | }, 14 | "keywords": [ 15 | "hyperscript", 16 | "dom", 17 | "element", 18 | "node", 19 | "create dom", 20 | "morphdom", 21 | "nanomorph", 22 | "unidirectional", 23 | "virtual-dom" 24 | ], 25 | "dependencies": { 26 | "global-undom": "^0.1.1", 27 | "html-tag-names": "^1.1.0", 28 | "svg-tag-names": "^1.0.0" 29 | }, 30 | "devDependencies": { 31 | "ava": "^0.18.1", 32 | "buble": "^0.15.2", 33 | "husky": "^0.13.1", 34 | "nyc": "^10.1.2", 35 | "serialize-dom": "^3.0.3", 36 | "standard": "^7.0.1", 37 | "tsml": "^1.0.1", 38 | "xyz": "^2.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Fabian Eichenberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const htmlTags = require('html-tag-names') 3 | const document = require('global-undom') 4 | const svgTags = require('svg-tag-names') 5 | 6 | const namespaces = { 7 | ev: 'http://www.w3.org/2001/xml-events', 8 | xlink: 'http://www.w3.org/1999/xlink', 9 | xml: 'http://www.w3.org/XML/1998/namespace', 10 | xmlns: 'http://www.w3.org/2000/xmlns/' 11 | } 12 | 13 | const booleanAttrs = [ 14 | 'defaultchecked', 15 | 'formnovalidate', 16 | 'indeterminate', 17 | 'willvalidate', 18 | 'autofocus', 19 | 'checked', 20 | 'disabled', 21 | 'readonly', 22 | 'required', 23 | 'selected' 24 | ] 25 | 26 | const isEventHandler = (key) => key.slice(0, 2) === 'on' 27 | 28 | const normalizeEventName = (event) => 29 | 'on' + event.slice(2, event.length).toLowerCase() 30 | 31 | const isPlainObject = (obj) => 32 | typeof obj === 'object' && obj.constructor === Object 33 | 34 | const contains = (val, obj) => obj.indexOf(val) !== -1 35 | 36 | const getSvgAttributeNamespace = (attr) => { 37 | const prefix = attr.split(':', 1)[0] 38 | return namespaces.hasOwnProperty(prefix) 39 | ? namespaces[prefix] 40 | : null 41 | } 42 | 43 | const createElementTag = (tagName) => { 44 | return contains(tagName, htmlTags) 45 | ? document.createElement(tagName) 46 | : document.createElementNS('http://www.w3.org/2000/svg', tagName) 47 | } 48 | 49 | const setAttribute = (element, key, value) => { 50 | return contains(':', key) 51 | ? element.setAttributeNS(getSvgAttributeNamespace(key), key, value) 52 | : element.setAttribute(key, value) 53 | } 54 | 55 | const createElement = (tagName, ...args) => { 56 | let attrs 57 | const children = [] 58 | args.forEach((arg) => { 59 | if (!arg) { 60 | return 61 | } else if (!attrs && isPlainObject(arg)) { 62 | attrs = arg 63 | } else if (Array.isArray(arg)) { 64 | children.push(...arg) 65 | } else { 66 | children.push(arg) 67 | } 68 | }) 69 | 70 | const element = createElementTag(tagName) 71 | 72 | for (const key in attrs) { 73 | const value = attrs[key] 74 | 75 | if (isEventHandler(key)) { 76 | element[normalizeEventName(key)] = value 77 | } else if (contains(key, booleanAttrs)) { 78 | value !== false && element.setAttribute(key, key) 79 | } else { 80 | setAttribute(element, key, value) 81 | } 82 | } 83 | 84 | if (children && children.length > 0) { 85 | children.forEach((child) => { 86 | element.appendChild( 87 | typeof child === 'string' 88 | ? document.createTextNode(child) 89 | : child 90 | ) 91 | }) 92 | } 93 | 94 | return element 95 | } 96 | 97 | const createTagFactory = (tag) => { 98 | return (...args) => createElement(tag, ...args) 99 | } 100 | 101 | module.exports = createElement 102 | 103 | svgTags.concat(htmlTags).forEach((tag) => { 104 | module.exports[tag] = createTagFactory(tag) 105 | }) 106 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 48 | 50 | 51 | 53 | image/svg+xml 54 | 56 | 57 | 58 | 59 | 60 | 65 | 68 | elementx 80 | 87 | 94 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | const serialize = require('serialize-dom') 3 | const createElement = require('./') 4 | const tsml = require('tsml') 5 | const test = require('ava') 6 | 7 | test('tag', (t) => { 8 | const actual = createElement('h1') 9 | const expected = '

' 10 | t.is(serialize(actual), expected) 11 | }) 12 | 13 | test('attributes', (t) => { 14 | const actual = createElement('h1', { 15 | id: 'header', 16 | class: 'big' 17 | }) 18 | 19 | const expected = '

' 20 | t.is(serialize(actual), expected) 21 | }) 22 | 23 | test('boolean attributes', (t) => { 24 | const actual = createElement('input', { 25 | type: 'checkbox', 26 | autofocus: true, 27 | checked: false 28 | }) 29 | 30 | const expected = tsml` 31 | 32 | ` 33 | 34 | t.is(serialize(actual), expected) 35 | }) 36 | 37 | test('svg attributes', (t) => { 38 | const node = createElement('use', { 39 | 'xlink:href': '#test' 40 | }) 41 | 42 | t.is(node.attributes[0].ns, 'http://www.w3.org/1999/xlink') 43 | }) 44 | 45 | test('unknown namespace attributes', (t) => { 46 | const node = createElement('use', { 47 | 'randomnamespace:href': '#test' 48 | }) 49 | 50 | t.is(node.attributes[0].ns, null) 51 | }) 52 | 53 | test('children', (t) => { 54 | const actual = createElement('div', 55 | createElement('p'), 56 | createElement('p') 57 | ) 58 | 59 | const expected = tsml` 60 |
61 |

62 |

63 |
64 | ` 65 | 66 | t.is(serialize(actual), expected) 67 | }) 68 | 69 | test('children as a falsy value', (t) => { 70 | const actual = createElement('div', 71 | undefined, 72 | null, 73 | createElement('p', 'hello') 74 | ) 75 | 76 | const expected = tsml` 77 |
78 |

hello

79 |
80 | ` 81 | 82 | t.is(serialize(actual), expected) 83 | }) 84 | 85 | test('children as an array', (t) => { 86 | const actual = createElement('div', [ 87 | createElement('p'), 88 | createElement('p') 89 | ]) 90 | 91 | const expected = tsml` 92 |
93 |

94 |

95 |
96 | ` 97 | 98 | t.is(serialize(actual), expected) 99 | }) 100 | 101 | test('children as an array with attributes', (t) => { 102 | const actual = createElement('div', { 103 | class: 'one' 104 | }, [ 105 | createElement('p'), 106 | createElement('p') 107 | ]) 108 | 109 | const expected = tsml` 110 |
111 |

112 |

113 |
114 | ` 115 | 116 | t.is(serialize(actual), expected) 117 | }) 118 | 119 | test('children as a text node', (t) => { 120 | const actual = createElement('p', 'text') 121 | const expected = '

text

' 122 | t.is(serialize(actual), expected) 123 | }) 124 | 125 | test('children and attributes', (t) => { 126 | const actual = createElement('div', { id: 'wrapper' }, 127 | createElement('p', '1'), 128 | createElement('p', '2') 129 | ) 130 | 131 | const expected = tsml` 132 |
133 |

1

134 |

2

135 |
136 | ` 137 | 138 | t.is(serialize(actual), expected) 139 | }) 140 | 141 | test('tags as functions', (t) => { 142 | const { div, p } = createElement 143 | 144 | const actual = div({ id: 'wrapper' }, 145 | p('1'), 146 | p('2') 147 | ) 148 | 149 | const expected = tsml` 150 |
151 |

1

152 |

2

153 |
154 | ` 155 | 156 | t.is(serialize(actual), expected) 157 | }) 158 | 159 | test.todo('svg tags as functions') 160 | 161 | test('events', (t) => { 162 | const handler = () => 'clicked' 163 | const node = createElement('button', { onClick: handler }) 164 | 165 | t.is(node.onclick, handler) 166 | t.is(node.onclick(), 'clicked') 167 | }) 168 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ![logo](./logo.png) 3 | 4 | > ​:zap:​ Create complex [DOM](https://de.wikipedia.org/wiki/Document_Object_Model) elements/trees using a functional approach. 5 | 6 | [![npm version][version-image]][version-url] 7 | [![build status][travis-image]][travis-url] 8 | [![test coverage][codecov-image]][codecov-url] 9 | [![dependency status][david-image]][david-url] 10 | [![license][license-image]][license-url] 11 | [![js standard style][standard-image]][standard-url] 12 | [![downloads per month][downloads-image]][downloads-url] 13 | 14 | This module provides an alternative to [JSX](https://facebook.github.io/jsx/) or [template strings](https://github.com/shama/bel) for those who want to build up their DOM trees using plain function composition. 15 | 16 | ```js 17 | const { div, h1, h2, button, ul, li } = require('elementx') 18 | 19 | div( 20 | h1({ class: 'bold' }, 'elementx'), 21 | h2({ id: 'subtitle' }, 'Create a DOM tree with ease'), 22 | button({ href: 'http://ghub.io/elementx' }, 'Open'), 23 | ul( 24 | ['simple', 'functional', 'fast'] 25 | .map(key => li(key)) 26 | ) 27 | ) 28 | ``` 29 | 30 | ## Features 31 | 32 | * **Universal** - Works in Node and Browser 33 | * **SVG Support** - Supports creating SVG Elements 34 | * **Functional** - Since it's just function composition we can arbitrarily compose them 35 | * **Small** Only `1.99 kB` minified and gzipped 36 | * **Interoperability** Can be used with diffing libraries like [morphdom](https://github.com/patrick-steele-idem/morphdom), [nanomorph](https://github.com/yoshuawuyts/nanomorph) or anyhting that uses the DOM 37 | 38 | ## Installation 39 | 40 | ```bash 41 | > npm install elementx 42 | ``` 43 | 44 | ## Example 45 | 46 | ```js 47 | const { div, h1, a } = require('elementx') 48 | 49 | const node = div( 50 | h1({ class: 'title' }, 'This is a title'), 51 | div({ class: 'bg-red' }, 52 | a({ href: 'http://github.com' }, 'Github') 53 | ) 54 | ) 55 | 56 | // mount the tree to the DOM 57 | document.body.appendChild(node) 58 | 59 | console.log(tree.outerHTML) 60 | /* 61 | * -> 62 | *
63 | *

This is a title

64 | *
65 | * Github 66 | *
67 | *
68 | */ 69 | ``` 70 | 71 | ## Getting Started 72 | 73 | Each [HTML tag](http://ghub.io/html-tag-names) is exposed as a function when requiring `elementx`. 74 | 75 | ```js 76 | // using destructuring 77 | const { div, h1, p, button } = require('elementx') 78 | ``` 79 | 80 | These functions have the following syntax: 81 | 82 | ```js 83 | tag(tagName, attributes, children) 84 | ``` 85 | 86 | All arguments are **optional** with at least **one argument needing to be present**. This kind of function overloading allows you to iterate on your DOM structure really fast and reduce visual noise. 87 | 88 | * **tagName** any [valid](https://github.com/wooorm/html-tag-names/blob/master/index.json) html tag 89 | * **attributes** is an object of dom attributes: `{ href: '#header' }` 90 | * **children** can be a string for a text node or an array of nodes 91 | 92 | ### Lifecycle hooks 93 | 94 | This module aims to be just the element creation layer. It can be used with any view framework using DOM as their base element abstraction for diffing. Some libraries like this include [choo](https://github.com/yoshuawuyts/choo) or [inu](https://github.com/ahdinosaur/inu). 95 | 96 | ### SVG 97 | 98 | SVG works as expected. Sets the appropriate namespace for both elements and attributes. All SVG tags can only be created with the `h`-helper: 99 | 100 | ```js 101 | const { svg } = require('elementx') 102 | 103 | const node = svg({ 104 | viewBox: '0 0 0 32 32', 105 | fill: 'currentColor', 106 | height: '32px', 107 | width: '32px' 108 | }, [ 109 | h('use', { 'xlink:href': '#my-id' }) 110 | ]) 111 | 112 | document.body.appendChild(node) 113 | ``` 114 | 115 | ### Use without helper functions 116 | 117 | Sometimes you need to fall back to the traditional `createElement(tag, attributes, children)` (aliased to `h`), for example svg tags. 118 | 119 | ```js 120 | const h = require('elementx') 121 | 122 | const node = h('h1', 'text') 123 | 124 | console.log(node.outerHTML) 125 | /* 126 | * -> 127 | *

text

128 | */ 129 | ``` 130 | 131 | ### Events 132 | 133 | All [HTML DOM Events](https://developer.mozilla.org/en-US/docs/Web/Events) can be attached. The casing of the event name doesn't matter (`onClick`, `onclick`, `ONCLICK` etc.) 134 | 135 | ```js 136 | const node = h.button({ 137 | onClick: () => console.log('button has been clicked') 138 | }) 139 | 140 | document.body.appendChild(node) 141 | ``` 142 | 143 | ## External tools 144 | 145 | * [html-to-hyperscript](html-to-hyperscript.paqmind.com) - Web-Service to convert HTML to hyperscript 146 | 147 | ## Tests 148 | 149 | ```bash 150 | > npm test 151 | ``` 152 | 153 | ## License 154 | 155 | [MIT][license-url] 156 | 157 | [travis-image]: https://img.shields.io/travis/queckezz/elementx.svg?style=flat-square 158 | [travis-url]: https://travis-ci.org/queckezz/elementx 159 | 160 | [version-image]: https://img.shields.io/npm/v/elementx.svg?style=flat-square 161 | [version-url]: https://npmjs.org/package/elementx 162 | 163 | [codecov-image]: https://img.shields.io/codecov/c/github/queckezz/elementx/master.svg?style=flat-square 164 | [codecov-url]: https://codecov.io/github/queckezz/elementx 165 | 166 | [downloads-image]: https://img.shields.io/npm/dm/elementx.svg?style=flat-square 167 | [downloads-url]: https://npmjs.org/package/elementx 168 | 169 | [david-image]: http://img.shields.io/david/queckezz/elementx.svg?style=flat-square 170 | [david-url]: https://david-dm.org/queckezz/elementx 171 | 172 | [standard-image]: https://img.shields.io/badge/code-standard-brightgreen.svg?style=flat-square 173 | [standard-url]: https://github.com/feross/standard 174 | 175 | [unfancy-js-image]: https://img.shields.io/badge/javascript-unfancy-ff69b4.svg?style=flat-square 176 | [unfancy-js-url]: https://github.com/yoshuawuyts/tiny-guide-to-non-fancy-node 177 | 178 | [license-image]: http://img.shields.io/npm/l/elementx.svg?style=flat-square 179 | [license-url]: ./license --------------------------------------------------------------------------------