ajo 0.0.4 → 0.0.7

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 +167 -0
  2. package/index.js +165 -167
  3. package/index.test.js +27 -12
  4. package/package.json +20 -16
  5. package/readme.md +61 -1
package/index.cjs ADDED
@@ -0,0 +1,167 @@
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 = (tagName, props, ...children) => {
36
+ children = children.length == 0 ? null : children.length == 1 ? children[0] : children;
37
+ return { children, ...props, tagName };
38
+ }, component = (setup) => ({ is, key, ref, host, ...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 == str) {
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 (hasOwn(h2, "tagName")) {
53
+ const { tagName, key, skip, ref, children, block, ...props } = h2;
54
+ if (key != null && (node = byKey?.get(key)))
55
+ ;
56
+ else
57
+ for (node = child; node; node = node.nextSibling)
58
+ if (node.localName == tagName)
59
+ break;
60
+ node ||= document.createElement(tagName);
61
+ key != null && (byKey ||= keyed.set(host, createMap(Map))).set(key, node);
62
+ update(props, node);
63
+ !(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node);
64
+ isFn(ref) && ref(node);
65
+ } else
66
+ node = h2;
67
+ node === child ? child = child.nextSibling : before(host, child, node);
68
+ }
69
+ while (child) {
70
+ const next = child.nextSibling;
71
+ host.removeChild(child).nodeType == 1 && dispose(child);
72
+ child = next;
73
+ }
74
+ }, refresh = (host, props, setup) => {
75
+ try {
76
+ if (setup && !isFn(setup))
77
+ throwTypeError("Setup", setup, fn);
78
+ props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {};
79
+ render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host);
80
+ } catch (error) {
81
+ propagate(host, error);
82
+ }
83
+ }, provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, createMap(Map))).set(key, value), consume = (host, key, fallback) => {
84
+ let map;
85
+ while (host) {
86
+ if ((map = provisions.get(host)) && map.has(key))
87
+ return map.get(key);
88
+ host = host.parentNode;
89
+ }
90
+ return fallback;
91
+ }, intercept = (host, interceptor) => {
92
+ if (!isFn(interceptor))
93
+ throwTypeError("Interceptor", interceptor, fn);
94
+ interceptors.set(host, interceptor);
95
+ }, propagate = (host, error) => {
96
+ for (let interceptor; host; host = host.parentNode)
97
+ if (interceptor = interceptors.get(host))
98
+ return render(host, interceptor(error));
99
+ throw error;
100
+ }, cleanup = (host, cleaner) => {
101
+ if (!isFn(cleaner))
102
+ throwTypeError("Cleaner", cleaner, fn);
103
+ (cleaners.get(host) ?? cleaners.set(host, /* @__PURE__ */ new Set())).add(cleaner);
104
+ }, 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), {});
105
+ const createMap = (constructor, ...args) => {
106
+ const instance = new constructor(...args);
107
+ const { set } = instance;
108
+ instance.set = (key, value) => (set.call(instance, key, value), value);
109
+ return instance;
110
+ }, throwTypeError = (name, value, expected) => {
111
+ throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`);
112
+ }, 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, search = /([a-z0-9])([A-Z])/g, replace = "$1-$2", str = "string", fn = "function", { keys, entries, hasOwn } = Object, { isArray, from } = Array, wm = WeakMap, deps = createMap(wm), memo = createMap(wm), keyed = createMap(wm), cache = createMap(wm), renders = createMap(wm), cleaners = createMap(wm), provisions = createMap(wm), interceptors = createMap(wm), normalize = function* (h2, host) {
113
+ let type, buffer = "";
114
+ for (h2 of isFn(h2?.[Symbol.iterator]) ? h2 : [h2]) {
115
+ if (h2 == null || (type = typeof h2) == "boolean")
116
+ continue;
117
+ if (h2 instanceof Node || hasOwn(h2, "tagName")) {
118
+ if (isFn(h2.tagName)) {
119
+ yield* normalize(h2.tagName(h2, host), host);
120
+ continue;
121
+ }
122
+ if (buffer) {
123
+ yield buffer;
124
+ buffer = "";
125
+ }
126
+ yield h2;
127
+ continue;
128
+ }
129
+ type == str || !isFn(h2[Symbol.iterator]) ? buffer += h2 : yield* normalize(h2, host);
130
+ }
131
+ if (buffer)
132
+ yield buffer;
133
+ }, update = (props, host) => {
134
+ const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {});
135
+ for (const name in { ...prev, ...props }) {
136
+ let value = props[name];
137
+ if (value === prev[name])
138
+ continue;
139
+ if (name.startsWith("set:")) {
140
+ host[name.slice(4)] = value;
141
+ continue;
142
+ }
143
+ if (value === true)
144
+ value = "";
145
+ else if (value == null || value === false) {
146
+ host.removeAttribute(name);
147
+ continue;
148
+ }
149
+ host.setAttribute(name, value);
150
+ }
151
+ cache.set(host, props);
152
+ }, before = (host, child, node) => {
153
+ if (node.contains?.(document.activeElement)) {
154
+ const ref = node.nextSibling;
155
+ while (child && child !== node) {
156
+ const next = child.nextSibling;
157
+ host.insertBefore(child, ref);
158
+ child = next;
159
+ }
160
+ } else
161
+ host.insertBefore(node, child);
162
+ }, dispose = (host) => {
163
+ for (const child of host.children)
164
+ dispose(child);
165
+ for (const cleaner of cleaners.get(host) ?? [])
166
+ cleaner(host);
167
+ };
package/index.js CHANGED
@@ -1,205 +1,203 @@
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 = (tagName, props, ...children) => {
5
+ children = children.length == 0 ? null : children.length == 1 ? children[0] : children
6
+ return { children, ...props, tagName }
7
+ },
7
8
 
8
- const Element = Symbol()
9
+ component = setup => ({ is, key, ref, host, ...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 createElement = (name, props, ...children) => {
11
- props = { ...props }
12
- children = props.children ?? children
13
- if (children.length > 0) props.children = children.length === 1 ? children[0] : children
14
- props[Element] = name
15
- return props
16
- }
13
+ render = (h, host) => {
17
14
 
18
- function* g(vnode, host) {
19
- const vnodes = isArray(vnode) ? vnode : [vnode]
20
- let data = ''
15
+ let child = host.firstChild, node, byKey = keyed.get(host)
21
16
 
22
- while (vnodes.length > 0) {
23
- vnode = vnodes.shift()
17
+ for (h of normalize(h, host)) {
24
18
 
25
- if (vnode == null || typeof vnode === 'boolean') continue
26
- if (typeof vnode === 'string') data += vnode
27
- else if (hasOwn(vnode, Element)) {
28
- const { [Element]: name } = vnode
19
+ if (typeof h == str) {
29
20
 
30
- if (typeof name === 'function') {
31
- vnodes.unshift(name(vnode, host))
32
- continue
33
- }
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)
34
23
 
35
- if (data) {
36
- yield { [Element]: '#text', data }
37
- data = ''
38
- }
24
+ } else if (hasOwn(h, 'tagName')) {
39
25
 
40
- yield vnode
41
- } else typeof vnode[Symbol.iterator] === 'function'
42
- ? vnodes.unshift(...vnode)
43
- : data += vnode
44
- }
26
+ const { tagName, key, skip, ref, children, block, ...props } = h
27
+
28
+ if (key != null && (node = byKey?.get(key)));
29
+ else for (node = child; node; node = node.nextSibling) if (node.localName == tagName) break
45
30
 
46
- if (data) yield { [Element]: '#text', data }
47
- }
31
+ node ||= document.createElement(tagName)
48
32
 
49
- export const render = (vnode, host) => {
50
- if (host?.nodeType !== Node.ELEMENT_NODE) return
33
+ key != null && (byKey ||= keyed.set(host, createMap(Map))).set(key, node)
51
34
 
52
- let child = host.firstChild
35
+ update(props, node)
53
36
 
54
- for (const { [Element]: name, ref, children, ...props } of g(vnode, host)) {
37
+ !(skip || block != null && every(deps.get(node), deps.set(node, block))) && render(children, node)
55
38
 
56
- if (name === Skip) {
57
- child = props.end ? null : child?.nextSibling ?? null
58
- continue
39
+ isFn(ref) && ref(node)
40
+
41
+ } else node = h
42
+
43
+ node === child ? child = child.nextSibling : before(host, child, node)
59
44
  }
60
45
 
61
- let node = child
46
+ while (child) {
47
+ const next = child.nextSibling
48
+ host.removeChild(child).nodeType == 1 && dispose(child)
49
+ child = next
50
+ }
51
+ },
62
52
 
63
- while (node != null) {
64
- if (node.nodeName.toLowerCase() === name && node.getAttribute?.('key') == props.key) break
65
- node = node.nextSibling
53
+ refresh = (host, props, setup) => {
54
+ try {
55
+ if (setup && !isFn(setup)) throwTypeError('Setup', setup, fn)
56
+ props = props ? memo.set(host, { ...setup?.props, ...props }) : memo.get(host) ?? {}
57
+ render((renders.get(host) ?? renders.set(host, setup(props, host)))(props, host), host)
58
+ } catch (error) {
59
+ propagate(host, error)
66
60
  }
61
+ },
67
62
 
68
- if (node == null) node = name === '#text'
69
- ? document.createTextNode('')
70
- : document.createElement(name)
63
+ provide = (host, key, value) => (provisions.get(host) ?? provisions.set(host, createMap(Map))).set(key, value),
71
64
 
72
- patch(props, node)
65
+ consume = (host, key, fallback) => {
66
+ let map
67
+ while (host) {
68
+ if ((map = provisions.get(host)) && map.has(key)) return map.get(key)
69
+ host = host.parentNode
70
+ }
71
+ return fallback
72
+ },
73
+
74
+ intercept = (host, interceptor) => {
75
+ if (!isFn(interceptor)) throwTypeError('Interceptor', interceptor, fn)
76
+ interceptors.set(host, interceptor)
77
+ },
78
+
79
+ propagate = (host, error) => {
80
+ for (let interceptor; host; host = host.parentNode)
81
+ if (interceptor = interceptors.get(host)) return render(host, interceptor(error))
82
+ throw error
83
+ },
84
+
85
+ cleanup = (host, cleaner) => {
86
+ if (!isFn(cleaner)) throwTypeError('Cleaner', cleaner, fn);
87
+ (cleaners.get(host) ?? cleaners.set(host, new Set)).add(cleaner)
88
+ },
89
+
90
+ clx = o => keys(o).filter(k => o[k]).join(' ') || null,
91
+ stx = o => entries(o).map(t => t.join(':')).join(';') || null,
92
+ keb = o => keys(o).reduce((r, k) => ((r[k.replace(search, replace).toLowerCase()] = o[k]), r), {})
93
+
94
+ const
95
+ createMap = (constructor, ...args) => {
96
+ const instance = new constructor(...args)
97
+ const { set } = instance
98
+ instance.set = (key, value) => (set.call(instance, key, value), value)
99
+ return instance
100
+ },
101
+
102
+ throwTypeError = (name, value, expected) => {
103
+ throw new TypeError(`Expected ${name} to be of type ${expected}, got ${typeof value} instead`)
104
+ },
105
+
106
+ every = (a, b) => a === b || isArray(a) && isArray(b) && a.length == b.length && a.every((v, i) => v === b[i]),
107
+ apply = (o, { name, value }) => ((o[name] = value), o),
108
+ reduce = list => from(list).reduce(apply, {}),
109
+ isFn = v => typeof v == fn,
110
+
111
+ search = /([a-z0-9])([A-Z])/g,
112
+ replace = '$1-$2',
113
+ str = 'string',
114
+ fn = 'function',
115
+
116
+ { keys, entries, hasOwn } = Object,
117
+ { isArray, from } = Array,
118
+ wm = WeakMap,
119
+
120
+ deps = createMap(wm),
121
+ memo = createMap(wm),
122
+ keyed = createMap(wm),
123
+ cache = createMap(wm),
124
+ renders = createMap(wm),
125
+ cleaners = createMap(wm),
126
+ provisions = createMap(wm),
127
+ interceptors = createMap(wm),
128
+
129
+ normalize = function* (h, host) {
130
+
131
+ let type, buffer = ''
132
+
133
+ for (h of isFn(h?.[Symbol.iterator]) ? h : [h]) {
134
+
135
+ if (h == null || (type = typeof h) == 'boolean') continue
136
+
137
+ if (h instanceof Node || hasOwn(h, 'tagName')) {
138
+
139
+ if (isFn(h.tagName)) {
140
+ yield* normalize(h.tagName(h, host), host)
141
+ continue
142
+ }
143
+
144
+ if (buffer) {
145
+ yield buffer
146
+ buffer = ''
147
+ }
148
+
149
+ yield h
150
+ continue
151
+ }
73
152
 
74
- if (typeof ref === 'function') ref(node)
75
- else if (typeof ref === 'object' && ref !== null) ref.current = node
76
-
77
- render(children, node)
153
+ (type == str || !isFn(h[Symbol.iterator])) ? buffer += h : yield* normalize(h, host)
154
+ }
78
155
 
79
- if (node === child) child = child.nextSibling
80
- else if (node.contains?.(document.activeElement)) {
81
- const { nextSibling } = node
82
- let ref = child
156
+ if (buffer) yield buffer
157
+ },
83
158
 
84
- while (ref != null && ref !== node) {
85
- const next = ref.nextSibling
86
- host.insertBefore(ref, nextSibling)
87
- ref = next
88
- }
89
- } else host.insertBefore(node, child)
90
- }
159
+ update = (props, host) => {
91
160
 
92
- while (child != null) {
93
- const { nodeType, nextSibling } = child
94
- if (nodeType === Node.ELEMENT_NODE) dispose(child)
95
- host.removeChild(child)
96
- child = nextSibling
97
- }
98
- }
161
+ const prev = cache.get(host) ?? (host.hasAttributes() ? reduce(host.attributes) : {})
99
162
 
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
- }
163
+ for (const name in { ...prev, ...props }) {
105
164
 
106
- for (const name of new Set([...node.getAttributeNames(), ...keys(props)])) {
107
- let value = props[name]
165
+ let value = props[name]
108
166
 
109
- if (name in node && !(typeof value === 'string' && typeof node[name] === 'boolean')) {
110
- try {
111
- if (node[name] !== value) node[name] = value
167
+ if (value === prev[name]) continue
168
+
169
+ if (name.startsWith('set:')) {
170
+ host[name.slice(4)] = value
112
171
  continue
113
- } catch (err) { }
114
- }
172
+ }
115
173
 
116
- if (value === true) value = ''
117
- else if (value == null || value === false) {
118
- node.removeAttribute(name)
119
- continue
174
+ if (value === true) value = ''
175
+ else if (value == null || value === false) {
176
+ host.removeAttribute(name)
177
+ continue
178
+ }
179
+
180
+ host.setAttribute(name, value)
120
181
  }
121
182
 
122
- if (node.getAttribute(name) !== value) node.setAttribute(name, value)
123
- }
124
- }
183
+ cache.set(host, props)
184
+ },
125
185
 
126
- const components = new WeakMap
186
+ before = (host, child, node) => {
187
+ if (node.contains?.(document.activeElement)) {
127
188
 
128
- export const createComponent = fn => ({ is, host, key, ...props }) =>
129
- createElement(is ?? fn.is ?? 'div', {
130
- ...fn.host, ...host, key, 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
- }
189
+ const ref = node.nextSibling
146
190
 
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 }
150
-
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)
191
+ while (child && child !== node) {
192
+ const next = child.nextSibling
193
+ host.insertBefore(child, ref)
194
+ child = next
155
195
  }
156
196
 
157
- const { value, done } = this.it[method](arg)
158
-
159
- render(value, this.host)
160
- if (done) this.host = null
161
- } catch (err) {
162
- this.err = true
163
- propagate(this.host.parentNode, err)
164
- }
165
- }
197
+ } else host.insertBefore(node, child)
198
+ },
166
199
 
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
- }
200
+ dispose = host => {
201
+ for (const child of host.children) dispose(child)
202
+ for (const cleaner of cleaners.get(host) ?? []) cleaner(host)
204
203
  }
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 { 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, { tagName: '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' }), { tagName: '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'), { tagName: '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, { tagName: '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, { tagName: 'span', children: null })
26
35
  })
27
36
 
28
37
  it.run()
@@ -33,16 +42,22 @@ 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
- it('should render a component', () => {
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
 
56
+ it('should render a stateful component', () => {
57
+ const host = document.createElement('div')
58
+ const Foo = component(() => ({ foo, children }) => h('div', { foo }, children))
59
+ render(h(Foo, { is: 'foo-component', foo: 'bar' }, h('span', null, 'foobar')), host)
60
+ assert.snapshot(host.innerHTML, '<foo-component><div foo="bar"><span>foobar</span></div></foo-component>')
61
+ })
62
+
48
63
  it.run()
package/package.json CHANGED
@@ -1,25 +1,29 @@
1
1
  {
2
2
  "name": "ajo",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
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
+ ```