creo 0.0.3-dev → 0.0.4-dev
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/.env.development +1 -0
- package/.github/workflows/main.yml +24 -0
- package/README.md +1 -1
- package/TODOS.md +2 -0
- package/index.ts +1 -0
- package/package.json +7 -2
- package/src/DOM/Context.ts +36 -0
- package/src/DOM/DomEngine.ts +106 -0
- package/src/DOM/IRenderCycle.ts +9 -0
- package/src/DOM/Key.ts +1 -0
- package/src/DOM/Node.ts +472 -0
- package/src/DOM/Registry.ts +53 -0
- package/src/creo.ts +134 -0
- package/src/data-structures/assert/assert.ts +12 -0
- package/src/data-structures/indexed-map/IndexedMap.ts +281 -0
- package/src/data-structures/linked-map/LinkedMap.spec.ts +67 -0
- package/src/data-structures/linked-map/LinkedMap.ts +198 -0
- package/src/data-structures/list/List.spec.ts +181 -0
- package/src/data-structures/list/List.ts +195 -0
- package/src/data-structures/maybe/Maybe.ts +25 -0
- package/src/data-structures/null/null.ts +3 -0
- package/src/{tools/isRecordLike.spec.ts → data-structures/record/IsRecordLike.spec.ts} +1 -1
- package/src/{tools/isRecordLike.ts → data-structures/record/IsRecordLike.ts} +1 -1
- package/src/{record/record.spec.ts → data-structures/record/Record.spec.ts} +96 -2
- package/src/data-structures/record/Record.ts +145 -0
- package/src/data-structures/shalllowEqual/shallowEqual.ts +26 -0
- package/src/data-structures/simpleKey/simpleKey.ts +8 -0
- package/src/data-structures/wildcard/wildcard.ts +1 -0
- package/src/examples/SimpleTodoList/SimpleTodoList.ts +53 -0
- package/src/globals.d.ts +1 -0
- package/src/main.ts +22 -11
- package/src/style.css +24 -79
- package/src/ui/html/Block.ts +10 -0
- package/src/ui/html/Button.ts +12 -0
- package/src/ui/html/HStack.ts +10 -0
- package/src/ui/html/Inline.ts +12 -0
- package/src/ui/html/List.ts +10 -0
- package/src/ui/html/Text.ts +9 -0
- package/src/ui/html/VStack.ts +11 -0
- package/tsconfig.json +2 -2
- package/vite.config.js +10 -0
- package/bun.lockb +0 -0
- package/src/record/record.ts +0 -101
- package/src/tools/optional.ts +0 -25
- package/src/ui/component.ts +0 -1
- package/src/ui/prop.ts +0 -13
- package/src/ui/state.ts +0 -0
- /package/src/{ui/index.ts → examples/simple.ts} +0 -0
package/.env.development
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__DEV__=true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Use bun
|
|
17
|
+
uses: oven-sh/setup-bun@v2
|
|
18
|
+
|
|
19
|
+
- name: install and tests
|
|
20
|
+
run: |
|
|
21
|
+
bun install
|
|
22
|
+
bun test
|
|
23
|
+
env:
|
|
24
|
+
CI: true
|
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Work in progress, stay tuned
|
|
1
|
+
Work in progress, stay tuned
|
package/TODOS.md
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-dev",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"typescript": "^5.2.2",
|
|
12
12
|
"vite": "^5.2.0",
|
|
13
|
-
"bun-types": "latest"
|
|
13
|
+
"bun-types": "latest",
|
|
14
|
+
"@types/bun": "latest"
|
|
15
|
+
},
|
|
16
|
+
"module": "index.ts",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"csstype": "^3.1.3"
|
|
14
19
|
}
|
|
15
20
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Maybe } from "../data-structures/maybe/Maybe";
|
|
2
|
+
import {
|
|
3
|
+
onDidUpdate,
|
|
4
|
+
record,
|
|
5
|
+
RecordOf,
|
|
6
|
+
} from "../data-structures/record/Record";
|
|
7
|
+
import { Node } from "./Node";
|
|
8
|
+
|
|
9
|
+
export class Context<P> {
|
|
10
|
+
private subscribers: Array<() => void> = [];
|
|
11
|
+
private node: Node;
|
|
12
|
+
tracked = <T extends {}>(t: T): RecordOf<T> => {
|
|
13
|
+
const rec = record(t);
|
|
14
|
+
this.subscribers.push(
|
|
15
|
+
onDidUpdate(rec, () => {
|
|
16
|
+
this.node.invalidate();
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
return rec;
|
|
20
|
+
};
|
|
21
|
+
p: P;
|
|
22
|
+
slot: Maybe<() => void>;
|
|
23
|
+
|
|
24
|
+
constructor(node: Node, initialParams: P, slot: Maybe<() => void>) {
|
|
25
|
+
this.node = node;
|
|
26
|
+
this.p = initialParams;
|
|
27
|
+
this.slot = slot;
|
|
28
|
+
}
|
|
29
|
+
dispose() {
|
|
30
|
+
this.subscribers.forEach((unsubscribe) => unsubscribe());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setSlot(slot: () => void): void {
|
|
34
|
+
this.slot = slot;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout engine abstract class
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
* Ideas:
|
|
6
|
+
* [ ] Event systems
|
|
7
|
+
* [ ] Animation engine
|
|
8
|
+
*/
|
|
9
|
+
import { Maybe } from "../data-structures/maybe/Maybe";
|
|
10
|
+
import { IRenderCycle } from "./IRenderCycle";
|
|
11
|
+
import { Node, RootNode } from "./Node";
|
|
12
|
+
import { Registry } from "./Registry";
|
|
13
|
+
|
|
14
|
+
let $activeEngine: Maybe<DomEngine>;
|
|
15
|
+
|
|
16
|
+
export class DomEngine implements IRenderCycle {
|
|
17
|
+
protected isRerenderingScheduled = true;
|
|
18
|
+
// Queue of currently rendering items
|
|
19
|
+
protected registry: Registry = new Registry();
|
|
20
|
+
protected root!: RootNode;
|
|
21
|
+
protected rootHtml: HTMLElement;
|
|
22
|
+
|
|
23
|
+
constructor(root: HTMLElement) {
|
|
24
|
+
this.rootHtml = root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public newNode(node: Node) {
|
|
28
|
+
this.registry.newNode(node);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public willRender(node: Node) {
|
|
32
|
+
this.registry.willRender(node);
|
|
33
|
+
this.scheduleRerender();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public isRendering(node: Node) {
|
|
37
|
+
this.registry.isRendering(node);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public didRender(node: Node): { justMounted: boolean } {
|
|
41
|
+
const result = this.registry.didRender(node);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public dispose(node: Node) {
|
|
46
|
+
this.registry.dispose(node);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
debugStatus() {
|
|
50
|
+
console.log(this.registry);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render(renderFn: () => void): void {
|
|
54
|
+
if (this.root != null) {
|
|
55
|
+
return this.forceRerender();
|
|
56
|
+
}
|
|
57
|
+
this.root = new RootNode(this.rootHtml, renderFn, this);
|
|
58
|
+
this.forceRerender();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
forceRerender(): void {
|
|
62
|
+
if (this.root == null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log("forcererender");
|
|
66
|
+
this.willRender(this.root);
|
|
67
|
+
this.rerender();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
scheduleRerender(): void {
|
|
71
|
+
if (this.isRerenderingScheduled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.isRerenderingScheduled = true;
|
|
75
|
+
globalThis.requestAnimationFrame(() => {
|
|
76
|
+
this.rerender();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected renderNextPending() {
|
|
81
|
+
this.isRerenderingScheduled = false;
|
|
82
|
+
const next = this.registry.getNextToRender();
|
|
83
|
+
if (next != null) {
|
|
84
|
+
next.render();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getParent(): Maybe<Node> {
|
|
89
|
+
return this.registry.getNextRendering();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rerender() {
|
|
93
|
+
$activeEngine = this;
|
|
94
|
+
this.renderNextPending();
|
|
95
|
+
$activeEngine = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
shouldUpdateNode(node: Node): boolean {
|
|
99
|
+
return this.registry.shouldUpdateNode(node);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// TODO enhance with other engines, use DOMEngine for now
|
|
104
|
+
export function getActiveEngine(): Maybe<DomEngine> {
|
|
105
|
+
return $activeEngine;
|
|
106
|
+
}
|
package/src/DOM/Key.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Key = number | string;
|
package/src/DOM/Node.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents physical node for UI rendering engine
|
|
3
|
+
* In charge of:
|
|
4
|
+
* 1. UI changes
|
|
5
|
+
* 2. Animations
|
|
6
|
+
* 3. GC when gets destroyed
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NodeBuilder, NodeMethods } from "../creo";
|
|
10
|
+
import { assertJust } from "../data-structures/assert/assert";
|
|
11
|
+
import { IndexedMap } from "../data-structures/indexed-map/IndexedMap";
|
|
12
|
+
import { Maybe, None } from "../data-structures/maybe/Maybe";
|
|
13
|
+
import { generateNextKey } from "../data-structures/simpleKey/simpleKey";
|
|
14
|
+
import { Wildcard } from "../data-structures/wildcard/wildcard";
|
|
15
|
+
import { Context } from "./Context";
|
|
16
|
+
import { DomEngine } from "./DomEngine";
|
|
17
|
+
import { IRenderCycle } from "./IRenderCycle";
|
|
18
|
+
import { Key } from "./Key";
|
|
19
|
+
|
|
20
|
+
export enum NodeStatus {
|
|
21
|
+
DIRTY,
|
|
22
|
+
UPDATING,
|
|
23
|
+
CLEAR,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export enum UpdateDirectiveEnum {
|
|
27
|
+
// Change element position
|
|
28
|
+
MOVE,
|
|
29
|
+
// No component, just create a new
|
|
30
|
+
NEW,
|
|
31
|
+
// Matched component, update existing item
|
|
32
|
+
REUSE,
|
|
33
|
+
// Matched key, but different component
|
|
34
|
+
REPLACE,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type UpdateDirective = {
|
|
38
|
+
updateDirective: UpdateDirectiveEnum;
|
|
39
|
+
node: Maybe<Node>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class Node implements IRenderCycle {
|
|
43
|
+
public publicApi: {
|
|
44
|
+
ext: Wildcard;
|
|
45
|
+
};
|
|
46
|
+
public c: Context<Wildcard>;
|
|
47
|
+
|
|
48
|
+
public children: IndexedMap<Node, "key"> = new IndexedMap("key");
|
|
49
|
+
public pendingChildrenState!: IndexedMap<Node, "key">;
|
|
50
|
+
public renderCursor: Maybe<Node>;
|
|
51
|
+
|
|
52
|
+
public lifecycle: NodeMethods<Wildcard, Wildcard>;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
public userKey: Maybe<Key>,
|
|
56
|
+
public key: Key,
|
|
57
|
+
p: Wildcard,
|
|
58
|
+
public slot: Maybe<() => void>,
|
|
59
|
+
public ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
60
|
+
public parent: Node,
|
|
61
|
+
public parentUI: UINode,
|
|
62
|
+
public engine: DomEngine,
|
|
63
|
+
) {
|
|
64
|
+
this.c = new Context(this, p, slot);
|
|
65
|
+
const { ext, ...lifecycle } = this.ctor(this.c);
|
|
66
|
+
this.lifecycle = lifecycle;
|
|
67
|
+
this.publicApi = {
|
|
68
|
+
ext,
|
|
69
|
+
}; //new Node(extension, this);
|
|
70
|
+
this.newNode(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
newNode(_node: Node): void {
|
|
74
|
+
this.engine.newNode(this);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// sets status to dirty for the node
|
|
78
|
+
invalidate() {
|
|
79
|
+
this.willRender();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
applyNewParams(newParams: Wildcard) {
|
|
83
|
+
if (this.shouldUpdate(newParams)) {
|
|
84
|
+
this.willRender();
|
|
85
|
+
}
|
|
86
|
+
this.c.p = newParams;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
willRender() {
|
|
90
|
+
this.engine.willRender(this);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
isRendering() {
|
|
94
|
+
this.engine.isRendering(this);
|
|
95
|
+
this.pendingChildrenState = new IndexedMap("key", ["userKey"]);
|
|
96
|
+
this.renderCursor = this.children.at(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
didRender(): { justMounted: boolean } {
|
|
100
|
+
// When the rendering cycle ends, all items starting
|
|
101
|
+
// from this.renderingCursor were not used and hence, need to be deleted
|
|
102
|
+
while (this.renderCursor != null) {
|
|
103
|
+
this.renderCursor.dispose();
|
|
104
|
+
this.renderCursor = this.children.getNext(this.renderCursor);
|
|
105
|
+
}
|
|
106
|
+
// At the end of the cycle, we replace children with pending children (this.renderingChildren)
|
|
107
|
+
this.children = this.pendingChildrenState;
|
|
108
|
+
this.pendingChildrenState = new IndexedMap("key");
|
|
109
|
+
return this.engine.didRender(this);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render() {
|
|
113
|
+
__DEV__ && console.log("Render:", this.key);
|
|
114
|
+
this.isRendering();
|
|
115
|
+
this.lifecycle.render();
|
|
116
|
+
const { justMounted } = this.didRender();
|
|
117
|
+
if (justMounted) {
|
|
118
|
+
this.lifecycle.didMount?.();
|
|
119
|
+
}
|
|
120
|
+
{
|
|
121
|
+
this.lifecycle.didUpdate?.();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
shouldUpdate(pendingParams: Wildcard): boolean {
|
|
126
|
+
if (this.lifecycle.shouldUpdate != null) {
|
|
127
|
+
return this.lifecycle.shouldUpdate(pendingParams);
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
generateUpdateDirective(
|
|
133
|
+
userKey: Maybe<Key>,
|
|
134
|
+
ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
135
|
+
tag: Maybe<string>,
|
|
136
|
+
): UpdateDirective {
|
|
137
|
+
const expectedChild: Maybe<Node> = this.renderCursor;
|
|
138
|
+
let expectedTag: Maybe<string> = null;
|
|
139
|
+
if (expectedChild instanceof UINode) {
|
|
140
|
+
expectedTag = expectedChild.tag;
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
expectedChild != null &&
|
|
144
|
+
expectedChild.ctor === ctor &&
|
|
145
|
+
expectedTag == tag &&
|
|
146
|
+
// TODO: We should respect & identify artificially generated keys too
|
|
147
|
+
(userKey == null || userKey === expectedChild.userKey)
|
|
148
|
+
) {
|
|
149
|
+
return {
|
|
150
|
+
updateDirective: UpdateDirectiveEnum.REUSE,
|
|
151
|
+
node: expectedChild,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Unhappy path exploration.
|
|
156
|
+
const maybeKeyedMatchedChildren: Node[] =
|
|
157
|
+
userKey != null ? this.children.getByIndex("userKey", userKey) : [];
|
|
158
|
+
|
|
159
|
+
if (maybeKeyedMatchedChildren.length > 1) {
|
|
160
|
+
throw new Error("Spotted duplicate keys for the node");
|
|
161
|
+
}
|
|
162
|
+
const maybeKeyedMatchedChild: Maybe<Node> = maybeKeyedMatchedChildren[0];
|
|
163
|
+
|
|
164
|
+
// Case 1: Key, no next component, but matched key: something went wrong
|
|
165
|
+
if (
|
|
166
|
+
expectedChild == null &&
|
|
167
|
+
userKey != null &&
|
|
168
|
+
maybeKeyedMatchedChild != null
|
|
169
|
+
) {
|
|
170
|
+
throw new Error(`Detected key duplication: ${userKey}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Case 2: Key, matched key, not matched next element
|
|
174
|
+
if (
|
|
175
|
+
expectedChild != null &&
|
|
176
|
+
userKey != null &&
|
|
177
|
+
maybeKeyedMatchedChild != null
|
|
178
|
+
) {
|
|
179
|
+
// We have matched key, but their constructors are different
|
|
180
|
+
// In theory that process is REPLACE, but for external usages, it works exactly the same way as NEW
|
|
181
|
+
// The difference is purely internal
|
|
182
|
+
if (maybeKeyedMatchedChild.ctor !== ctor || expectedTag != tag) {
|
|
183
|
+
return {
|
|
184
|
+
updateDirective: UpdateDirectiveEnum.REPLACE,
|
|
185
|
+
node: maybeKeyedMatchedChild,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// TODO detect potential key duplication?
|
|
189
|
+
// Everything is fine, we just need to move that item to different place
|
|
190
|
+
return {
|
|
191
|
+
updateDirective: UpdateDirectiveEnum.MOVE,
|
|
192
|
+
node: maybeKeyedMatchedChild,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Default outcome: Component creation
|
|
197
|
+
// Cases:
|
|
198
|
+
// 1. No key, component mismatch or no component
|
|
199
|
+
// 2. Key, no next component, no matched key
|
|
200
|
+
return {
|
|
201
|
+
updateDirective: UpdateDirectiveEnum.NEW,
|
|
202
|
+
node: null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
renderChild<Tag extends Maybe<string>>(
|
|
207
|
+
userKey: Maybe<Key>,
|
|
208
|
+
ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
209
|
+
params: Maybe<Wildcard>,
|
|
210
|
+
slot: Maybe<() => void>,
|
|
211
|
+
tag: Tag,
|
|
212
|
+
): Tag extends None ? Node : UINode {
|
|
213
|
+
const directive = this.generateUpdateDirective(userKey, ctor, tag);
|
|
214
|
+
|
|
215
|
+
let newNode: Node;
|
|
216
|
+
|
|
217
|
+
switch (directive.updateDirective) {
|
|
218
|
+
case UpdateDirectiveEnum.REUSE: {
|
|
219
|
+
// TODO: MAYBE update SLOT
|
|
220
|
+
assertJust(directive.node, "Cannot re-use null node");
|
|
221
|
+
newNode = directive.node;
|
|
222
|
+
this.pendingChildrenState.put(newNode);
|
|
223
|
+
this.renderCursor =
|
|
224
|
+
this.renderCursor != null
|
|
225
|
+
? this.children.getNext(this.renderCursor)
|
|
226
|
+
: null;
|
|
227
|
+
this.children.delete(newNode.key);
|
|
228
|
+
// The component can decide on its own if the component needs to get updated
|
|
229
|
+
directive.node.applyNewParams(params);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case UpdateDirectiveEnum.MOVE: {
|
|
233
|
+
// TODO: move all DOM children which are connected to the top-level
|
|
234
|
+
throw new Error("Not implemented");
|
|
235
|
+
}
|
|
236
|
+
case UpdateDirectiveEnum.NEW: {
|
|
237
|
+
newNode = this.createNewNode(userKey, ctor, params, slot, tag);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case UpdateDirectiveEnum.REPLACE: {
|
|
241
|
+
assertJust(directive.node, "Cannot replace null component");
|
|
242
|
+
const toReplace = directive.node;
|
|
243
|
+
if (this.renderCursor === toReplace) {
|
|
244
|
+
this.renderCursor = this.children.getNext(this.renderCursor);
|
|
245
|
+
}
|
|
246
|
+
// Delete conflicting element, as they should not exist anymore
|
|
247
|
+
this.children.delete(toReplace.key);
|
|
248
|
+
// Cleanup the component:
|
|
249
|
+
toReplace.dispose();
|
|
250
|
+
newNode = this.createNewNode(userKey, ctor, params, slot, tag);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (this.engine.shouldUpdateNode(newNode)) {
|
|
255
|
+
newNode.render();
|
|
256
|
+
}
|
|
257
|
+
return newNode;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected createNewNode(
|
|
261
|
+
userKey: Maybe<Key>,
|
|
262
|
+
ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
263
|
+
params: Maybe<Wildcard>,
|
|
264
|
+
slot: Maybe<() => void>,
|
|
265
|
+
tag: Maybe<string>,
|
|
266
|
+
): Node {
|
|
267
|
+
let node: Node;
|
|
268
|
+
if (tag == null) {
|
|
269
|
+
node = new Node(
|
|
270
|
+
userKey,
|
|
271
|
+
generateNextKey(this.pendingChildrenState.size()),
|
|
272
|
+
params,
|
|
273
|
+
slot,
|
|
274
|
+
ctor,
|
|
275
|
+
this,
|
|
276
|
+
this.parentUI,
|
|
277
|
+
this.engine,
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
node = new UINode(
|
|
281
|
+
userKey,
|
|
282
|
+
generateNextKey(this.pendingChildrenState.size()),
|
|
283
|
+
params,
|
|
284
|
+
slot,
|
|
285
|
+
ctor,
|
|
286
|
+
this,
|
|
287
|
+
this.parentUI,
|
|
288
|
+
this.engine,
|
|
289
|
+
tag,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.pendingChildrenState.put(node);
|
|
294
|
+
return node;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
dispose(): void {
|
|
298
|
+
// delete any DOM / whatever nodes we have
|
|
299
|
+
// Propagate removal of data to all children
|
|
300
|
+
|
|
301
|
+
// Remove subscriptions
|
|
302
|
+
this.c.dispose();
|
|
303
|
+
|
|
304
|
+
// Parent component should alredy not to have children, but we can have sanity check there
|
|
305
|
+
for (const child of this.children) {
|
|
306
|
+
child.dispose();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Mark component as not dirty, as there is no point in keeping that item anymore
|
|
310
|
+
this.engine.dispose(this);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export class UINode extends Node {
|
|
315
|
+
public uiChildren: IndexedMap<UINode, "key"> = new IndexedMap("key", [
|
|
316
|
+
"userKey",
|
|
317
|
+
]);
|
|
318
|
+
public pendingUIChildrenState!: IndexedMap<UINode, "key">;
|
|
319
|
+
protected domNode: Maybe<HTMLElement>;
|
|
320
|
+
protected domText: Maybe<Text>;
|
|
321
|
+
public publicNode: () => Maybe<HTMLElement | Text>;
|
|
322
|
+
constructor(
|
|
323
|
+
userKey: Maybe<Key>,
|
|
324
|
+
internalKey: Key,
|
|
325
|
+
p: Wildcard,
|
|
326
|
+
slot: Maybe<() => void>,
|
|
327
|
+
ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
328
|
+
parent: Node,
|
|
329
|
+
parentUI: UINode,
|
|
330
|
+
engine: DomEngine,
|
|
331
|
+
public tag: string,
|
|
332
|
+
) {
|
|
333
|
+
super(userKey, internalKey, p, slot, ctor, parent, parentUI, engine);
|
|
334
|
+
this.publicNode = () => {
|
|
335
|
+
return (this.domNode ?? this.domText) as Maybe<HTMLElement | Text>;
|
|
336
|
+
};
|
|
337
|
+
// Root element does not have parentUI
|
|
338
|
+
this.parentUI?.appendUIChild(this);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
appendUIChild(node: UINode) {
|
|
342
|
+
this.uiChildren.put(node);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
protected createNewNode(
|
|
346
|
+
userKey: Maybe<Key>,
|
|
347
|
+
ctor: NodeBuilder<Wildcard, Wildcard>,
|
|
348
|
+
params: Maybe<Wildcard>,
|
|
349
|
+
slot: Maybe<() => void>,
|
|
350
|
+
tag: Maybe<string>,
|
|
351
|
+
): Node {
|
|
352
|
+
let node: Node;
|
|
353
|
+
if (tag == null) {
|
|
354
|
+
node = new Node(
|
|
355
|
+
userKey,
|
|
356
|
+
generateNextKey(this.pendingChildrenState.size()),
|
|
357
|
+
params,
|
|
358
|
+
slot,
|
|
359
|
+
ctor,
|
|
360
|
+
this,
|
|
361
|
+
this,
|
|
362
|
+
this.engine,
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
node = new UINode(
|
|
366
|
+
userKey,
|
|
367
|
+
generateNextKey(this.pendingChildrenState.size()),
|
|
368
|
+
params,
|
|
369
|
+
slot,
|
|
370
|
+
ctor,
|
|
371
|
+
this,
|
|
372
|
+
this,
|
|
373
|
+
this.engine,
|
|
374
|
+
tag,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.pendingChildrenState.put(node);
|
|
379
|
+
return node;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
renderUI() {
|
|
383
|
+
// rerender
|
|
384
|
+
if (this.domText != null) {
|
|
385
|
+
const params = this.c.p;
|
|
386
|
+
if (typeof params === "string" && this.domText.textContent != params) {
|
|
387
|
+
const newItem = document.createTextNode(params);
|
|
388
|
+
this.parentUI.domNode?.replaceChild(newItem, this.domText);
|
|
389
|
+
this.domText = newItem;
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (this.domNode != null) {
|
|
394
|
+
const params = this.c.p;
|
|
395
|
+
const element = this.domNode;
|
|
396
|
+
if (typeof params === "object") {
|
|
397
|
+
for (const key in params) {
|
|
398
|
+
if (element.getAttribute(key) !== params[key]) {
|
|
399
|
+
element.setAttribute(key, params[key]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// mount
|
|
406
|
+
if (this.tag === "text") {
|
|
407
|
+
this.domText = document.createTextNode(this.c.p);
|
|
408
|
+
this.parentUI.domNode?.appendChild(this.domText);
|
|
409
|
+
return;
|
|
410
|
+
} else {
|
|
411
|
+
this.domNode = document.createElement(this.tag);
|
|
412
|
+
const params = this.c.p;
|
|
413
|
+
const element = this.domNode;
|
|
414
|
+
if (typeof params === "object") {
|
|
415
|
+
for (const key in params) {
|
|
416
|
+
if (element.getAttribute(key) !== params[key]) {
|
|
417
|
+
element.setAttribute(key, params[key]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
this.parentUI.domNode?.appendChild(this.domNode);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
render() {
|
|
426
|
+
this.isRendering();
|
|
427
|
+
__DEV__ && console.log("UI render:", this.tag, this.key);
|
|
428
|
+
this.renderUI();
|
|
429
|
+
//this.layoutNode = this.layout.renderNode(this);
|
|
430
|
+
this.lifecycle.render();
|
|
431
|
+
const { justMounted } = this.didRender();
|
|
432
|
+
if (justMounted) {
|
|
433
|
+
this.lifecycle.didMount?.();
|
|
434
|
+
} else {
|
|
435
|
+
this.lifecycle.didUpdate?.();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
dispose(): void {
|
|
440
|
+
super.dispose();
|
|
441
|
+
const toDelete = this.domNode ?? this.domText;
|
|
442
|
+
if (toDelete != null && this.parentUI != null) {
|
|
443
|
+
this.parentUI.domNode?.removeChild(toDelete);
|
|
444
|
+
}
|
|
445
|
+
this.domNode = null;
|
|
446
|
+
this.domText = null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export class RootNode extends UINode {
|
|
451
|
+
constructor(htmlElement: HTMLElement, slot: () => void, engine: DomEngine) {
|
|
452
|
+
super(
|
|
453
|
+
null,
|
|
454
|
+
"root",
|
|
455
|
+
null,
|
|
456
|
+
slot,
|
|
457
|
+
/* ctor */ (c) => ({
|
|
458
|
+
render() {
|
|
459
|
+
c.slot?.();
|
|
460
|
+
},
|
|
461
|
+
}),
|
|
462
|
+
// @ts-ignore
|
|
463
|
+
/* parent */ null,
|
|
464
|
+
/* parentUI */ null,
|
|
465
|
+
engine,
|
|
466
|
+
/* tag */ null,
|
|
467
|
+
);
|
|
468
|
+
this.domNode = htmlElement;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
renderUI() {}
|
|
472
|
+
}
|