├── src ├── picoKey.js ├── document.js ├── css.js ├── _c-node.js ├── find.js ├── _template.js ├── create.js ├── _c-element.js └── _c-list.js ├── dist ├── browser.gz ├── browser.min.js ├── index.js └── browser.js ├── .gitignore ├── .editorconfig ├── index.html ├── module.js ├── examples ├── index.html ├── rollup.config.js ├── icons.js ├── Store.js ├── table.js ├── transition.js ├── index.js └── tachyons.css ├── tst ├── css.js ├── text.js ├── find.js ├── list.js └── element.js ├── CHANGELOG.md ├── package.json └── readme.md /src/picoKey.js: -------------------------------------------------------------------------------- 1 | export var picoKey = '_pico' 2 | -------------------------------------------------------------------------------- /dist/browser.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hville/pico-dom/HEAD/dist/browser.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules/ 3 | notes/ 4 | example/ 5 | bld/ 6 | note/ 7 | old/ 8 | *.log 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{md,json}] 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | picoDOM v0.18.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // create template 4 | export {text, element, svg, elementNS, list, template} from './src/create' 5 | 6 | // utils 7 | export {setDocument, D} from './src/document' 8 | export {find} from './src/find' 9 | export {css} from './src/css' 10 | -------------------------------------------------------------------------------- /src/document.js: -------------------------------------------------------------------------------- 1 | export var D = typeof document !== 'undefined' ? document : null 2 | 3 | /** 4 | * @function setDocument 5 | * @param {Document} doc DOM document 6 | * @return {Document} DOM document 7 | */ 8 | export function setDocument(doc) { 9 | return D = doc 10 | } 11 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | picoDOM examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/rollup.config.js: -------------------------------------------------------------------------------- 1 | // cli: rollup -c -w -m inline entryFile 2 | import serve from 'rollup-plugin-serve' 3 | import livereload from 'rollup-plugin-livereload' 4 | 5 | export default { 6 | format: 'iife', 7 | dest: 'index.js', 8 | plugins: [ 9 | serve({open: true, contentBase: ''}), //{open: true, contentBase: ['dist', 'html']} 10 | livereload('') //'dist' 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/css.js: -------------------------------------------------------------------------------- 1 | import {D} from '../module' 2 | 3 | var sheet = null 4 | 5 | export function css(cssRuleText) { 6 | (sheet || getSheet()).insertRule( 7 | cssRuleText, 8 | sheet.cssRules.length 9 | ) 10 | } 11 | 12 | function getSheet() { 13 | var sheets = D.styleSheets, 14 | media = /^$|^all$/ //mediaTypes: all, print, screen, speach 15 | 16 | // get existing sheet 17 | for (var i=0; ic.node.textContent === 'H21'), h21) 39 | ct('===', find(h, c=>c.node.textContent === 'H21', h21), h21) 40 | ct('===', find(h, c=>c.node.textContent === 'H21', h12), null) 41 | }) 42 | -------------------------------------------------------------------------------- /examples/icons.js: -------------------------------------------------------------------------------- 1 | // immutable templates, svg elements 2 | import {svg, template} from '../module' 3 | 4 | var ic_circle = template( // template used to pre-resolve the node structure 5 | svg('svg', { 6 | attrs: { 7 | fill: '#000000', 8 | height: '24', 9 | viewBox: '0 0 24 24', 10 | width: '24' 11 | }}, 12 | svg('path', { 13 | attrs: { 14 | fill: 'none', 15 | d: 'M0 0h24v24H0z' 16 | } 17 | }) 18 | ).create().node // template will clone the node instead of runing all steps 19 | ) 20 | 21 | export var ic_add = ic_circle.append( //ic_add_circle_outline_black_36px 22 | svg('path').attr('d', 23 | 'M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z' 24 | ) 25 | ) 26 | 27 | export var ic_clear = ic_circle.append( //ic_clear_black_36px 28 | svg('path').attr('d', 29 | 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z' 30 | ) 31 | ) 32 | 33 | export var ic_remove = ic_circle.append( //ic_remove_circle_outline_black_36px 34 | svg('path').attr('d', 35 | 'M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z' 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /examples/Store.js: -------------------------------------------------------------------------------- 1 | // generic simple store for the examples 2 | 3 | export function Store(config) { 4 | this.data = {} 5 | for (var i=0, ks=Object.keys(config); i 2 | 3 | # Change Log 4 | 5 | - based on [Keep a Changelog](http://keepachangelog.com/) 6 | - adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | ~~Removed, Changed, Deprecated, Added, Fixed, Security~~ 10 | - minor: use Object.create(null) for key maps 11 | 12 | ## [1.0.1] - 2017-05-29 13 | ### Fixed 14 | - minor internal simplifications for lists 15 | 16 | ## [1.0.0] - 2017-05-28 17 | ### Changed 18 | - major API changes 19 | - .assign changed to .extra and .extras 20 | - variadic template functions changed distinct single and plural form 21 | - .prop(s), .attr(s) and .attrs 22 | - reverted .store and .common to .root 23 | - added examples: table and transition 24 | 25 | ## [0.33.0] - 2017-05-17 26 | ### Added 27 | - component.common instead of .store and .state 28 | - template function 29 | - find function 30 | 31 | ## [0.30.0] - 2017-05-14 32 | ### Changed 33 | - rewrite, simpler API, all components 34 | - setDocument instead of defaultView 35 | 36 | ### Added 37 | - Select List to conditionally display different components 38 | 39 | ## [0.20.0] - 2017-04-10 40 | ### Changed 41 | - expose List 42 | - moveto renamed to moveTo 43 | - clear renamed to removeChildren 44 | 45 | ### Fixed 46 | - fix Component and List methods to all return `this` instead of `Node` 47 | 48 | ## [0.19.0] - 2017-04-07 49 | ### Changed 50 | - build system to ES5 modules with CJS and Browser Exports 51 | - expose Component and List 52 | - list.dataKey(v,i,a) instead of list.dataKey(v,i) 53 | - Component(node, extra, key, index) 54 | 55 | ## [0.18.0] - 2017-04-04 56 | ### Changed 57 | - all component decorators grouped under the `extra` decorator 58 | - list.update performance improvement 59 | - minimal WeakMap polyfill for older browsers 60 | 61 | ## [0.15.0] - 2017-04-02 62 | ### Changed 63 | - change all method names to full names (element, component, fragment, text, comment) 64 | - list signature changed to (component|factory, dataKey) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pico-dom", 3 | "version": "1.0.1", 4 | "main": "./dist/index.js", 5 | "module": "./module.js", 6 | "browser": "./dist/browser.js", 7 | "description": "small DOM component system", 8 | "keywords": [ 9 | "component", 10 | "dom", 11 | "createElement", 12 | "hyperscript", 13 | "svg", 14 | "xml", 15 | "namespace" 16 | ], 17 | "author": "Hugo Villeneuve", 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "cotest": "^2.1.1", 21 | "rollup-plugin-livereload": "^0.4.0", 22 | "rollup-plugin-serve": "^0.3.0" 23 | }, 24 | "scripts": { 25 | "test": "npm run build:main && cotest tst", 26 | "gzip": "node -e \"fs.writeFileSync(process.argv[2], zlib.gzipSync(fs.readFileSync(process.argv[1])))\"", 27 | "build:docs": "jsdoc2md --no-gfm ./src/*.js > api.md", 28 | "build:main": "rollup -o ./dist/index.js -f cjs --banner \"/* hugov@runbox.com | https://github.com/hville/pico-dom.git | license:MIT */\" ./module.js", 29 | "build:browser": "rollup -o ./dist/browser.js -f iife -n picoDOM --banner \"/* hugov@runbox.com | https://github.com/hville/pico-dom.git | license:MIT */\" ./module.js", 30 | "build:min": "google-closure-compiler-js --compilationLevel ADVANCED --languageIn ES5 --languageOut ES5 --useTypesForOptimization true ./dist/browser.js > ./dist/browser.min.js", 31 | "build:gzip": "npm run gzip -- ./dist/browser.min.js ./dist/browser.gz", 32 | "build": "npm run build:main && npm run build:module && npm run build:browser && npm run build:min", 33 | "prepublish": "npm test && npm run build:browser && npm run build:min", 34 | "example:table": "cd ./examples && rollup table.js -c -w", 35 | "example:transition": "cd ./examples && rollup transition.js -c -w" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/hville/pico-dom.git" 40 | }, 41 | "private": false, 42 | "license": "MIT", 43 | "publishConfig": { 44 | "registry": "https://registry.npmjs.org/" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/transition.js: -------------------------------------------------------------------------------- 1 | import {D, css, element as el, list} from '../module' 2 | 3 | css('.transitionEx { opacity: 0.5; transition: all 1s ease; }') 4 | css('.transitionIn { opacity: 1.0; transition: all 1s ease; }') 5 | 6 | var optionValues = { 7 | templates: 'immutable template', 8 | lists: 'select list', 9 | components: 'dynamic components', 10 | css: 'css rule insertion', 11 | transitions: 'css transitions', 12 | async: 'async operations', 13 | events: 'event listeners setting and removal' 14 | } 15 | 16 | var optionKeys = Object.keys(optionValues) 17 | 18 | 19 | var select = el('select', 20 | list( optionKeys.map(function(k) { 21 | return el('option', {attr: 'selected'}, k) 22 | })) 23 | ) 24 | .attr('multiple') 25 | .attr('size', optionKeys.length) 26 | 27 | 28 | var item = el('li', 29 | function() { 30 | var comp = this, 31 | moveTo = comp.moveTo, 32 | destroy = comp.destroy 33 | 34 | this.class('transitionEx pl5 light-blue') 35 | 36 | // on insert, async change of the class to trigger transition 37 | this.moveTo = function(parent, before) { 38 | if (!this.node.parentNode) D.defaultView.requestAnimationFrame(function() { 39 | comp.class('transitionIn pl1 dark-blue' ) 40 | }) 41 | return moveTo.call(this, parent, before) 42 | } 43 | 44 | // on remove, change the class and wait for transition end before removing 45 | this.destroy = function() { 46 | this.event('transitionend', function() { 47 | this.event('transitionend') //remove the listener 48 | destroy.call(comp) 49 | }) 50 | if (this.node.parentNode) D.defaultView.requestAnimationFrame(function() { 51 | comp.class('transitionEx pl5 light-blue') 52 | }) 53 | return this 54 | } 55 | this.update = this.text 56 | } 57 | ) 58 | 59 | 60 | el('div', 61 | el('h2', {class: 'pl3'}, 'example with'), 62 | el('div', {class: ''}, 63 | el('div', {class: 'fl w-25 pa3'}, 64 | select.class('v-top').call(function() { 65 | this.root.refs.select = this 66 | this.event('change', function() { this.root.update() }) 67 | }) 68 | ), 69 | el('div', {class: 'fl w-25 pa3'}, 70 | el('ol', {class: 'v-top'}, 71 | list( 72 | item 73 | ).extra('update', function() { 74 | var opts = this.root.refs.select.node.options, 75 | keys = [] 76 | for (var i=0; i v.k) 115 | ).create() 116 | var elem = co.node 117 | 118 | ct('===', toString(elem.childNodes), '^$') 119 | 120 | co.update([{k: 1, v:1}, {k: 'b', v:'b'}]) 121 | ct('===', toString(elem.childNodes), '^1b$') 122 | 123 | co.update([{ k: 'b', v: 'bb' }, { k: 1, v: 11 }]) 124 | ct('===', toString(elem.childNodes), '^b1$', 'must use existing nodes') 125 | 126 | co.update([{k: 'c', v:'c'}]) 127 | ct('===', toString(elem.childNodes), '^c$') 128 | 129 | co.update([{ k: 'b', v: 'bbb' }, { k: 'c', v: 'ccc' }, { k: 1, v: 111 }]) 130 | ct('===', toString(elem.childNodes), '^bbbc111$', 're-creates removed nodes') 131 | }) 132 | 133 | ct('list select', function() { 134 | var co = el('h1', 135 | list({ 136 | a: text('').extra('update', function(v) { this.text('a'+v) }), 137 | b: text('').extra('update', function(v) { this.text('b'+v) }) 138 | }).extra('select', v => v) 139 | ).create() 140 | var elem = co.node 141 | 142 | ct('===', toString(elem.childNodes), '^$') 143 | 144 | co.update('a') 145 | ct('===', toString(elem.childNodes), '^aa$') 146 | 147 | co.update('b') 148 | ct('===', toString(elem.childNodes), '^bb$') 149 | 150 | co.update('c') 151 | ct('===', toString(elem.childNodes), '^$') 152 | }) 153 | -------------------------------------------------------------------------------- /src/_c-list.js: -------------------------------------------------------------------------------- 1 | import {D} from './document' 2 | import {picoKey} from './picoKey' 3 | import {CElementProto} from './_c-element' 4 | 5 | 6 | /** 7 | * @constructor 8 | * @param {!Object} template 9 | */ 10 | export function CList(template) { 11 | this.root = null 12 | this.template = template 13 | this.node = D.createComment('^') 14 | this.foot = D.createComment('$') 15 | this.refs = Object.create(null) 16 | this.node[picoKey] = this 17 | this.foot[picoKey] = this 18 | 19 | //keyed 20 | if (template.create) this.update = this.updateChildren 21 | // select list 22 | else { 23 | this.update = this.updateChildren = updateSelectChildren 24 | for (var i=0, ks=Object.keys(template); i 2 | 3 | # pico-dom 4 | 5 | *minimalist tool for dynamic DOM tree and component creation, updates, events and lifecycles functions. Supports svg, namespace, keyed lists, and conditional elements, all in 2kb gzip, ES5, no dependencies. Support direct use in browser, CJS and ES modules* 6 | 7 | • [Example](#example) • [Why](#why) • [API](#api) • [License](#license) 8 | 9 | ## Example 10 | 11 | supports different environments 12 | * CJS: `require('pico-dom').element` 13 | * can also be used server-side. See `setDocument` below 14 | * ES modules: `import {element} from 'pico-dom'` 15 | * browser: (`picoDOM.element`)* 16 | 17 | ```javascript 18 | import {D, element as el, list} from '../module' 19 | import {Store} from './Store' // any user store will do 20 | import {ic_remove, ic_add} from './icons' 21 | 22 | var store = new Store([]), 23 | i = 0, 24 | j = 0 25 | 26 | var table = el('table', 27 | el('caption', {class: 'f4'}, 'table example with...'), 28 | el('tbody', 29 | list( 30 | el('tr', 31 | function() { i = this.key }, 32 | el('td', //leading column with icon 33 | function() { this.i = i }, 34 | { events: { click: function() { this.root.store.delRow(this.i) } } }, 35 | ic_remove 36 | ), 37 | list( // data columns 38 | el('td', 39 | function() { j = this.key }, 40 | el('input', 41 | function() { 42 | this.i = i; this.j = j 43 | this.update = function(v) { this.node.value = v } 44 | this.event('change', function() { 45 | this.root.store.set(this.node.value, [this.i, this.j]) 46 | }) 47 | } 48 | ) 49 | ) 50 | ) 51 | ) 52 | ), 53 | el('tr', 54 | el('td', 55 | { events: {click: function() { this.root.store.addRow() } } }, 56 | ic_add 57 | ) 58 | ) 59 | ) 60 | ).create() 61 | .extra('store', store) 62 | .moveTo(D.body) 63 | 64 | store.onchange = function() { table.update( store.get() ) } 65 | store.set([ 66 | ['icons', 'SVG icons'], 67 | ['keyed', 'keyed list'], 68 | ['store', 'data flow'], 69 | ['event', 'event listeners'] 70 | ]) 71 | ``` 72 | 73 | ## Why 74 | 75 | To explore ideas for a flexible and concise API with minimal tooling and memory footprint. 76 | 77 | 78 | ### Features 79 | 80 | * dynamic lists and nested lists (keyed, indexed or select) 81 | * svg and namespace support 82 | * ability to inject a `document API` for server use and/or testing (e.g. `jsdom`) 83 | * no virtual DOM, all operations are done on actual nodes 84 | * 2kb gzip, no dependencies, all under 600 lines including comments and jsDocs 85 | * all text injections and manipulations done through the secure `textContent` and `nodeValue` DOM API 86 | * available in CommonJS, ES6 modules and browser versions 87 | * All in ES5 with ES modules, CJS module and iife for browsers. Should work well on mobile and older browsers like IE9. 88 | 89 | 90 | ### Limitations 91 | 92 | * view and event helpers only 93 | * limited css utility 94 | * strictly DOM element creation and manipulation (no router, no store) 95 | 96 | 97 | ## API 98 | 99 | ### Templates 100 | 101 | A template is an immutable set of instructions to generate multiple components. 102 | 103 | Element templates 104 | * `element(tagName [, ...options|children|transforms])` 105 | * `elementNS(nsURI, tagName [, ...options|children|transforms])` 106 | * `svg(tagName [, ...options|children|transforms])` 107 | 108 | Node template 109 | * `text(textContent [, ...options|transforms])` 110 | * `template(node, [, ...options|transforms])` 111 | 112 | List template 113 | * `list(template, [, ...options|transforms])` 114 | 115 | 116 | The 6 template generating functions take the following types of arguments: 117 | 118 | * **options**: Object to define future operations `{methodName: arguments}` upon component creation. Examples 119 | * `{class: 'abc'}` to set the component node class attribute once an element component is created 120 | * `{attrs: {id: 'abc'}}` to set component node attributes once an element component is created 121 | * `{events: {click: function() { this.text('CLICKED!') } }}` to set element component event listeners 122 | * `{props: {id: 'abc'}}` to set component node properties 123 | * `{extras: {someKey: someValue}}` to set component properties on the component itself 124 | 125 | * **transforms** are just functions called with the component context. Examples 126 | * `function() { this.class('abc') }` to set the component node class attribute once an element component is created 127 | * `function() { this.attr(id: 'abc') }` to set component node attributes once an element component is created 128 | * `function() { this.event('click', handler) }` to set element component event listeners 129 | 130 | * **children** can be templates, nodes or numbers and strings to be converted to text nodes. Same as using the `{append: [...]}` option 131 | 132 | Templates have a number of chainable methods that generate new templates (templates are immutable). The methods are the same as the option keys listed above with additional methods that take more than one argument. 133 | * element attributes: `.attr(key, val)`, `.attrs({key, val})` 134 | * element children: `.append(node, number, string, template)` 135 | * element event listeners: `.event(name, callback)`, `.events({name, callback})` 136 | * node properties: `.prop(key, val)`, `.props({key, val})` 137 | * component properties: `.extra(key, val)`, `.extras({key, val})` 138 | * component operations: `.call(funtion() {})` 139 | 140 | 141 | ### List 142 | 143 | List are special components representing a group of multiple nodes. 144 | 145 | Resizable lists take a single template that will be used to generate list of varying sizes 146 | * `list(template)` to create dynamic indexed set of nodes based on the size of the array upon updates 147 | * `list(template, {getKey: function(v) {return v.id}})` for a keyed list 148 | 149 | Select lists have predefined templates that are used to conditionally display subsets on updates 150 | * `list({a: templateA, b:templateB}, {select: function(v) {return [v.id]}})` created a conditional list template 151 | 152 | lists can be stacked and nested. 153 | 154 | 155 | ### Components 156 | 157 | Components are created from templates: `template.create()`. 158 | Normally, only the main component is manually created and all other templates are initiated from within. 159 | 160 | In addition to the same methods found in templates (`attr(s)`, `extra(s)`, `call`, `append`, `class`, `event`), Components have the following properties and methods for dealing with DOM Nodes 161 | 162 | #### DOM references 163 | 164 | * `.node`: the associated DOM node or anchor node for lists 165 | * `.root`: the root component that created the component. (`null` for the main one created by the user) 166 | 167 | #### DOM functions 168 | 169 | * `.moveTo(parent [,before])`: to move a component 170 | * `.remove()`: to remove a component from the DOM 171 | * `.destroy()`: to remove a component and remove listeners 172 | 173 | #### Update Functions 174 | 175 | * `.text(v)`: to set the node textContent of element Component 176 | * `.update(...)` the function to trigger changes based on external data 177 | * `.updateChildren(..)` to pass update data down the tree. 178 | 179 | By default, update is set to `text` for text components and `updateChildren` for the rest. 180 | Normally, only the main component is updated and the update trickles down the DOM tree according the the rules predefined in the templates. 181 | 182 | #### Other 183 | 184 | * `.key` key for identification and sorting. Set by parent lists on componet children 185 | * `.refs`: used in list to hold node references 186 | * `.getKey(val, key, arr) => string`: to get the unique key for keyed lists 187 | * `.select(val, key) => array`: array of keys for conditional/select lists 188 | 189 | 190 | #### Lifecycle operations 191 | 192 | Lifecycle hooks are not provided directly but can be acheived by wrapping component methods 193 | 194 | * on creation: `template.call(...)` 195 | * on insert: `template.call(...)` or wrap `component.moveTo(...)` 196 | * on remove: wrap `component.remove()` 197 | * on destroy: wrap `component.destroy()` 198 | 199 | ```javascript 200 | var textTemplate = text('ABC', function() { 201 | var item = this, 202 | remove = item.remove 203 | this.remove = function() { 204 | window.requestAnimationFrame(remove.call(item)) 205 | } 206 | }) 207 | ``` 208 | 209 | 210 | ### Other helpers 211 | * `setDocument(document)` to set the Document interface for testing or server use 212 | * eg. `setDocument((new JSDOM).window.document)` 213 | * `D` reference to the Document interface for testing or server use 214 | * eg. `var body = D.body` 215 | * `find(from [, test] [, until])` find a component within nodes or components and matching the test function. It parses nodes up and down following the html markup order. 216 | * eg. `find(document.body)` to get the first component in the document 217 | * eg. `find(tableComponent, function(c) { return c.key === 5 } )` 218 | * `css(ruleText)` to insert a rule in the document for cases where an exported template relies on a specific css rule that is not convenient or practical to include in a seperate css file 219 | 220 | ## License 221 | 222 | [MIT](http://www.opensource.org/licenses/MIT) © [Hugo Villeneuve](https://github.com/hville) 223 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | document.write('