ajo 0.0.5 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/index.cjs +172 -0
  2. package/index.js +157 -163
  3. package/index.test.js +21 -13
  4. package/package.json +20 -16
  5. package/readme.md +61 -1
package/index.cjs ADDED
@@ -0,0 +1,172 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from2, except, desc) => {
10
+ if (from2 && typeof from2 === "object" || typeof from2 === "function") {
11
+ for (let key of __getOwnPropNames(from2))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var ajo_exports = {};
19
+ __export(ajo_exports, {
20
+ Fragment: () => Fragment,
21
+ cleanup: () => cleanup,
22
+ clx: () => clx,
23
+ component: () => component,
24
+ consume: () => consume,
25
+ h: () => h,
26
+ intercept: () => intercept,
27
+ keb: () => keb,
28
+ propagate: () => propagate,
29
+ provide: () => provide,
30
+ refresh: () => refresh,
31
+ render: () => render,
32
+ stx: () => stx
33
+ });
34
+ module.exports = __toCommonJS(ajo_exports);
35
+ const Fragment = ({ children }) => children, h = (nodeName, props, ...children) => {
36
+ children = children.length == 0 ? null : children.length == 1 ? children[0] : children;
37
+ return { children, ...props, nodeName };
38
+ }, component = (setup) => ({ is, key, host, ref, ...props }) => h(is ?? setup.is ?? "div", {
39
+ key,
40
+ ...setup.host,
41
+ ...host,
42
+ skip: true,
43
+ ref: (host2) => (refresh(host2, props, setup), isFn(ref) && ref(host2))
44
+ }), render = (h2, host) => {
45
+ let child = host.firstChild, node, byKey = keyed.get(host);
46
+ for (h2 of normalize(h2, host)) {
47
+ if (typeof h2 == "string") {
48
+ for (node = child; node; node = node.nextSibling)
49
+ if (node.nodeType == 3)
50
+ break;
51
+ node ? node.data !== h2 && (node.data = h2) : node = document.createTextNode(h2);
52
+ } else if (h2 instanceof Node) {
53
+ node = h2;
54
+ } else {
55
+ const { key, nodeName, skip, block, children, ref, ...props } = h2;
56
+ if (key != null && (node = byKey?.get(key)))
57
+ ;
58
+ else
59
+ for (node = child; node; node = node.nextSibling)
60
+ if (node.localName == nodeName)
61
+ break;
62
+ node ||= document.createElement(nodeName);
63
+ key != null && (byKey ||= keyed.set(host, /* @__PURE__ */ new Map())).set(key, node);
64
+ update(props, node);
65
+ !(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node);
66
+ isFn(ref) && ref(node);
67
+ }
68
+ node === child ? child = child.nextSibling : before(host, child, node);
69
+ }
70
+ while (child) {
71
+ const next = child.nextSibling;
72
+ host.removeChild(child).nodeType == 1 && dispose(child);
73
+ child = next;
74
+ }
75
+ }, refresh = (host, props, setup) => {
76
+ try {
77
+ if (setup && !isFn(setup))
78
+ throwTypeError("Setup", setup, fn);
79
+ props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {};
80
+ render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host);
81
+ } catch (error) {
82
+ propagate(host, error);
83
+ }
84
+ }, provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, /* @__PURE__ */ new Map())).set(key, value), consume = (host, key, fallback) => {
85
+ let map;
86
+ while (host) {
87
+ if ((map = provisions.get(host)) && map.has(key))
88
+ return map.get(key);
89
+ host = host.parentNode;
90
+ }
91
+ return fallback;
92
+ }, intercept = (host, interceptor) => {
93
+ if (!isFn(interceptor))
94
+ throwTypeError("Interceptor", interceptor, fn);
95
+ interceptors.set(host, interceptor);
96
+ }, propagate = (host, error) => {
97
+ for (let interceptor; host; host = host.parentNode)
98
+ if (interceptor = interceptors.get(host))
99
+ return render(host, interceptor(error));
100
+ throw error;
101
+ }, cleanup = (host, cleaner) => {
102
+ if (!isFn(cleaner))
103
+ throwTypeError("Cleaner", cleaner, fn);
104
+ (cleaners.get(host) ?? cleaners.set(host, /* @__PURE__ */ new Set())).add(cleaner);
105
+ }, clx = (o) => keys(o).filter((k) => o[k]).join(" ") || null, stx = (o) => entries(o).map((t) => t.join(":")).join(";") || null, keb = (o) => keys(o).reduce((r, k) => (r[k.replace(search, replace).toLowerCase()] = o[k], r), {});
106
+ const wm = () => {
107
+ const instance = /* @__PURE__ */ new WeakMap();
108
+ const { set } = instance;
109
+ instance.set = (key, value) => (set.call(instance, key, value), value);
110
+ return instance;
111
+ }, throwTypeError = (name, value, expected) => {
112
+ throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`);
113
+ }, every = (a, b) => a === b || isArray(a) && isArray(b) && a.length == b.length && a.every((v, i) => v === b[i]), apply = (o, { name, value }) => (o[name] = value, o), reduce = (list) => from(list).reduce(apply, {}), isFn = (v) => typeof v == fn, { keys, entries } = Object, { isArray, from } = Array, fn = "function", search = /([a-z0-9])([A-Z])/g, replace = "$1-$2", keyed = wm(), deps = wm(), memo = wm(), renders = wm(), provisions = wm(), interceptors = wm(), cleaners = wm(), cache = wm(), normalize = function* (h2, host) {
114
+ let type, buffer = "";
115
+ for (h2 of isFn(h2?.[Symbol.iterator]) ? h2 : [h2]) {
116
+ if (h2 == null || (type = typeof h2) == "boolean")
117
+ continue;
118
+ if (type == "string" || type == "number") {
119
+ buffer += h2;
120
+ continue;
121
+ }
122
+ if ("nodeName" in Object(h2)) {
123
+ if (isFn(h2.nodeName)) {
124
+ yield* normalize(h2.nodeName(h2, host), host);
125
+ continue;
126
+ }
127
+ if (buffer) {
128
+ yield buffer;
129
+ buffer = "";
130
+ }
131
+ yield h2;
132
+ continue;
133
+ }
134
+ isFn(h2[Symbol.iterator]) ? yield* normalize(h2, host) : buffer += h2;
135
+ }
136
+ if (buffer)
137
+ yield buffer;
138
+ }, update = (props, host) => {
139
+ const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {});
140
+ for (const name in { ...prev, ...props }) {
141
+ let value = props[name];
142
+ if (value === prev[name])
143
+ continue;
144
+ if (name.startsWith("set:")) {
145
+ host[name.slice(4)] = value;
146
+ continue;
147
+ }
148
+ if (value === true)
149
+ value = "";
150
+ else if (value == null || value === false) {
151
+ host.removeAttribute(name);
152
+ continue;
153
+ }
154
+ host.setAttribute(name, value);
155
+ }
156
+ cache.set(host, props);
157
+ }, before = (host, child, node) => {
158
+ if (node.contains?.(document.activeElement)) {
159
+ const ref = node.nextSibling;
160
+ while (child && child !== node) {
161
+ const next = child.nextSibling;
162
+ host.insertBefore(child, ref);
163
+ child = next;
164
+ }
165
+ } else
166
+ host.insertBefore(node, child);
167
+ }, dispose = (host) => {
168
+ for (const child of host.children)
169
+ dispose(child);
170
+ for (const cleaner of cleaners.get(host) ?? [])
171
+ cleaner(host);
172
+ };
package/index.js CHANGED
@@ -1,205 +1,199 @@
1
- const { isArray } = Array
2
- const { hasOwn, keys } = Object
1
+ export const
2
+ Fragment = ({ children }) => children,
3
3
 
4
- export const Skip = Symbol()
5
- export const Fragment = ({ children }) => children
6
- export const Portal = ({ host, children }) => render(host, children)
4
+ h = (nodeName, props, ...children) => {
5
+ children = children.length == 0 ? null : children.length == 1 ? children[0] : children
6
+ return { children, ...props, nodeName }
7
+ },
7
8
 
8
- const Element = Symbol()
9
+ component = setup => ({ is, key, host, ref, ...props }) => h(is ?? setup.is ?? 'div', {
10
+ key, ...setup.host, ...host, skip: true, ref: host => (refresh(host, props, setup), isFn(ref) && ref(host))
11
+ }),
9
12
 
10
- export const createTextNode = data => ({ [Element]: '#text', data, skip: true })
13
+ render = (h, host) => {
11
14
 
12
- export const createElement = (name, props, ...children) => {
13
- props = { ...props }
14
- children = props.children ?? children
15
- if (children.length > 0) props.children = children.length === 1 ? children[0] : children
16
- props[Element] = name
17
- return props
18
- }
15
+ let child = host.firstChild, node, byKey = keyed.get(host)
19
16
 
20
- function* g(vnode, host) {
21
- const vnodes = isArray(vnode) ? vnode : [vnode]
22
- let data = ''
17
+ for (h of normalize(h, host)) {
23
18
 
24
- while (vnodes.length > 0) {
25
- vnode = vnodes.shift()
19
+ if (typeof h == 'string') {
26
20
 
27
- if (vnode == null || typeof vnode === 'boolean') continue
28
- if (typeof vnode === 'string') data += vnode
29
- else if (hasOwn(vnode, Element)) {
30
- const { [Element]: name } = vnode
21
+ for (node = child; node; node = node.nextSibling) if (node.nodeType == 3) break
22
+ node ? node.data !== h && (node.data = h) : node = document.createTextNode(h)
31
23
 
32
- if (typeof name === 'function') {
33
- vnodes.unshift(name(vnode, host))
34
- continue
35
- }
24
+ } else if (h instanceof Node) {
36
25
 
37
- if (data) {
38
- yield createTextNode(data)
39
- data = ''
40
- }
26
+ node = h
41
27
 
42
- yield vnode
43
- } else typeof vnode[Symbol.iterator] === 'function'
44
- ? vnodes.unshift(...vnode)
45
- : data += vnode
46
- }
28
+ } else {
29
+
30
+ const { key, nodeName, skip, block, children, ref, ...props } = h
31
+
32
+ if (key != null && (node = byKey?.get(key)));
33
+ else for (node = child; node; node = node.nextSibling) if (node.localName == nodeName) break
34
+
35
+ node ||= document.createElement(nodeName)
47
36
 
48
- if (data) yield createTextNode(data)
49
- }
37
+ key != null && (byKey ||= keyed.set(host, new Map)).set(key, node)
50
38
 
51
- export const render = (vnode, host) => {
52
- let child = host.firstChild
39
+ update(props, node)
53
40
 
54
- for (const { [Element]: name, ref, skip, children, ...props } of g(vnode, host)) {
41
+ !(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node)
55
42
 
56
- if (name === Skip) {
57
- child = props.end ? null : child?.nextSibling ?? null
58
- continue
43
+ isFn(ref) && ref(node)
44
+ }
45
+
46
+ node === child ? child = child.nextSibling : before(host, child, node)
59
47
  }
60
48
 
61
- let node = child
49
+ while (child) {
50
+ const next = child.nextSibling
51
+ host.removeChild(child).nodeType == 1 && dispose(child)
52
+ child = next
53
+ }
54
+ },
62
55
 
63
- while (node != null) {
64
- if (node.nodeName.toLowerCase() === name && node.getAttribute?.('key') == props.key) break
65
- node = node.nextSibling
56
+ refresh = (host, props, setup) => {
57
+ try {
58
+ if (setup && !isFn(setup)) throwTypeError('Setup', setup, fn)
59
+ props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {}
60
+ render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host)
61
+ } catch (error) {
62
+ propagate(host, error)
66
63
  }
64
+ },
65
+
66
+ provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, new Map)).set(key, value),
67
67
 
68
- if (node == null) node = name === '#text'
69
- ? document.createTextNode('')
70
- : document.createElement(name)
68
+ consume = (host, key, fallback) => {
69
+ let map
70
+ while (host) {
71
+ if ((map = provisions.get(host)) && map.has(key)) return map.get(key)
72
+ host = host.parentNode
73
+ }
74
+ return fallback
75
+ },
71
76
 
72
- patch(props, node)
77
+ intercept = (host, interceptor) => {
78
+ if (!isFn(interceptor)) throwTypeError('Interceptor', interceptor, fn)
79
+ interceptors.set(host, interceptor)
80
+ },
73
81
 
74
- if (typeof ref === 'function') ref(node)
75
- else if (typeof ref === 'object' && ref !== null) ref.current = node
76
-
77
- skip || render(children, node)
82
+ propagate = (host, error) => {
83
+ for (let interceptor; host; host = host.parentNode)
84
+ if (interceptor = interceptors.get(host)) return render(host, interceptor(error))
85
+ throw error
86
+ },
78
87
 
79
- if (node === child) child = child.nextSibling
80
- else if (node.contains?.(document.activeElement)) {
81
- const { nextSibling } = node
82
- let ref = child
88
+ cleanup = (host, cleaner) => {
89
+ if (!isFn(cleaner)) throwTypeError('Cleaner', cleaner, fn);
90
+ (cleaners.get(host) ?? cleaners.set(host, new Set)).add(cleaner)
91
+ },
83
92
 
84
- while (ref != null && ref !== node) {
85
- const next = ref.nextSibling
86
- host.insertBefore(ref, nextSibling)
87
- ref = next
93
+ clx = o => keys(o).filter(k => o[k]).join(' ') || null,
94
+ stx = o => entries(o).map(t => t.join(':')).join(';') || null,
95
+ keb = o => keys(o).reduce((r, k) => ((r[k.replace(search, replace).toLowerCase()] = o[k]), r), {})
96
+
97
+ const
98
+ wm = () => {
99
+ const instance = new WeakMap
100
+ const { set } = instance
101
+ instance.set = (key, value) => (set.call(instance, key, value), value)
102
+ return instance
103
+ },
104
+
105
+ throwTypeError = (name, value, expected) => {
106
+ throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`)
107
+ },
108
+
109
+ every = (a, b) => a === b || isArray(a) && isArray(b) && a.length == b.length && a.every((v, i) => v === b[i]),
110
+ apply = (o, { name, value }) => ((o[name] = value), o),
111
+ reduce = list => from(list).reduce(apply, {}),
112
+ isFn = v => typeof v == fn,
113
+
114
+ { keys, entries } = Object, { isArray, from } = Array,
115
+
116
+ fn = 'function', search = /([a-z0-9])([A-Z])/g, replace = '$1-$2',
117
+
118
+ keyed = wm(), deps = wm(), memo = wm(), renders = wm(), provisions = wm(), interceptors = wm(), cleaners = wm(), cache = wm(),
119
+
120
+ normalize = function* (h, host) {
121
+
122
+ let type, buffer = ''
123
+
124
+ for (h of isFn(h?.[Symbol.iterator]) ? h : [h]) {
125
+
126
+ if (h == null || (type = typeof h) == 'boolean') continue
127
+
128
+ if (type == 'string' || type == 'number') {
129
+ buffer += h
130
+ continue
88
131
  }
89
- } else host.insertBefore(node, child)
90
- }
91
132
 
92
- while (child != null) {
93
- const { nextSibling } = child
94
- if (child.nodeType === Node.ELEMENT_NODE) dispose(child)
95
- host.removeChild(child)
96
- child = nextSibling
97
- }
98
- }
133
+ if ('nodeName' in Object(h)) {
99
134
 
100
- const patch = (props, node) => {
101
- if (node.nodeType === Node.TEXT_NODE) {
102
- if (node.data !== props.data) node.data = props.data
103
- return
104
- }
135
+ if (isFn(h.nodeName)) {
136
+ yield* normalize(h.nodeName(h, host), host)
137
+ continue
138
+ }
105
139
 
106
- for (const name of new Set([...node.getAttributeNames(), ...keys(props)])) {
107
- let value = props[name]
140
+ if (buffer) {
141
+ yield buffer
142
+ buffer = ''
143
+ }
108
144
 
109
- if (name in node && !(typeof value === 'string' && typeof node[name] === 'boolean')) {
110
- try {
111
- if (node[name] !== value) node[name] = value
145
+ yield h
112
146
  continue
113
- } catch (err) { }
114
- }
147
+ }
115
148
 
116
- if (value === true) value = ''
117
- else if (value == null || value === false) {
118
- node.removeAttribute(name)
119
- continue
149
+ isFn(h[Symbol.iterator]) ? yield* normalize(h, host) : buffer += h
120
150
  }
121
151
 
122
- if (node.getAttribute(name) !== value) node.setAttribute(name, value)
123
- }
124
- }
152
+ if (buffer) yield buffer
153
+ },
125
154
 
126
- const components = new WeakMap
155
+ update = (props, host) => {
127
156
 
128
- export const createComponent = fn => ({ is, host, key, ...props }) =>
129
- createElement(is ?? fn.is ?? 'div', {
130
- ...fn.host, ...host, key, skip: true, ref: host => {
131
- let cmp = components.get(host)
132
- if (cmp == null) components.set(host, cmp = new Component(host, fn))
133
- cmp.props = { ...fn.props, ...props }
134
- cmp.update()
135
- }
136
- })
137
-
138
- class Component {
139
- constructor(host, fn) {
140
- this.host = host
141
- this.fn = fn
142
- this.props = null
143
- this.it = null
144
- this.err = false
145
- }
157
+ const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {})
146
158
 
147
- update({ method = 'next', arg } = {}) {
148
- if (this.host == null) return
149
- if (this.err) try { this.it.return() } catch (e) { this.host = null } finally { return }
159
+ for (const name in { ...prev, ...props }) {
150
160
 
151
- try {
152
- if (this.it == null) {
153
- const init = this.fn.call(this, this.props, this.host)
154
- this.it = typeof init?.next === 'function' ? init : generate.call(this, init)
161
+ let value = props[name]
162
+
163
+ if (value === prev[name]) continue
164
+
165
+ if (name.startsWith('set:')) {
166
+ host[name.slice(4)] = value
167
+ continue
155
168
  }
156
169
 
157
- const { value, done } = this.it[method](arg)
170
+ if (value === true) value = ''
171
+ else if (value == null || value === false) {
172
+ host.removeAttribute(name)
173
+ continue
174
+ }
158
175
 
159
- render(value, this.host)
160
- if (done) this.host = null
161
- } catch (err) {
162
- this.err = true
163
- propagate(this.host.parentNode, err)
176
+ host.setAttribute(name, value)
164
177
  }
165
- }
166
178
 
167
- *[Symbol.iterator]() {
168
- while (this.host) yield this.props
169
- }
170
- }
171
-
172
- function* generate(init) {
173
- yield init
174
- for (const props of this) yield this.fn.call(this, props, this.host)
175
- }
176
-
177
- const propagate = (el, err) => {
178
- if (el == null) throw err
179
- const cmp = components.get(el)
180
- typeof cmp?.it?.throw === 'function'
181
- ? cmp.update({ method: 'throw', arg: err })
182
- : propagate(el.parentNode, err)
183
- }
184
-
185
- const dispose = el => {
186
- for (const child of el.children) dispose(child)
187
- components.get(el)?.update({ method: 'return' })
188
- }
189
-
190
- const provisions = new WeakMap
191
-
192
- export const provide = (host, key, value) => {
193
- let map = provisions.get(host)
194
- if (map == null) provisions.set(host, map = new Map)
195
- map.set(key, value)
196
- }
197
-
198
- export const consume = (host, key) => {
199
- for (let node = host; node != null; node = node.parentNode) {
200
- if (provisions.has(node)) {
201
- const map = provisions.get(node)
202
- if (map.has(key)) return map.get(key)
203
- }
179
+ cache.set(host, props)
180
+ },
181
+
182
+ before = (host, child, node) => {
183
+ if (node.contains?.(document.activeElement)) {
184
+
185
+ const ref = node.nextSibling
186
+
187
+ while (child && child !== node) {
188
+ const next = child.nextSibling
189
+ host.insertBefore(child, ref)
190
+ child = next
191
+ }
192
+
193
+ } else host.insertBefore(node, child)
194
+ },
195
+
196
+ dispose = host => {
197
+ for (const child of host.children) dispose(child)
198
+ for (const cleaner of cleaners.get(host) ?? []) cleaner(host)
204
199
  }
205
- }
package/index.test.js CHANGED
@@ -1,28 +1,37 @@
1
- import 'backdom/register.js'
1
+ import 'backdom/register'
2
2
  import { suite } from 'uvu'
3
3
  import * as assert from 'uvu/assert'
4
- import { createComponent, createElement, render, } from './index.js'
4
+ import { component, h, render } from './index.js'
5
5
 
6
6
  let it
7
7
 
8
8
  // ----------------------------------------------------------------------------
9
9
 
10
- it = suite('createElement')
10
+ it = suite('h')
11
11
 
12
12
  it('should create empty vnode', () => {
13
- assert.equal(createElement('div'), {})
13
+ const vnode = h('div')
14
+ assert.equal(vnode, { nodeName: 'div', children: null })
14
15
  })
15
16
 
16
17
  it('should create vnode with props', () => {
17
- assert.equal(createElement('div', { id: 'app' }), { id: 'app' })
18
+ assert.equal(h('div', { id: 'app' }), { nodeName: 'div', children: null, id: 'app' })
18
19
  })
19
20
 
20
21
  it('should create vnode with one string child', () => {
21
- assert.equal(createElement('div', null, 'foo'), { children: 'foo' })
22
+ assert.equal(h('div', null, 'foo'), { nodeName: 'div', children: 'foo' })
22
23
  })
23
24
 
24
25
  it('should create vnode with one vnode child', () => {
25
- assert.equal(createElement('div', null, createElement('span')), { children: {} })
26
+ const child = h('span')
27
+ const vnode = h('div', null, child)
28
+ assert.equal(vnode.children, { nodeName: 'span', children: null })
29
+ })
30
+
31
+ it('should allow children as prop', () => {
32
+ const child = h('span')
33
+ const vnode = h('div', { children: child })
34
+ assert.equal(vnode.children, { nodeName: 'span', children: null })
26
35
  })
27
36
 
28
37
  it.run()
@@ -33,22 +42,21 @@ it = suite('render')
33
42
 
34
43
  it('should render a vnode', () => {
35
44
  const host = document.createElement('div')
36
- render(createElement('div', { foo: 'bar' }, 'foobar'), host)
45
+ render(h('div', { foo: 'bar' }, 'foobar'), host)
37
46
  assert.snapshot(host.innerHTML, '<div foo="bar">foobar</div>')
38
47
  })
39
48
 
40
49
  it('should render a stateless component', () => {
41
50
  const host = document.createElement('div')
42
- const Foo = ({ foo, children }) => createElement('div', { foo }, children)
43
- debugger
44
- render(createElement(Foo, { foo: 'bar' }, createElement('span', null, 'foobar')), host)
51
+ const Foo = ({ foo, children }) => h('div', { foo }, children)
52
+ render(h(Foo, { foo: 'bar' }, h('span', null, 'foobar')), host)
45
53
  assert.snapshot(host.innerHTML, '<div foo="bar"><span>foobar</span></div>')
46
54
  })
47
55
 
48
56
  it('should render a stateful component', () => {
49
57
  const host = document.createElement('div')
50
- const Foo = createComponent(({ foo, children }) => createElement('div', { foo }, children))
51
- render(createElement(Foo, { is: 'foo-component', foo: 'bar' }, createElement('span', null, 'foobar')), host)
58
+ const Foo = component(() => ({ foo, children }) => h('div', { foo }, children))
59
+ render(h(Foo, { is: 'foo-component', foo: 'bar' }, h('span', null, 'foobar')), host)
52
60
  assert.snapshot(host.innerHTML, '<foo-component><div foo="bar"><span>foobar</span></div></foo-component>')
53
61
  })
54
62
 
package/package.json CHANGED
@@ -1,25 +1,29 @@
1
1
  {
2
2
  "name": "ajo",
3
- "version": "0.0.5",
3
+ "version": "0.0.8",
4
4
  "description": "ajo is a JavaScript view library for building user interfaces",
5
5
  "type": "module",
6
- "main": "index.js",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/cristianfalcone/ajo.git"
6
+ "module": "index.js",
7
+ "main": "index.cjs",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "require": "./index.cjs"
12
+ }
10
13
  },
14
+ "scripts": {
15
+ "build": "esbuild --format=cjs --out-extension:.js=.cjs --outdir=. index.js",
16
+ "test": "uvu",
17
+ "release": "npm run build && npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
18
+ },
19
+ "repository": "cristianfalcone/ajo",
11
20
  "author": "Cristian Falcone",
12
21
  "license": "ISC",
13
- "bugs": {
14
- "url": "https://github.com/cristianfalcone/ajo/issues"
15
- },
22
+ "bugs": "https://github.com/cristianfalcone/ajo/issues",
16
23
  "homepage": "https://github.com/cristianfalcone/ajo#readme",
17
24
  "devDependencies": {
18
- "backdom": "^0.0.2",
19
- "uvu": "^0.5.3"
20
- },
21
- "scripts": {
22
- "test": "uvu"
23
- },
24
- "readme": "# ajo\najo is a JavaScript view library for building user interfaces"
25
- }
25
+ "backdom": "^0.0.4",
26
+ "esbuild": "^0.14.51",
27
+ "uvu": "^0.5.6"
28
+ }
29
+ }
package/readme.md CHANGED
@@ -1,2 +1,62 @@
1
1
  # ajo
2
- ajo is a JavaScript view library for building user interfaces
2
+ ajo is a JavaScript view library for building user interfaces
3
+
4
+ ```sh
5
+ npm install ajo
6
+ ```
7
+
8
+ ## Incremental DOM
9
+ To keep the UI in sync, ajo uses a technique called incremental DOM.
10
+ It is a way to build UI components without the need to keep previous virtual DOM in memory.
11
+ Instead, generated virtual DOM is diffed against the actual DOM, and changes are applied along the way.
12
+ This reduces memory usage and makes ajo code more simple and concise. As a result, ajo is easy to read and maintain, but lacks perfomance oportunities that diffing two virtual DOM trees can provide.
13
+
14
+ ```jsx
15
+ /** @jsx h */
16
+ import { h, render } from 'ajo'
17
+
18
+ document.body.innerHTML = '<div>Hello World</div>'
19
+
20
+ render(document.body, <div>Goodbye World</div>)
21
+ ```
22
+
23
+ ## Stateless components
24
+ As a way to reuse markup snipets, ajo uses simple synchroneous functions that return virtual DOM.
25
+ This type of components are ment to be "consumers" of data.
26
+ No state is preserved between invocations, so generated virtual DOM should rely exclusively on function's arguments.
27
+
28
+ ```jsx
29
+ /** @jsx h */
30
+ import { h, render } from 'ajo'
31
+
32
+ const Greet = ({ name }) => <div>Hello {name}</div>
33
+
34
+ render(document.body, <Greet name="World" />)
35
+ ```
36
+
37
+ ## Stateful components
38
+ Since ajo does not store previous virtual DOM, stateful components rely on a DOM node to preserve its state between UI updates.
39
+ This DOM node is called a host node (similar to a Web Component host node).
40
+ State is declared in a generator function local scope.
41
+ Then ajo asociates the returned iterator with the host, and updates host children nodes each time, retrieving iterator's next value. Lifecycle of these components are closely related to its host nodes, and generator function provides a way to manage them.
42
+
43
+ ```jsx
44
+ /** @jsx h */
45
+ import { h, component, render, refresh } from 'ajo'
46
+
47
+ const Counter = component(({ start = 0 }, host) => {
48
+ let count = start
49
+
50
+ const increment = () => {
51
+ count++
52
+ refresh(host)
53
+ }
54
+
55
+ return () =>
56
+ <button onclick={increment}>
57
+ Current: {count}
58
+ </button>
59
+ })
60
+
61
+ render(document.body, <Counter start={1} />)
62
+ ```