├── 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('