ajo 0.0.3 → 0.0.6

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.
package/cjs/index.js ADDED
@@ -0,0 +1,224 @@
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, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, 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
+ Portal: () => Portal,
22
+ Skip: () => Skip,
23
+ consume: () => consume,
24
+ createComponent: () => createComponent,
25
+ createElement: () => createElement,
26
+ createTextNode: () => createTextNode,
27
+ isElement: () => isElement,
28
+ provide: () => provide,
29
+ render: () => render
30
+ });
31
+ module.exports = __toCommonJS(ajo_exports);
32
+ const { isArray } = Array;
33
+ const { hasOwn, keys } = Object;
34
+ const Skip = Symbol();
35
+ const Fragment = ({ children }) => children;
36
+ const Portal = ({ host, children }) => render(host, children);
37
+ const Element = Symbol();
38
+ const createTextNode = (data) => ({ [Element]: "#text", data, skip: true });
39
+ const createElement = (name, props, ...children) => {
40
+ props = { ...props, [Element]: name };
41
+ children = props.children ?? children;
42
+ if (children.length > 0)
43
+ props.children = children.length === 1 ? children[0] : children;
44
+ return props;
45
+ };
46
+ const isElement = (el) => hasOwn(el ?? {}, Element);
47
+ function* g(vnode, host) {
48
+ const vnodes = isArray(vnode) ? vnode : [vnode];
49
+ let data = "";
50
+ while (vnodes.length > 0) {
51
+ vnode = vnodes.shift();
52
+ if (vnode == null || typeof vnode === "boolean")
53
+ continue;
54
+ if (typeof vnode === "string")
55
+ data += vnode;
56
+ else if (isElement(vnode)) {
57
+ const { [Element]: name } = vnode;
58
+ if (typeof name === "function") {
59
+ vnodes.unshift(name(vnode, host));
60
+ continue;
61
+ }
62
+ if (data) {
63
+ yield createTextNode(data);
64
+ data = "";
65
+ }
66
+ yield vnode;
67
+ } else
68
+ typeof vnode[Symbol.iterator] === "function" ? vnodes.unshift(...vnode) : data += vnode;
69
+ }
70
+ if (data)
71
+ yield createTextNode(data);
72
+ }
73
+ const render = (vnode, host) => {
74
+ let child = host.firstChild;
75
+ for (const { [Element]: name, ref, skip, children, ...props } of g(vnode, host)) {
76
+ if (name === Skip) {
77
+ child = props.end ? null : child?.nextSibling ?? null;
78
+ continue;
79
+ }
80
+ let node = child;
81
+ while (node != null) {
82
+ if (node.nodeName.toLowerCase() === name && node.getAttribute?.("key") == props.key)
83
+ break;
84
+ node = node.nextSibling;
85
+ }
86
+ if (node == null)
87
+ node = name === "#text" ? document.createTextNode("") : document.createElement(name);
88
+ patch(props, node);
89
+ if (typeof ref === "function")
90
+ ref(node);
91
+ else if (typeof ref === "object" && ref !== null)
92
+ ref.current = node;
93
+ skip || render(children, node);
94
+ if (node === child)
95
+ child = child.nextSibling;
96
+ else if (node.contains?.(document.activeElement)) {
97
+ const { nextSibling } = node;
98
+ let ref2 = child;
99
+ while (ref2 != null && ref2 !== node) {
100
+ const next = ref2.nextSibling;
101
+ host.insertBefore(ref2, nextSibling);
102
+ ref2 = next;
103
+ }
104
+ } else
105
+ host.insertBefore(node, child);
106
+ }
107
+ while (child != null) {
108
+ const { nextSibling } = child;
109
+ if (child.nodeType === Node.ELEMENT_NODE)
110
+ dispose(child);
111
+ host.removeChild(child);
112
+ child = nextSibling;
113
+ }
114
+ };
115
+ const patch = (props, node) => {
116
+ if (node.nodeType === Node.TEXT_NODE) {
117
+ if (node.data !== props.data)
118
+ node.data = props.data;
119
+ return;
120
+ }
121
+ for (const name of /* @__PURE__ */ new Set([...node.getAttributeNames(), ...keys(props)])) {
122
+ let value = props[name];
123
+ if (name in node && !(typeof value === "string" && typeof node[name] === "boolean")) {
124
+ try {
125
+ if (node[name] !== value)
126
+ node[name] = value;
127
+ continue;
128
+ } catch (err) {
129
+ }
130
+ }
131
+ if (value === true)
132
+ value = "";
133
+ else if (value == null || value === false) {
134
+ node.removeAttribute(name);
135
+ continue;
136
+ }
137
+ if (node.getAttribute(name) !== value)
138
+ node.setAttribute(name, value);
139
+ }
140
+ };
141
+ const components = /* @__PURE__ */ new WeakMap();
142
+ const createComponent = (fn) => ({ is, host, key, ...props }) => createElement(is ?? fn.is ?? "div", {
143
+ ...fn.host,
144
+ ...host,
145
+ key,
146
+ skip: true,
147
+ ref: (host2) => {
148
+ let cmp = components.get(host2);
149
+ if (cmp == null)
150
+ components.set(host2, cmp = new Component(host2, fn));
151
+ cmp.props = { ...fn.props, ...props };
152
+ cmp.update();
153
+ }
154
+ });
155
+ class Component {
156
+ constructor(host, fn) {
157
+ this.host = host;
158
+ this.fn = fn;
159
+ this.props = null;
160
+ this.it = null;
161
+ this.err = false;
162
+ }
163
+ update({ method = "next", arg } = {}) {
164
+ if (this.host == null)
165
+ return;
166
+ if (this.err)
167
+ try {
168
+ this.it.return();
169
+ } catch (e) {
170
+ this.host = null;
171
+ } finally {
172
+ return;
173
+ }
174
+ try {
175
+ if (this.it == null) {
176
+ const init = this.fn.call(this, this.props, this.host);
177
+ this.it = typeof init?.next === "function" ? init : generate.call(this, init);
178
+ }
179
+ const { value, done } = this.it[method](arg);
180
+ render(value, this.host);
181
+ if (done)
182
+ this.host = null;
183
+ } catch (err) {
184
+ this.err = true;
185
+ propagate(this.host.parentNode, err);
186
+ }
187
+ }
188
+ *[Symbol.iterator]() {
189
+ while (this.host)
190
+ yield this.props;
191
+ }
192
+ }
193
+ function* generate(init) {
194
+ yield init;
195
+ for (const props of this)
196
+ yield this.fn.call(this, props, this.host);
197
+ }
198
+ const propagate = (el, err) => {
199
+ if (el == null)
200
+ throw err;
201
+ const cmp = components.get(el);
202
+ typeof cmp?.it?.throw === "function" ? cmp.update({ method: "throw", arg: err }) : propagate(el.parentNode, err);
203
+ };
204
+ const dispose = (el) => {
205
+ for (const child of el.children)
206
+ dispose(child);
207
+ components.get(el)?.update({ method: "return" });
208
+ };
209
+ const provisions = /* @__PURE__ */ new WeakMap();
210
+ const provide = (host, key, value) => {
211
+ let map = provisions.get(host);
212
+ if (map == null)
213
+ provisions.set(host, map = /* @__PURE__ */ new Map());
214
+ map.set(key, value);
215
+ };
216
+ const consume = (host, key) => {
217
+ for (let node = host; node != null; node = node.parentNode) {
218
+ if (provisions.has(node)) {
219
+ const map = provisions.get(node);
220
+ if (map.has(key))
221
+ return map.get(key);
222
+ }
223
+ }
224
+ };
package/index.js CHANGED
@@ -7,12 +7,17 @@ export const Portal = ({ host, children }) => render(host, children)
7
7
 
8
8
  const Element = Symbol()
9
9
 
10
+ export const createTextNode = data => ({ [Element]: '#text', data, skip: true })
11
+
10
12
  export const createElement = (name, props, ...children) => {
11
- const { length } = children
12
- if (length > 0) (props ?? (props = {})).children = length === 1 ? children[0] : children
13
- return { ...props, [Element]: name }
13
+ props = { ...props, [Element]: name }
14
+ children = props.children ?? children
15
+ if (children.length > 0) props.children = children.length === 1 ? children[0] : children
16
+ return props
14
17
  }
15
18
 
19
+ export const isElement = el => hasOwn(el ?? {}, Element)
20
+
16
21
  function* g(vnode, host) {
17
22
  const vnodes = isArray(vnode) ? vnode : [vnode]
18
23
  let data = ''
@@ -22,7 +27,7 @@ function* g(vnode, host) {
22
27
 
23
28
  if (vnode == null || typeof vnode === 'boolean') continue
24
29
  if (typeof vnode === 'string') data += vnode
25
- else if (hasOwn(vnode, Element)) {
30
+ else if (isElement(vnode)) {
26
31
  const { [Element]: name } = vnode
27
32
 
28
33
  if (typeof name === 'function') {
@@ -31,7 +36,7 @@ function* g(vnode, host) {
31
36
  }
32
37
 
33
38
  if (data) {
34
- yield { [Element]: '#text', data }
39
+ yield createTextNode(data)
35
40
  data = ''
36
41
  }
37
42
 
@@ -41,13 +46,13 @@ function* g(vnode, host) {
41
46
  : data += vnode
42
47
  }
43
48
 
44
- if (data) yield { [Element]: '#text', data }
49
+ if (data) yield createTextNode(data)
45
50
  }
46
51
 
47
52
  export const render = (vnode, host) => {
48
53
  let child = host.firstChild
49
54
 
50
- for (const { [Element]: name, ref, children, ...props } of g(vnode, host)) {
55
+ for (const { [Element]: name, ref, skip, children, ...props } of g(vnode, host)) {
51
56
 
52
57
  if (name === Skip) {
53
58
  child = props.end ? null : child?.nextSibling ?? null
@@ -65,13 +70,12 @@ export const render = (vnode, host) => {
65
70
  ? document.createTextNode('')
66
71
  : document.createElement(name)
67
72
 
68
- if (name === '#text') {
69
- if (node.data !== props.data) node.data = props.data
70
- } else {
71
- patch(props, node)
72
- setRef(ref, node)
73
- render(children, node)
74
- }
73
+ patch(props, node)
74
+
75
+ if (typeof ref === 'function') ref(node)
76
+ else if (typeof ref === 'object' && ref !== null) ref.current = node
77
+
78
+ skip || render(children, node)
75
79
 
76
80
  if (node === child) child = child.nextSibling
77
81
  else if (node.contains?.(document.activeElement)) {
@@ -87,14 +91,19 @@ export const render = (vnode, host) => {
87
91
  }
88
92
 
89
93
  while (child != null) {
90
- const { nodeType, nextSibling } = child
91
- if (nodeType === 1) dispose(child)
94
+ const { nextSibling } = child
95
+ if (child.nodeType === Node.ELEMENT_NODE) dispose(child)
92
96
  host.removeChild(child)
93
97
  child = nextSibling
94
98
  }
95
99
  }
96
100
 
97
101
  const patch = (props, node) => {
102
+ if (node.nodeType === Node.TEXT_NODE) {
103
+ if (node.data !== props.data) node.data = props.data
104
+ return
105
+ }
106
+
98
107
  for (const name of new Set([...node.getAttributeNames(), ...keys(props)])) {
99
108
  let value = props[name]
100
109
 
@@ -115,21 +124,15 @@ const patch = (props, node) => {
115
124
  }
116
125
  }
117
126
 
118
- const setRef = (ref, node) => {
119
- if (typeof ref === 'function') ref(node)
120
- else if (typeof ref === 'object' && ref !== null) ref.current = node
121
- }
122
-
123
127
  const components = new WeakMap
124
128
 
125
- export const createComponent = fn => ({ is, host, key, ref, ...props }) =>
129
+ export const createComponent = fn => ({ is, host, key, ...props }) =>
126
130
  createElement(is ?? fn.is ?? 'div', {
127
- ...fn.host, ...host, key, ['ref']: host => {
131
+ ...fn.host, ...host, key, skip: true, ref: host => {
128
132
  let cmp = components.get(host)
129
133
  if (cmp == null) components.set(host, cmp = new Component(host, fn))
130
134
  cmp.props = { ...fn.props, ...props }
131
135
  cmp.update()
132
- setRef(ref, host)
133
136
  }
134
137
  })
135
138
 
@@ -163,7 +166,7 @@ class Component {
163
166
  }
164
167
 
165
168
  *[Symbol.iterator]() {
166
- while (true) yield this.props
169
+ while (this.host) yield this.props
167
170
  }
168
171
  }
169
172
 
package/index.test.js CHANGED
@@ -1,7 +1,7 @@
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 { createComponent, createElement, isElement, render, } from './index.js'
5
5
 
6
6
  let it
7
7
 
@@ -10,7 +10,9 @@ let it
10
10
  it = suite('createElement')
11
11
 
12
12
  it('should create empty vnode', () => {
13
- assert.equal(createElement('div'), {})
13
+ const vnode = createElement('div')
14
+ assert.ok(isElement(vnode))
15
+ assert.equal(vnode, {})
14
16
  })
15
17
 
16
18
  it('should create vnode with props', () => {
@@ -22,7 +24,15 @@ it('should create vnode with one string child', () => {
22
24
  })
23
25
 
24
26
  it('should create vnode with one vnode child', () => {
25
- assert.equal(createElement('div', null, createElement('span')), { children: {} })
27
+ const child = createElement('span')
28
+ const vnode = createElement('div', null, child)
29
+ assert.ok(isElement(vnode.children))
30
+ })
31
+
32
+ it('should allow children as prop', () => {
33
+ const child = createElement('span')
34
+ const vnode = createElement('div', { children: child })
35
+ assert.ok(isElement(vnode.children))
26
36
  })
27
37
 
28
38
  it.run()
@@ -37,4 +47,19 @@ it('should render a vnode', () => {
37
47
  assert.snapshot(host.innerHTML, '<div foo="bar">foobar</div>')
38
48
  })
39
49
 
50
+ it('should render a stateless component', () => {
51
+ const host = document.createElement('div')
52
+ const Foo = ({ foo, children }) => createElement('div', { foo }, children)
53
+ debugger
54
+ render(createElement(Foo, { foo: 'bar' }, createElement('span', null, 'foobar')), host)
55
+ assert.snapshot(host.innerHTML, '<div foo="bar"><span>foobar</span></div>')
56
+ })
57
+
58
+ it('should render a stateful component', () => {
59
+ const host = document.createElement('div')
60
+ const Foo = createComponent(({ foo, children }) => createElement('div', { foo }, children))
61
+ render(createElement(Foo, { is: 'foo-component', foo: 'bar' }, createElement('span', null, 'foobar')), host)
62
+ assert.snapshot(host.innerHTML, '<foo-component><div foo="bar"><span>foobar</span></div></foo-component>')
63
+ })
64
+
40
65
  it.run()
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "ajo",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "description": "ajo is a JavaScript view library for building user interfaces",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "cjs/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./index.js",
10
+ "require": "./cjs/index.js"
11
+ }
12
+ },
7
13
  "repository": {
8
14
  "type": "git",
9
15
  "url": "git+https://github.com/cristianfalcone/ajo.git"
@@ -15,11 +21,13 @@
15
21
  },
16
22
  "homepage": "https://github.com/cristianfalcone/ajo#readme",
17
23
  "devDependencies": {
18
- "backdom": "^0.0.2",
24
+ "backdom": "^0.0.3",
25
+ "esbuild": "^0.14.31",
19
26
  "uvu": "^0.5.3"
20
27
  },
21
28
  "scripts": {
29
+ "build": "esbuild --outdir=cjs --format=cjs index.js",
22
30
  "test": "uvu"
23
31
  },
24
- "readme": "# ajo\najo is a JavaScript view library for building user interfaces"
32
+ "readme": "# ajo\najo is a JavaScript view library for building user interfaces\n\n```bash\nnpm install ajo\n```\n\n## Incremental DOM\nTo keep the UI in sync, ajo uses a technique called incremental DOM.\nIt is a way to build UI components without the need to keep previous virtual DOM in memory.\nInstead, generated virtual DOM is diffed against the actual DOM, and changes are applied along the way.\nThis 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.\n\n```jsx\n/** @jsx createElement */\nimport { render, createElement } from 'ajo'\n\ndocument.body.innerHTML = '<div>Hello World</div>'\n\nrender(<div>Goodbye World</div>, document.body)\n```\n\n## Stateless components\nAs a way to reuse markup snipets, ajo uses simple synchroneous functions that return virtual DOM.\nThis type of components are ment to be \"consumers\" of data.\nNo state is preserved between invocations, so generated virtual DOM should rely exclusively on function's arguments. \n\n```jsx\n/** @jsx createElement */\nimport { render, createElement } from 'ajo'\n\nconst Greet = ({ name }) => <div>Hello {name}</div>\n\nrender(<Greet name=\"World\" />, document.body)\n```\n\n## Stateful components\nSince ajo does not store previous virtual DOM, stateful components rely on a DOM node to preserve its state between UI updates.\nThis DOM node is called a host node (similar to a Web Component host node).\nState is declared in a generator function local scope.\nThen 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.\n\n```jsx\n/** @jsx createElement */\nimport { render, createElement, createComponent } from 'ajo'\n\nconst Counter = createComponent(function* () {\n\tlet count = 1\n\t\n\tconst increment = () => {\n\t\tcount++\n\t\tthis.update()\n\t}\n\t\n\tfor ({} of this) yield (\n\t\t<button onclick={increment}>\n\t\t\tCurrent: {count}\n\t\t</button>\n\t)\n})\n\nrender(<Counter />, document.body)\n```\n"
25
33
  }
package/readme.md CHANGED
@@ -1,2 +1,63 @@
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
+ ```bash
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 createElement */
16
+ import { render, createElement } from 'ajo'
17
+
18
+ document.body.innerHTML = '<div>Hello World</div>'
19
+
20
+ render(<div>Goodbye World</div>, document.body)
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 createElement */
30
+ import { render, createElement } from 'ajo'
31
+
32
+ const Greet = ({ name }) => <div>Hello {name}</div>
33
+
34
+ render(<Greet name="World" />, document.body)
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 createElement */
45
+ import { render, createElement, createComponent } from 'ajo'
46
+
47
+ const Counter = createComponent(function* () {
48
+ let count = 1
49
+
50
+ const increment = () => {
51
+ count++
52
+ this.update()
53
+ }
54
+
55
+ for ({} of this) yield (
56
+ <button onclick={increment}>
57
+ Current: {count}
58
+ </button>
59
+ )
60
+ })
61
+
62
+ render(<Counter />, document.body)
63
+ ```