@lukekaalim/act-web 3.4.0-alpha.2 → 4.0.0
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/CHANGELOG.md +39 -0
- package/README.md +38 -0
- package/docs/index.ts +5 -0
- package/mod.ts +1 -0
- package/package.json +9 -4
- package/render.ts +29 -5
- package/scheduler.ts +42 -15
- package/server.ts +91 -0
- package/space.ts +62 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @lukekaalim/act-web
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 6658c01: Internal Refactor!
|
|
8
|
+
- afd247e: Another major refactor! So everything is broken. Good luck!
|
|
9
|
+
|
|
10
|
+
### Minor Changes
|
|
11
|
+
|
|
12
|
+
- bd0a076: Add support for data-\* attributes on HTML elements
|
|
13
|
+
- b3f6c49: Added debug capabilities and protocol
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 7597a8f: Added "Style" prop support to SVG
|
|
18
|
+
- ccb3900: Reconciler should apply all changes in the correct order, and not skip any.
|
|
19
|
+
- Reconciler does send out a Render command once it completes all pending renders (called a "ThreadStack")
|
|
20
|
+
Scheduler has been updated to perform some updates in Sync.
|
|
21
|
+
- Updated dependencies [6658c01]
|
|
22
|
+
- Updated dependencies [fdf1557]
|
|
23
|
+
- Updated dependencies [ccb3900]
|
|
24
|
+
- Updated dependencies [afd247e]
|
|
25
|
+
- Updated dependencies [2984273]
|
|
26
|
+
- Updated dependencies [b3f6c49]
|
|
27
|
+
- Updated dependencies [c5e8775]
|
|
28
|
+
- @lukekaalim/act-backstage@3.0.0
|
|
29
|
+
- @lukekaalim/act-recon@3.0.0
|
|
30
|
+
- @lukekaalim/act@4.0.0
|
|
31
|
+
|
|
32
|
+
## 3.4.0-alpha.3
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- ccb3900: Reconciler should apply all changes in the correct order, and not skip any.
|
|
37
|
+
- Reconciler does send out a Render command once it completes all pending renders (called a "ThreadStack")
|
|
38
|
+
Scheduler has been updated to perform some updates in Sync.
|
|
39
|
+
- Updated dependencies [ccb3900]
|
|
40
|
+
- @lukekaalim/act-recon@3.0.0-alpha.3
|
|
41
|
+
|
|
3
42
|
## 3.4.0-alpha.2
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @lukekaalim/act-web
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm i @lukekaalim/act-web
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { h, Component } from "@lukekaalim/act";
|
|
9
|
+
import { render } from "@lukekaalim/act-web";
|
|
10
|
+
|
|
11
|
+
const MyApp = () => {
|
|
12
|
+
return h('article', {}, [
|
|
13
|
+
h('h1', {}, "Hello, world!")
|
|
14
|
+
])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
render(h(MyApp), document.body)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
<TypeDoc project="@lukekaalim/act-web" name="render" />
|
|
21
|
+
|
|
22
|
+
## Capabilities
|
|
23
|
+
|
|
24
|
+
- [x] Supports HTML elements and SVG elements
|
|
25
|
+
- [x] Add in the `web:html` or `web:svg` root pick the correct type
|
|
26
|
+
- [x] Prop support
|
|
27
|
+
- [x] **Attributes.** Most props will just be assigned directly to the element
|
|
28
|
+
- [x] **Event Listeners.** `onClick` adds an event listener for `click`
|
|
29
|
+
- [x] **Ref.** `ref` will attach a `HTMLElement` to `ref.current`
|
|
30
|
+
- [x] **Styles.** `style` will set styles directly on the element
|
|
31
|
+
- [x] **ClassList.** An array of strings or falseish values will be added as classes via `classList`
|
|
32
|
+
|
|
33
|
+
## Builder
|
|
34
|
+
|
|
35
|
+
If you are composing another renderer and want to give
|
|
36
|
+
it web powers, use the builder:
|
|
37
|
+
|
|
38
|
+
<TypeDoc project="@lukekaalim/act-web" name="createWebNodeBuilder" />
|
package/docs/index.ts
ADDED
package/mod.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lukekaalim/act-web",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "mod.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@lukekaalim/act": "^
|
|
8
|
-
"@lukekaalim/act-
|
|
9
|
-
"@lukekaalim/act-
|
|
7
|
+
"@lukekaalim/act": "^4.0.0",
|
|
8
|
+
"@lukekaalim/act-backstage": "^3.0.0",
|
|
9
|
+
"@lukekaalim/act-recon": "^3.0.0",
|
|
10
|
+
"@types/hast": "^3.0.4",
|
|
11
|
+
"hast-util-to-html": "^9.0.5",
|
|
12
|
+
"hastscript": "^9.0.1",
|
|
13
|
+
"rehype": "^13.0.2",
|
|
14
|
+
"rehype-stringify": "^10.0.1"
|
|
10
15
|
}
|
|
11
16
|
}
|
package/render.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
import { h } from "@lukekaalim/act";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { h, Node } from "@lukekaalim/act";
|
|
2
|
+
import { createWebNodeBuilder, HTML } from "./space";
|
|
3
|
+
import { RenderSpace2 } from "@lukekaalim/act-backstage";
|
|
4
4
|
import { createDOMScheduler } from "./scheduler";
|
|
5
|
+
import { Reconciler2 } from "@lukekaalim/act-recon";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Custom options for web.render
|
|
9
|
+
*/
|
|
10
|
+
type Options = {
|
|
11
|
+
window?: Window;
|
|
12
|
+
}
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Render a node into the page.
|
|
16
|
+
*
|
|
17
|
+
*
|
|
18
|
+
* @param node
|
|
19
|
+
* @param root
|
|
20
|
+
* @param options
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
23
|
+
export const render = (node: Node, root: HTMLElement, options: Options = {}) => {
|
|
24
|
+
const scheduler = createDOMScheduler();
|
|
25
|
+
const reconciler = new Reconciler2(scheduler);
|
|
26
|
+
const space = new RenderSpace2(reconciler.tree, createWebNodeBuilder(root, options.window));
|
|
27
|
+
|
|
28
|
+
reconciler.bus = space.bus;
|
|
29
|
+
const ref = reconciler.mount(h(HTML, {}, node));
|
|
30
|
+
|
|
31
|
+
return {reconciler, space,ref};
|
|
32
|
+
}
|
package/scheduler.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
-
import { createId } from "@lukekaalim/act";
|
|
2
1
|
import { Scheduler } from "@lukekaalim/act-recon";
|
|
3
2
|
|
|
4
3
|
export const createDOMScheduler = (): Scheduler => {
|
|
5
|
-
const workMap = new Map();
|
|
6
4
|
let id: number | null = null;
|
|
7
|
-
|
|
5
|
+
let callbackFunc = () => console.error(`DOMScheduler got callback before callback function was configured`)
|
|
6
|
+
let synccall_available = false;
|
|
7
|
+
let synccall_requested = false;
|
|
8
|
+
const time_budget = 60;
|
|
9
|
+
|
|
10
|
+
const onTimeout = () => {
|
|
11
|
+
const start = performance.now();
|
|
8
12
|
id = null;
|
|
9
|
-
for (const [,callback] of workMap)
|
|
10
|
-
callback();
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
synccall_available = true;
|
|
15
|
+
// at least 1 call
|
|
16
|
+
callbackFunc();
|
|
17
|
+
|
|
18
|
+
// if callback func re-requested a call,
|
|
19
|
+
// do the rest in sync
|
|
20
|
+
while (synccall_requested) {
|
|
21
|
+
synccall_requested = false;
|
|
22
|
+
const now = performance.now();
|
|
23
|
+
|
|
24
|
+
if (now - start >= time_budget) {
|
|
25
|
+
synccall_available = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
callbackFunc();
|
|
29
|
+
}
|
|
30
|
+
synccall_available = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
setCallbackFunc(newCallbackFunc) {
|
|
35
|
+
callbackFunc = newCallbackFunc;
|
|
36
|
+
},
|
|
37
|
+
isCallbackPending() {
|
|
38
|
+
return id !== null;
|
|
39
|
+
},
|
|
40
|
+
requestCallback() {
|
|
41
|
+
if (synccall_available) {
|
|
42
|
+
synccall_requested = true;
|
|
43
|
+
}
|
|
44
|
+
else if (!id) {
|
|
45
|
+
id = window.setTimeout(onTimeout, 0);
|
|
46
|
+
}
|
|
21
47
|
},
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
cancelCallback() {
|
|
49
|
+
if (id !== null)
|
|
50
|
+
window.clearTimeout(id);
|
|
24
51
|
},
|
|
25
52
|
}
|
|
26
53
|
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { h, s } from 'hastscript';
|
|
2
|
+
import { Element, Nodes as HNode, Root } from 'hast';
|
|
3
|
+
import { NodeBuilder, RenderSpace2 } from "@lukekaalim/act-backstage";
|
|
4
|
+
import { primitiveNodeTypes } from '@lukekaalim/act';
|
|
5
|
+
import { CommitTree2 } from '@lukekaalim/act-recon';
|
|
6
|
+
|
|
7
|
+
export const createHASTBuilder = (root: Root): NodeBuilder<HNode, 'web:html' | 'web:svg'> => ({
|
|
8
|
+
roots: new Set(['web:html', 'web:svg']),
|
|
9
|
+
|
|
10
|
+
create(element, root): HNode | null {
|
|
11
|
+
switch (element.type) {
|
|
12
|
+
case primitiveNodeTypes.string:
|
|
13
|
+
case primitiveNodeTypes.string:
|
|
14
|
+
case primitiveNodeTypes.number:
|
|
15
|
+
return { type: 'text', value: '' };
|
|
16
|
+
default: {
|
|
17
|
+
switch (typeof element.type) {
|
|
18
|
+
case 'string':
|
|
19
|
+
switch (root) {
|
|
20
|
+
case 'web:html':
|
|
21
|
+
return h(element.type);
|
|
22
|
+
case 'web:svg':
|
|
23
|
+
return s(element.type);
|
|
24
|
+
}
|
|
25
|
+
default:
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
link(child, parent) {
|
|
32
|
+
switch (child.type) {
|
|
33
|
+
case 'element':
|
|
34
|
+
case 'comment':
|
|
35
|
+
case 'text':
|
|
36
|
+
switch (parent.type) {
|
|
37
|
+
case 'element':
|
|
38
|
+
case 'root':
|
|
39
|
+
parent.children.push(child);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
case 'doctype':
|
|
43
|
+
switch (parent.type) {
|
|
44
|
+
case 'root':
|
|
45
|
+
parent.children.push(child);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
default:
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
linkRoot(child) {
|
|
52
|
+
switch (child.type) {
|
|
53
|
+
case 'doctype':
|
|
54
|
+
case 'element':
|
|
55
|
+
case 'comment':
|
|
56
|
+
case 'text':
|
|
57
|
+
root.children.push(child);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
update(el, next, prev) {
|
|
61
|
+
switch (el.type) {
|
|
62
|
+
case 'text':
|
|
63
|
+
el.value = (next.props.value as number | string | boolean).toString()
|
|
64
|
+
return;
|
|
65
|
+
case 'element':
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
unlink(child, parent) {
|
|
70
|
+
if ("children" in parent) {
|
|
71
|
+
parent.children = parent.children.filter(c => c !== child);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
sort(el, children) {
|
|
75
|
+
if ("children" in el) {
|
|
76
|
+
el.children = children as Element[];
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
type DehydratedRender = {
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const dehydrate = (tree: CommitTree2, renderer: RenderSpace2<HNode, string | symbol>) => {
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const rehydrate = () => {
|
|
90
|
+
|
|
91
|
+
}
|
package/space.ts
CHANGED
|
@@ -1,69 +1,77 @@
|
|
|
1
1
|
import * as act from '@lukekaalim/act';
|
|
2
|
-
import * as recon from '@lukekaalim/act-recon';
|
|
3
|
-
|
|
4
|
-
import { createSimpleRenderSpace } from '@lukekaalim/act-backstage/mod.ts';
|
|
5
2
|
|
|
6
3
|
import { setProps } from './props.ts';
|
|
4
|
+
import { NodeBuilder } from '@lukekaalim/act-backstage';
|
|
7
5
|
|
|
8
6
|
export const HTML: act.Component = ({ children }) => act.h(act.renderNodeType, { type: 'web:html' }, children);
|
|
9
7
|
export const SVG: act.Component = ({ children }) => act.h(act.renderNodeType, { type: 'web:svg' }, children);
|
|
10
8
|
|
|
11
9
|
const defaultWindow = window;
|
|
12
10
|
|
|
13
|
-
export const
|
|
14
|
-
tree: recon.CommitTree,
|
|
11
|
+
export const createWebNodeBuilder = (
|
|
15
12
|
root: HTMLElement,
|
|
16
13
|
window: Window = defaultWindow
|
|
17
|
-
) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
14
|
+
): NodeBuilder<HTMLElement | SVGElement | Text, 'web:html' | 'web:svg'> => ({
|
|
15
|
+
roots: new Set(['web:html', 'web:svg'] as const),
|
|
16
|
+
|
|
17
|
+
create(element, rootType) {
|
|
18
|
+
const tag = element.type;
|
|
19
|
+
|
|
20
|
+
switch (typeof tag) {
|
|
21
|
+
case 'symbol': {
|
|
22
|
+
switch (tag) {
|
|
23
|
+
case act.primitiveNodeTypes.string:
|
|
24
|
+
case act.primitiveNodeTypes.number:
|
|
25
|
+
return window.document.createTextNode("<empty text>");
|
|
26
|
+
default:
|
|
27
|
+
return null;
|
|
32
28
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
}
|
|
30
|
+
case 'string': {
|
|
31
|
+
switch (rootType) {
|
|
32
|
+
case 'web:html':
|
|
33
|
+
return window.document.createElementNS('http://www.w3.org/1999/xhtml', tag);
|
|
34
|
+
case 'web:svg':
|
|
35
|
+
return window.document.createElementNS('http://www.w3.org/2000/svg', tag);
|
|
40
36
|
}
|
|
41
|
-
default:
|
|
42
|
-
return null;
|
|
43
37
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
38
|
+
default:
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
update(el, next, prev) {
|
|
43
|
+
setProps(window, el, next, prev);
|
|
44
|
+
},
|
|
45
|
+
link(el, parent) {
|
|
46
|
+
parent.appendChild(el);
|
|
47
|
+
},
|
|
48
|
+
linkRoot(child) {
|
|
49
|
+
root.appendChild(child);
|
|
50
|
+
},
|
|
51
|
+
unlink(el, parent) {
|
|
52
|
+
if (el.parentNode === parent)
|
|
53
|
+
parent.removeChild(el);
|
|
54
|
+
},
|
|
55
|
+
destroy(el) {
|
|
56
|
+
if (el.parentNode)
|
|
57
|
+
el.parentNode.removeChild(el)
|
|
58
|
+
},
|
|
59
|
+
suspend(el, parent) {
|
|
60
|
+
if (el instanceof HTMLElement)
|
|
61
|
+
el.style.opacity = 0.5.toString();
|
|
62
|
+
},
|
|
63
|
+
unsuspend(el, parent) {
|
|
64
|
+
if (el instanceof HTMLElement)
|
|
65
|
+
el.style.opacity = (1).toString();
|
|
66
|
+
},
|
|
67
|
+
sort(el, newChildren) {
|
|
68
|
+
if (el instanceof Text)
|
|
69
|
+
return;
|
|
70
|
+
if (newChildren.length < 2)
|
|
71
|
+
return;
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (prev.parentNode)
|
|
66
|
-
prev.parentNode.removeChild(prev);
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
};
|
|
73
|
+
for (let i = 0; i < newChildren.length; i++)
|
|
74
|
+
if (el.children[i] !== newChildren[i])
|
|
75
|
+
el.insertBefore(newChildren[i], el.children[i])
|
|
76
|
+
},
|
|
77
|
+
})
|