rask-ui 0.2.1 → 0.2.3

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.
@@ -1,9 +1,77 @@
1
+ import { elementsToFragment } from "./dom-utils";
1
2
  export class AbstractVNode {
2
3
  key;
3
4
  parent;
4
5
  root;
5
6
  elm;
6
7
  children;
8
+ applyDOMOperations(operations, atVNode) {
9
+ if (!this.elm || !this.children) {
10
+ this.parent?.applyDOMOperations(operations, this);
11
+ return;
12
+ }
13
+ console.log(operations);
14
+ operations.forEach((operation) => {
15
+ switch (operation.type) {
16
+ case "insert": {
17
+ const fragment = elementsToFragment(operation.elms);
18
+ // Insert after afterElm (or at start if undefined)
19
+ if (operation.afterElm === undefined) {
20
+ // Insert at the start
21
+ this.elm.insertBefore(fragment, this.elm.firstChild);
22
+ }
23
+ else {
24
+ // Insert after afterElm
25
+ const target = operation.afterElm.nextSibling;
26
+ this.elm.insertBefore(fragment, target);
27
+ }
28
+ break;
29
+ }
30
+ case "remove": {
31
+ const elms = operation.elms;
32
+ if (elms.length === 1) {
33
+ this.elm.removeChild(elms[0]);
34
+ }
35
+ else {
36
+ const range = new Range();
37
+ range.setStartBefore(elms[0]);
38
+ range.setEndAfter(elms[elms.length - 1]);
39
+ range.deleteContents();
40
+ }
41
+ break;
42
+ }
43
+ case "replace": {
44
+ const oldElms = operation.oldElms;
45
+ const newElms = operation.newElms;
46
+ if (oldElms.length === 1) {
47
+ this.elm.replaceChild(elementsToFragment(newElms), oldElms[0]);
48
+ }
49
+ else {
50
+ const range = new Range();
51
+ range.setStartBefore(oldElms[0]);
52
+ range.setEndAfter(oldElms[oldElms.length - 1]);
53
+ range.deleteContents();
54
+ range.insertNode(elementsToFragment(newElms));
55
+ }
56
+ break;
57
+ }
58
+ case "move": {
59
+ const fragment = elementsToFragment(operation.elms);
60
+ // Insert after afterElm (or at start if undefined)
61
+ if (operation.afterElm === undefined) {
62
+ // Insert at the start
63
+ this.elm.insertBefore(fragment, this.elm.firstChild);
64
+ }
65
+ else {
66
+ // Insert after afterElm
67
+ const target = operation.afterElm.nextSibling;
68
+ this.elm.insertBefore(fragment, target);
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ });
74
+ }
7
75
  getHTMLElement() {
8
76
  if (!this.elm || !(this.elm instanceof HTMLElement)) {
9
77
  throw new Error("This VNode does not have an HTMLElement");
@@ -18,6 +86,7 @@ export class AbstractVNode {
18
86
  return [this.elm];
19
87
  }
20
88
  if (!this.children) {
89
+ console.log("WTF", this);
21
90
  throw new Error("This VNode has no element or children");
22
91
  }
23
92
  return this.children.map((child) => child.getElements()).flat();
@@ -55,52 +124,132 @@ export class AbstractVNode {
55
124
  patchChildren(newChildren) {
56
125
  const prevChildren = this.children;
57
126
  // When there are only new children, we just mount them
58
- if (newChildren && prevChildren.length === 0) {
127
+ if (prevChildren.length === 0) {
59
128
  newChildren.forEach((child) => child.mount(this));
60
- return newChildren;
129
+ return {
130
+ children: newChildren,
131
+ operations: [
132
+ {
133
+ type: "insert",
134
+ elms: newChildren.map((child) => child.getElements()).flat(),
135
+ },
136
+ ],
137
+ };
61
138
  }
62
139
  // If we want to remove all children, we just unmount the previous ones
63
140
  if (!newChildren.length && prevChildren.length) {
64
141
  prevChildren.forEach((child) => child.unmount());
65
- return [];
142
+ return {
143
+ children: [],
144
+ operations: [
145
+ {
146
+ type: "remove",
147
+ elms: prevChildren.map((child) => child.getElements()).flat(),
148
+ },
149
+ ],
150
+ };
66
151
  }
152
+ const operations = [];
67
153
  const oldKeys = {};
154
+ // Build oldKeys map and handle duplicate keys
68
155
  prevChildren.forEach((prevChild, index) => {
69
- oldKeys[prevChild.key || index] = prevChild;
156
+ const key = prevChild.key || index;
157
+ // If key already exists, we have a duplicate - unmount the old one immediately
158
+ if (oldKeys[key]) {
159
+ oldKeys[key].unmount();
160
+ operations.push({ type: "remove", elms: oldKeys[key].getElements() });
161
+ }
162
+ oldKeys[key] = prevChild;
70
163
  });
71
- // Build result array in the NEW order
164
+ // Helper to get afterElm for a position in result array
165
+ const getAfterElm = (index, result) => {
166
+ let currentIndex = index;
167
+ let prevChild = result[--currentIndex];
168
+ let afterElm;
169
+ while (prevChild) {
170
+ const prevElms = prevChild.getElements();
171
+ afterElm = prevElms[prevElms.length - 1];
172
+ if (afterElm) {
173
+ break;
174
+ }
175
+ prevChild = result[--currentIndex];
176
+ }
177
+ return afterElm;
178
+ };
179
+ // === PASS 1: Build result array (mount/patch all children) ===
72
180
  const result = [];
181
+ const newChildrenMeta = [];
73
182
  newChildren.forEach((newChild, index) => {
74
183
  const key = newChild.key || index;
75
- const prevChild = oldKeys[key];
76
- if (!prevChild) {
184
+ const oldChild = oldKeys[key];
185
+ if (!oldChild) {
77
186
  // New child - mount and add to result
78
187
  newChild.mount(this);
79
188
  result.push(newChild);
189
+ newChildrenMeta.push({ isNew: true });
80
190
  }
81
- else if (prevChild === newChild) {
82
- // Same instance - no patching needed, just reuse
83
- result.push(prevChild);
84
- delete oldKeys[key];
85
- }
86
- else if (this.canPatch(prevChild, newChild)) {
191
+ else if (this.canPatch(oldChild, newChild)) {
87
192
  // Compatible types - patch and reuse old VNode
88
- prevChild.patch(newChild);
89
- result.push(prevChild);
193
+ if (oldChild !== newChild) {
194
+ oldChild.patch(newChild);
195
+ }
196
+ result.push(oldChild);
90
197
  delete oldKeys[key];
198
+ newChildrenMeta.push({ isNew: false });
91
199
  }
92
200
  else {
93
201
  // Incompatible types - replace completely
94
202
  newChild.mount(this);
95
- prevChild.unmount();
203
+ oldChild.unmount();
96
204
  result.push(newChild);
97
205
  delete oldKeys[key];
206
+ newChildrenMeta.push({ isNew: true, replacedOld: oldChild });
207
+ }
208
+ });
209
+ // === PASS 2: Generate operations by comparing positions ===
210
+ result.forEach((child, newIndex) => {
211
+ const oldIndex = prevChildren.indexOf(child);
212
+ const meta = newChildrenMeta[newIndex];
213
+ if (meta.isNew) {
214
+ // New child - generate insert operation
215
+ const afterElm = getAfterElm(newIndex, result);
216
+ if (meta.replacedOld) {
217
+ // This is a replacement
218
+ const prevElms = meta.replacedOld.getElements();
219
+ if (prevElms.length) {
220
+ operations.push({
221
+ type: "replace",
222
+ oldElms: prevElms,
223
+ newElms: child.getElements(),
224
+ });
225
+ return;
226
+ }
227
+ }
228
+ operations.push({
229
+ type: "insert",
230
+ elms: child.getElements(),
231
+ afterElm,
232
+ });
233
+ }
234
+ else if (oldIndex !== newIndex) {
235
+ // Existing child that moved - generate move operation
236
+ const afterElm = getAfterElm(newIndex, result);
237
+ operations.push({
238
+ type: "move",
239
+ elms: child.getElements(),
240
+ afterElm,
241
+ });
98
242
  }
243
+ // else: child is in same position, no operation needed
99
244
  });
100
245
  // Unmount any old children that weren't reused
101
246
  for (const key in oldKeys) {
102
247
  oldKeys[key].unmount();
248
+ operations.push({ type: "remove", elms: oldKeys[key].getElements() });
103
249
  }
104
- return result;
250
+ return {
251
+ children: result,
252
+ operations,
253
+ };
105
254
  }
106
255
  }
@@ -40,7 +40,6 @@ export declare class ComponentVNode extends AbstractVNode {
40
40
  children: VNode[];
41
41
  instance?: ComponentInstance;
42
42
  constructor(component: Component<any>, props: Props, children: VNode[], key?: string);
43
- rerender(): void;
44
43
  mount(parent?: VNode): Node[];
45
44
  patch(newNode: ComponentVNode): void;
46
45
  unmount(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,MAAM,GACN,IAAI,GACJ,MAAM,GACN,SAAS,GACT,OAAO,CAAC;AACZ,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,cAAc,EAAE,CAAC;AAElE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,KAAK,IACjC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,iBAAiB,CAAC,GACvC,CAAC,MAAM,MAAM,iBAAiB,CAAC,CAAC;AAEpC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAKF,wBAAgB,mBAAmB,sBAYlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAYrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAYvC;AAED,qBAAa,cAAe,SAAQ,aAAa;IAC/C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;IAEb,QAAQ,EAAE,KAAK,EAAE,CAAM;IACvB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;gBAE3B,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IAWd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAyH7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
1
+ {"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,MAAM,GACN,IAAI,GACJ,MAAM,GACN,SAAS,GACT,OAAO,CAAC;AACZ,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,cAAc,EAAE,CAAC;AAElE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,KAAK,IACjC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,iBAAiB,CAAC,GACvC,CAAC,MAAM,MAAM,iBAAiB,CAAC,CAAC;AAEpC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAKF,wBAAgB,mBAAmB,sBAYlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAYrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAYvC;AAED,qBAAa,cAAe,SAAQ,aAAa;IAC/C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;IAEb,QAAQ,EAAE,KAAK,EAAE,CAAM;IACvB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;gBAE3B,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IAWd,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IA0I7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
@@ -51,9 +51,6 @@ export class ComponentVNode extends AbstractVNode {
51
51
  this.children = [];
52
52
  this.key = key;
53
53
  }
54
- rerender() {
55
- this.parent?.rerender();
56
- }
57
54
  mount(parent) {
58
55
  this.parent = parent;
59
56
  if (parent instanceof RootVNode) {
@@ -78,28 +75,39 @@ export class ComponentVNode extends AbstractVNode {
78
75
  }
79
76
  return normalizeChildren(renderResult);
80
77
  };
78
+ let isObserverQueued = false;
81
79
  const instance = (this.instance = {
82
80
  parent,
83
81
  contexts: null,
84
82
  onCleanups: [],
85
83
  onMounts: [],
86
84
  observer: new Observer(() => {
87
- this.root?.setAsCurrent();
88
- const newChildren = executeRender();
89
- const prevChildren = this.children;
90
- this.children = this.patchChildren(newChildren);
91
- // Typically components return a single element, which does
92
- // not require the parent to apply elements to the DOM again
93
- const canSelfUpdate = prevChildren.length === 1 &&
94
- this.children.length === 1 &&
95
- prevChildren[0] instanceof ElementVNode &&
96
- this.children[0] instanceof ElementVNode &&
97
- this.canPatch(prevChildren[0], this.children[0]);
98
- if (!canSelfUpdate) {
99
- this.parent?.rerender();
85
+ if (isObserverQueued) {
86
+ return;
100
87
  }
101
- this.root?.flushLifecycle();
102
- this.root?.clearCurrent();
88
+ isObserverQueued = true;
89
+ this.root?.queueObserver(() => {
90
+ if (instance.observer.isDisposed) {
91
+ return;
92
+ }
93
+ isObserverQueued = false;
94
+ this.root?.setAsCurrent();
95
+ const newChildren = executeRender();
96
+ const prevChildren = this.children;
97
+ const { children, operations } = this.patchChildren(newChildren);
98
+ this.children = children;
99
+ // Typically components return a single element, which does
100
+ // not require the parent to apply elements to the DOM again
101
+ const canSelfUpdate = prevChildren.length === 1 &&
102
+ this.children.length === 1 &&
103
+ prevChildren[0] instanceof ElementVNode &&
104
+ this.children[0] instanceof ElementVNode &&
105
+ this.canPatch(prevChildren[0], this.children[0]);
106
+ if (!canSelfUpdate) {
107
+ this.parent?.applyDOMOperations(operations, this);
108
+ }
109
+ this.root?.clearCurrent();
110
+ });
103
111
  }),
104
112
  reactiveProps: createReactiveProps(this.props),
105
113
  get error() {
@@ -8,7 +8,6 @@ export declare class ElementVNode extends AbstractVNode {
8
8
  private ref?;
9
9
  private eventListeners?;
10
10
  constructor(tag: string, { ref, ...props }: Props, children: VNode[], key?: string);
11
- rerender(): void;
12
11
  mount(parent?: VNode): Node;
13
12
  /**
14
13
  * An ELEMENT patch goes through three operations
@@ -1 +1 @@
1
- {"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUvC,qBAAa,YAAa,SAAQ,aAAa;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,CAA0D;IACtE,OAAO,CAAC,cAAc,CAAC,CAA6B;gBAElD,GAAG,EAAE,MAAM,EACX,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,EACxB,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IASd,QAAQ,IAAI,IAAI;IAOhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAW3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;CAgBzB"}
1
+ {"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAWvC,qBAAa,YAAa,SAAQ,aAAa;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,CAA0D;IACtE,OAAO,CAAC,cAAc,CAAC,CAA6B;gBAElD,GAAG,EAAE,MAAM,EACX,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,EACxB,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IASd,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAO3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;CAgBzB"}
@@ -17,12 +17,6 @@ export class ElementVNode extends AbstractVNode {
17
17
  this.key = key;
18
18
  this.ref = ref;
19
19
  }
20
- rerender() {
21
- const childrenElms = this.children
22
- .map((child) => child.getElements())
23
- .flat();
24
- this.elm.replaceChildren(...childrenElms);
25
- }
26
20
  mount(parent) {
27
21
  this.parent = parent;
28
22
  if (parent instanceof RootVNode) {
@@ -60,11 +54,9 @@ export class ElementVNode extends AbstractVNode {
60
54
  patch(newNode) {
61
55
  this.patchProps(newNode.props);
62
56
  this.props = newNode.props;
63
- this.children = this.patchChildren(newNode.children);
64
- const childrenElms = this.children
65
- .map((child) => child.getElements())
66
- .flat();
67
- this.elm.replaceChildren(...childrenElms);
57
+ const { children, operations } = this.patchChildren(newNode.children);
58
+ this.children = children;
59
+ this.applyDOMOperations(operations);
68
60
  }
69
61
  unmount() {
70
62
  this.children.forEach((child) => child.unmount());
@@ -6,7 +6,6 @@ export declare class FragmentVNode extends AbstractVNode {
6
6
  key?: string;
7
7
  constructor(children: VNode[], key?: string);
8
8
  mount(parent?: VNode): Node[];
9
- rerender(): void;
10
9
  patch(newNode: FragmentVNode): void;
11
10
  unmount(): void;
12
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,OAAO,EAAE,aAAa;IAG5B,OAAO;CAMR"}
1
+ {"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,KAAK,CAAC,OAAO,EAAE,aAAa;IAK5B,OAAO;CAMR"}
@@ -19,11 +19,10 @@ export class FragmentVNode extends AbstractVNode {
19
19
  }
20
20
  return this.children.map((child) => child.mount(this)).flat();
21
21
  }
22
- rerender() {
23
- this.parent?.rerender();
24
- }
25
22
  patch(newNode) {
26
- this.children = this.patchChildren(newNode.children);
23
+ const { children, operations } = this.patchChildren(newNode.children);
24
+ this.children = children;
25
+ this.applyDOMOperations(operations);
27
26
  }
28
27
  unmount() {
29
28
  this.children.forEach((child) => child.unmount());
@@ -1,4 +1,4 @@
1
- import { AbstractVNode } from "./AbstractVNode";
1
+ import { AbstractVNode, DOMOperation } from "./AbstractVNode";
2
2
  import { VNode } from "./types";
3
3
  import { ComponentInstance } from "./ComponentVNode";
4
4
  export declare let currentRoot: RootVNode | undefined;
@@ -6,9 +6,12 @@ export declare class RootVNode extends AbstractVNode {
6
6
  children: VNode[];
7
7
  componentStack: ComponentInstance[];
8
8
  private lifecycleQueue;
9
+ private hasPendingMicroTask;
10
+ private pendingObservers;
9
11
  constructor(rootNode: VNode, container: HTMLElement);
10
12
  queueMount(cb: () => void): void;
11
13
  queueUnmount(cb: () => void): void;
14
+ queueObserver(cb: () => void): void;
12
15
  flushLifecycle(): void;
13
16
  pushComponent(instance: ComponentInstance): void;
14
17
  popComponent(): void;
@@ -16,7 +19,7 @@ export declare class RootVNode extends AbstractVNode {
16
19
  clearCurrent(): void;
17
20
  mount(): Node | Node[];
18
21
  patch(): void;
19
- rerender(): void;
22
+ applyDOMOperations(operations: DOMOperation[], atVNode: VNode): void;
20
23
  unmount(): void;
21
24
  }
22
25
  //# sourceMappingURL=RootVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RootVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/RootVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,eAAO,IAAI,WAAW,EAAE,SAAS,GAAG,SAAS,CAAC;AAE9C,qBAAa,SAAU,SAAQ,aAAa;IAC1C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,cAAc,EAAE,iBAAiB,EAAE,CAAM;IACzC,OAAO,CAAC,cAAc,CAGpB;gBAEU,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW;IAMnD,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI;IAIzB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI;IAI3B,cAAc;IAOd,aAAa,CAAC,QAAQ,EAAE,iBAAiB;IAIzC,YAAY;IAIZ,YAAY;IAIZ,YAAY;IAMZ,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE;IAGtB,KAAK,IAAI,IAAI;IACb,QAAQ,IAAI,IAAI;IAShB,OAAO,IAAI,IAAI;CAChB"}
1
+ {"version":3,"file":"RootVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/RootVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,eAAO,IAAI,WAAW,EAAE,SAAS,GAAG,SAAS,CAAC;AAE9C,qBAAa,SAAU,SAAQ,aAAa;IAC1C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,cAAc,EAAE,iBAAiB,EAAE,CAAM;IACzC,OAAO,CAAC,cAAc,CAGpB;IACF,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,gBAAgB,CAAyB;gBAErC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW;IAMnD,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI;IAIzB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI;IAG3B,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI;IAa5B,cAAc;IAOd,aAAa,CAAC,QAAQ,EAAE,iBAAiB;IAIzC,YAAY;IAIZ,YAAY;IAIZ,YAAY;IAMZ,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE;IAGtB,KAAK,IAAI,IAAI;IACb,kBAAkB,CAAC,UAAU,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,IAAI;IAIpE,OAAO,IAAI,IAAI;CAChB"}
@@ -9,6 +9,8 @@ export class RootVNode extends AbstractVNode {
9
9
  toMount: [],
10
10
  toUnmount: [],
11
11
  };
12
+ hasPendingMicroTask = false;
13
+ pendingObservers = [];
12
14
  constructor(rootNode, container) {
13
15
  super();
14
16
  this.elm = container;
@@ -20,6 +22,19 @@ export class RootVNode extends AbstractVNode {
20
22
  queueUnmount(cb) {
21
23
  this.lifecycleQueue.toUnmount.push(cb);
22
24
  }
25
+ queueObserver(cb) {
26
+ this.pendingObservers.push(cb);
27
+ if (!this.hasPendingMicroTask) {
28
+ this.hasPendingMicroTask = true;
29
+ queueMicrotask(() => {
30
+ this.hasPendingMicroTask = false;
31
+ const pendingObservers = this.pendingObservers;
32
+ this.pendingObservers = [];
33
+ pendingObservers.forEach((pendingObserver) => pendingObserver());
34
+ this.flushLifecycle();
35
+ });
36
+ }
37
+ }
23
38
  flushLifecycle() {
24
39
  this.lifecycleQueue.toUnmount.forEach((cb) => cb());
25
40
  this.lifecycleQueue.toUnmount = [];
@@ -44,11 +59,8 @@ export class RootVNode extends AbstractVNode {
44
59
  return this.children.map((childNode) => childNode.mount(this)).flat();
45
60
  }
46
61
  patch() { }
47
- rerender() {
48
- const childrenElms = this.children
49
- .map((child) => child.getElements())
50
- .flat();
51
- this.elm.replaceChildren(...childrenElms);
62
+ applyDOMOperations(operations, atVNode) {
63
+ super.applyDOMOperations(operations, atVNode);
52
64
  this.flushLifecycle();
53
65
  }
54
66
  unmount() { }
@@ -5,7 +5,6 @@ export declare class TextVNode extends AbstractVNode {
5
5
  constructor(text: string);
6
6
  mount(parent?: VNode): Node;
7
7
  patch(newNode: TextVNode): void;
8
- rerender(): void;
9
8
  unmount(): void;
10
9
  }
11
10
  //# sourceMappingURL=TextVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/TextVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,SAAU,SAAQ,aAAa;IAC1C,IAAI,EAAE,MAAM,CAAC;gBACD,IAAI,EAAE,MAAM;IAIxB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAe3B,KAAK,CAAC,OAAO,EAAE,SAAS;IAIxB,QAAQ,IAAI,IAAI;IAChB,OAAO;CAMR"}
1
+ {"version":3,"file":"TextVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/TextVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,SAAU,SAAQ,aAAa;IAC1C,IAAI,EAAE,MAAM,CAAC;gBACD,IAAI,EAAE,MAAM;IAIxB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAe3B,KAAK,CAAC,OAAO,EAAE,SAAS;IAQxB,OAAO;CAMR"}
@@ -19,10 +19,12 @@ export class TextVNode extends AbstractVNode {
19
19
  return textNode;
20
20
  }
21
21
  patch(newNode) {
22
+ if (newNode.text === this.text) {
23
+ return;
24
+ }
22
25
  this.text = newNode.text;
23
26
  this.elm.textContent = this.text;
24
27
  }
25
- rerender() { }
26
28
  unmount() {
27
29
  this.root?.queueUnmount(() => {
28
30
  delete this.elm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",