@ryupold/vode 1.8.4 → 1.8.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/dist/vode.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  // src/vode.ts
2
2
  var globals = {
3
3
  currentViewTransition: void 0,
4
- requestAnimationFrame: !!window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : ((cb) => cb()),
5
- startViewTransition: !!document.startViewTransition ? document.startViewTransition.bind(document) : null
4
+ requestAnimationFrame: typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : ((cb) => cb()),
5
+ startViewTransition: typeof document !== "undefined" && typeof document.startViewTransition === "function" ? document.startViewTransition.bind(document) : null
6
6
  };
7
7
  function vode(tag2, props2, ...children2) {
8
8
  if (!tag2) throw new Error("first argument to vode() must be a tag name or a vode");
9
9
  if (Array.isArray(tag2)) return tag2;
10
- else if (props2) return [tag2, props2, ...children2];
10
+ else if (typeof props2 === "object") return [tag2, props2, ...children2];
11
11
  else return [tag2, ...children2];
12
12
  }
13
13
  function app(container, state, dom, ...initialPatches) {
@@ -20,6 +20,7 @@ function app(container, state, dom, ...initialPatches) {
20
20
  _vode.qSync = null;
21
21
  _vode.qAsync = null;
22
22
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
23
+ _vode.unmounts = [];
23
24
  const patchableState = state;
24
25
  if ("patch" in state && typeof state.patch === "function" && Array.isArray(state.patch.initialPatches)) {
25
26
  initialPatches = [...state.patch.initialPatches, ...initialPatches];
@@ -90,7 +91,7 @@ function app(container, state, dom, ...initialPatches) {
90
91
  function renderDom(isAsync) {
91
92
  const sw = Date.now();
92
93
  const vom = dom(_vode.state);
93
- _vode.vode = render(_vode.state, container.parentElement, 0, 0, _vode.vode, vom);
94
+ _vode.vode = render(_vode.state, container.parentElement, 0, 0, _vode.vode, vom, null, _vode.unmounts, 0);
94
95
  if (container.tagName.toUpperCase() !== vom[0].toUpperCase()) {
95
96
  container = _vode.vode.node;
96
97
  container._vode = _vode;
@@ -143,14 +144,20 @@ function app(container, state, dom, ...initialPatches) {
143
144
  const root = container;
144
145
  root._vode = _vode;
145
146
  const indexInParent = Array.from(container.parentElement.children).indexOf(container);
147
+ _vode.isRendering = true;
146
148
  _vode.vode = render(
147
149
  state,
148
150
  container.parentElement,
149
151
  indexInParent,
150
152
  indexInParent,
151
153
  hydrate(container, true),
152
- dom(state)
154
+ dom(state),
155
+ null,
156
+ _vode.unmounts,
157
+ 0
153
158
  );
159
+ _vode.isRendering = false;
160
+ if (_vode.qSync) _vode.renderSync();
154
161
  for (const effect of initialPatches) {
155
162
  patchableState.patch(effect);
156
163
  }
@@ -200,8 +207,6 @@ function hydrate(element, prepareForRender) {
200
207
  if (element.nodeValue?.trim() !== "")
201
208
  return prepareForRender ? element : element.nodeValue;
202
209
  return void 0;
203
- } else if (element.nodeType === Node.COMMENT_NODE) {
204
- return void 0;
205
210
  } else if (element.nodeType === Node.ELEMENT_NODE) {
206
211
  const tag2 = element.tagName.toLowerCase();
207
212
  const root = [tag2];
@@ -319,7 +324,7 @@ function mergeState(target, source, allowDeletion) {
319
324
  }
320
325
  return target;
321
326
  }
322
- function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmlns) {
327
+ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmlns, unmounts, unmountStart) {
323
328
  try {
324
329
  newVode = remember(state, newVode, oldVode);
325
330
  const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
@@ -329,7 +334,17 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
329
334
  const oldIsText = oldVode?.nodeType === Node.TEXT_NODE;
330
335
  const oldNode = oldIsText ? oldVode : oldVode?.node;
331
336
  if (isNoVode) {
332
- oldNode?.onUnmount && state.patch(oldNode.onUnmount(oldNode));
337
+ if (!oldIsText && typeof oldVode?.unmountCount === "number") {
338
+ const start = oldVode.unmountStart;
339
+ const count = oldVode.unmountCount;
340
+ for (let i = count - 1; i >= 0; i--) {
341
+ const fn = unmounts[start + i];
342
+ if (fn) {
343
+ state.patch(fn(state, oldNode));
344
+ unmounts[start + i] = null;
345
+ }
346
+ }
347
+ }
333
348
  oldNode?.remove();
334
349
  return void 0;
335
350
  }
@@ -352,7 +367,17 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
352
367
  if (isText && (!oldNode || !oldIsText)) {
353
368
  const text = document.createTextNode(newVode);
354
369
  if (oldNode) {
355
- oldNode.onUnmount && state.patch(oldNode.onUnmount(oldNode));
370
+ if (!oldIsText && typeof oldVode?.unmountCount === "number") {
371
+ const start = oldVode.unmountStart;
372
+ const count = oldVode.unmountCount;
373
+ for (let i = count - 1; i >= 0; i--) {
374
+ const fn = unmounts[start + i];
375
+ if (fn) {
376
+ state.patch(fn(state, oldNode));
377
+ unmounts[start + i] = null;
378
+ }
379
+ }
380
+ }
356
381
  oldNode.replaceWith(text);
357
382
  } else {
358
383
  let inserted = false;
@@ -385,9 +410,21 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
385
410
  newVode.node.removeAttribute("catch");
386
411
  }
387
412
  if (oldNode) {
388
- oldNode.onUnmount && state.patch(oldNode.onUnmount(oldNode));
413
+ if (!oldIsText && typeof oldVode?.unmountCount === "number") {
414
+ const start = oldVode.unmountStart;
415
+ const count = oldVode.unmountCount;
416
+ for (let i = count - 1; i >= 0; i--) {
417
+ const fn = unmounts[start + i];
418
+ if (fn) {
419
+ state.patch(fn(state, oldNode));
420
+ unmounts[start + i] = null;
421
+ }
422
+ }
423
+ }
424
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
389
425
  oldNode.replaceWith(newNode);
390
426
  } else {
427
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
391
428
  let inserted = false;
392
429
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
393
430
  const nextSibling = parent.childNodes[i];
@@ -401,18 +438,27 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
401
438
  parent.appendChild(newNode);
402
439
  }
403
440
  }
441
+ let totalChildUnmounts = 0;
442
+ let childUnmountStart = unmountStart + 1;
404
443
  const newKids = children(newVode);
405
444
  if (newKids) {
406
445
  const childOffset = !!properties ? 2 : 1;
407
446
  let indexP = 0;
408
447
  for (let i = 0; i < newKids.length; i++) {
409
448
  const child2 = newKids[i];
410
- const attached = render(state, newNode, i, indexP, void 0, child2, xmlns ?? null);
449
+ const attached = render(state, newNode, i, indexP, void 0, child2, xmlns ?? null, unmounts, childUnmountStart);
411
450
  newVode[i + childOffset] = attached;
412
- if (attached) indexP++;
451
+ if (attached) {
452
+ indexP++;
453
+ const childUnmounts = attached.unmountCount || 0;
454
+ totalChildUnmounts += childUnmounts;
455
+ childUnmountStart += childUnmounts;
456
+ }
413
457
  }
414
458
  }
415
459
  newNode.onMount && state.patch(newNode.onMount(newNode));
460
+ newVode.unmountCount = 1 + totalChildUnmounts;
461
+ newVode.unmountStart = unmountStart;
416
462
  return newVode;
417
463
  }
418
464
  if (!oldIsText && isNode && oldVode[0] === newVode[0]) {
@@ -435,6 +481,9 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
435
481
  newVode.node["catch"] = null;
436
482
  newVode.node.removeAttribute("catch");
437
483
  }
484
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
485
+ let totalChildUnmounts = 0;
486
+ let childUnmountStart = unmountStart + 1;
438
487
  const newKids = children(newVode);
439
488
  const oldKids = children(oldVode);
440
489
  if (newKids) {
@@ -443,17 +492,24 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
443
492
  for (let i = 0; i < newKids.length; i++) {
444
493
  const child2 = newKids[i];
445
494
  const oldChild = oldKids && oldKids[i];
446
- const attached = render(state, oldNode, i, indexP, oldChild, child2, xmlns);
495
+ const attached = render(state, oldNode, i, indexP, oldChild, child2, xmlns, unmounts, childUnmountStart);
447
496
  newVode[i + childOffset] = attached;
448
- if (attached) indexP++;
497
+ if (attached) {
498
+ indexP++;
499
+ const childUnmounts = attached.unmountCount || 0;
500
+ totalChildUnmounts += childUnmounts;
501
+ childUnmountStart += childUnmounts;
502
+ }
449
503
  }
450
504
  }
451
505
  if (oldKids) {
452
506
  const newKidsCount = newKids ? newKids.length : 0;
453
507
  for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
454
- render(state, oldNode, i, i, oldKids[i], void 0, xmlns);
508
+ render(state, oldNode, i, i, oldKids[i], void 0, xmlns, unmounts, oldKids[i].unmountStart);
455
509
  }
456
510
  }
511
+ newVode.unmountCount = 1 + totalChildUnmounts;
512
+ newVode.unmountStart = unmountStart;
457
513
  return newVode;
458
514
  }
459
515
  } catch (error) {
@@ -467,7 +523,9 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
467
523
  indexInParent,
468
524
  hydrate(newVode?.node || oldVode?.node, true),
469
525
  handledVode,
470
- xmlns
526
+ xmlns,
527
+ unmounts,
528
+ unmountStart
471
529
  );
472
530
  } else {
473
531
  throw error;
@@ -496,9 +554,28 @@ function remember(state, present, past) {
496
554
  }
497
555
  if (same) return past;
498
556
  }
499
- const newRender = unwrap(present, state);
557
+ const result = present(state);
558
+ if (typeof result === "function" && result?.__memo) {
559
+ const resultMemo = result.__memo;
560
+ if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
561
+ let same = true;
562
+ for (let i = 0; i < resultMemo.length; i++) {
563
+ if (resultMemo[i] !== pastMemo[i]) {
564
+ same = false;
565
+ break;
566
+ }
567
+ }
568
+ if (same) return past;
569
+ }
570
+ const innerRender = result(state);
571
+ if (typeof innerRender === "object") {
572
+ innerRender.__memo = resultMemo;
573
+ }
574
+ return innerRender;
575
+ }
576
+ const newRender = typeof result === "function" ? unwrap(result, state) : result;
500
577
  if (typeof newRender === "object") {
501
- newRender.__memo = present?.__memo;
578
+ newRender.__memo = result?.__memo || present?.__memo;
502
579
  }
503
580
  return newRender;
504
581
  }
@@ -857,8 +934,11 @@ function mergeClass(...classes) {
857
934
  }
858
935
 
859
936
  // src/merge-style.ts
860
- var tempDivForStyling = document.createElement("div");
937
+ var tempDivForStyling;
861
938
  function mergeStyle(...props2) {
939
+ if (!tempDivForStyling) {
940
+ tempDivForStyling = document.createElement("div");
941
+ }
862
942
  try {
863
943
  const merged = tempDivForStyling.style;
864
944
  for (const style of props2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -46,12 +46,13 @@
46
46
  "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types",
47
47
  "publish": "npm publish --access public",
48
48
  "clean": "tsc -b --clean && rm dist/*",
49
- "watch": "tsc -b -w"
49
+ "watch": "tsc -b -w",
50
+ "test": "tsc -p tsconfig.test.json && esbuild test/index.ts --bundle --outfile=test/bundle.js --platform=node && node test/bundle.js"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@babel/cli": "7.28.6",
53
54
  "@babel/core": "7.29.0",
54
- "@babel/preset-env": "7.29.3",
55
+ "@babel/preset-env": "7.29.5",
55
56
  "babel-preset-minify": "0.5.2",
56
57
  "dts-bundle-generator": "9.5.1",
57
58
  "esbuild": "0.28.0",
@@ -1,10 +1,13 @@
1
1
  import { StyleProp } from "./vode";
2
2
 
3
- const tempDivForStyling = document.createElement('div');
3
+ let tempDivForStyling: HTMLElement | undefined;
4
4
 
5
5
  /** merge `StyleProps`s regardless of type
6
6
  * @returns {string} merged StyleProp */
7
7
  export function mergeStyle(...props: StyleProp[]): StyleProp {
8
+ if (!tempDivForStyling) {
9
+ tempDivForStyling = document.createElement('div');
10
+ }
8
11
  try{
9
12
  const merged = tempDivForStyling.style;
10
13
  for (const style of props) {
package/src/vode.ts CHANGED
@@ -5,7 +5,7 @@ export type JustTagVode = [tag: Tag];
5
5
  export type ChildVode<S = PatchableState> = Vode<S> | TextVode | NoVode | Component<S>;
6
6
  export type TextVode = string & {};
7
7
  export type NoVode = undefined | null | number | boolean | bigint | void;
8
- export type AttachedVode<S> = Vode<S> & { node: ChildNode } | Text & { node?: never };
8
+ export type AttachedVode<S> = Vode<S> & { node: ChildNode, unmountCount: number, unmountStart: number } | Text & { node?: never, unmounts?: never, unmountStart?: never };
9
9
  export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap) | (string & {});
10
10
  export type Component<S> = (s: S) => ChildVode<S>;
11
11
 
@@ -78,8 +78,8 @@ export type PatchableState<S = object> = S & Patchable<S>;
78
78
 
79
79
  export const globals = {
80
80
  currentViewTransition: <ViewTransition | null | undefined>undefined,
81
- requestAnimationFrame: !!window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : ((cb: () => void) => cb()),
82
- startViewTransition: !!document.startViewTransition ? document.startViewTransition.bind(document) : null,
81
+ requestAnimationFrame: typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : ((cb: () => void) => cb()),
82
+ startViewTransition: typeof document !== "undefined" && typeof document.startViewTransition === "function" ? document.startViewTransition.bind(document) : null,
83
83
  };
84
84
 
85
85
  export interface ContainerNode<S = PatchableState> extends HTMLElement {
@@ -97,6 +97,7 @@ export interface ContainerNode<S = PatchableState> extends HTMLElement {
97
97
  qAsync: {} | undefined | null, // next render-patches to be animated after another
98
98
  isRendering: boolean,
99
99
  isAnimating: boolean,
100
+ unmounts: (MountFunction<S> | null)[],
100
101
  /** stats about the overall patches & last render time */
101
102
  stats: {
102
103
  patchCount: number,
@@ -122,7 +123,7 @@ export function vode<S = PatchableState>(tag: Tag | Vode<S>, props?: Props<S> |
122
123
  if (!tag) throw new Error("first argument to vode() must be a tag name or a vode");
123
124
 
124
125
  if (Array.isArray(tag)) return tag;
125
- else if (props) return [tag, props as Props<S>, ...children];
126
+ else if (typeof props === "object") return [tag, props as Props<S>, ...children];
126
127
  else return [tag, ...children];
127
128
  }
128
129
 
@@ -149,6 +150,7 @@ export function app<S extends PatchableState = PatchableState>(
149
150
  _vode.qSync = null;
150
151
  _vode.qAsync = null;
151
152
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
153
+ _vode.unmounts = [];
152
154
 
153
155
  const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void };
154
156
 
@@ -219,7 +221,7 @@ export function app<S extends PatchableState = PatchableState>(
219
221
  function renderDom(isAsync: boolean) {
220
222
  const sw = Date.now();
221
223
  const vom = dom(_vode.state);
222
- _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
224
+ _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom, null, _vode.unmounts, 0)!;
223
225
 
224
226
  if ((<ContainerNode<S>>container).tagName.toUpperCase() !== (vom[0] as Tag).toUpperCase()) { //the tag name was changed during render -> update reference to vode-app-root
225
227
  container = _vode.vode.node as Element;
@@ -280,14 +282,20 @@ export function app<S extends PatchableState = PatchableState>(
280
282
  const root = container as ContainerNode<S>;
281
283
  root._vode = _vode;
282
284
  const indexInParent = Array.from(container.parentElement.children).indexOf(container);
285
+ _vode.isRendering = true;
283
286
  _vode.vode = render(
284
287
  <S>state,
285
288
  container.parentElement,
286
289
  indexInParent,
287
290
  indexInParent,
288
291
  hydrate<S>(container, true) as AttachedVode<S>,
289
- dom(<S>state)
292
+ dom(<S>state),
293
+ null,
294
+ _vode.unmounts,
295
+ 0
290
296
  )!;
297
+ _vode.isRendering = false;
298
+ if (_vode.qSync) _vode.renderSync();
291
299
 
292
300
  for (const effect of initialPatches) {
293
301
  patchableState.patch(effect);
@@ -349,9 +357,6 @@ export function hydrate<S = PatchableState>(element: Element | Text, prepareForR
349
357
  return prepareForRender ? element as Text : (element as Text).nodeValue!;
350
358
  return undefined; //ignore (mostly html whitespace)
351
359
  }
352
- else if (element.nodeType === Node.COMMENT_NODE) {
353
- return undefined; //ignore (not interesting)
354
- }
355
360
  else if (element.nodeType === Node.ELEMENT_NODE) {
356
361
  const tag: Tag = (<Element>element).tagName.toLowerCase();
357
362
  const root: Vode<S> = [tag];
@@ -506,14 +511,20 @@ function mergeState(target: any, source: any, allowDeletion: boolean) {
506
511
  return target;
507
512
  };
508
513
 
509
- function render<S extends PatchableState>(state: S, parent: Element, childIndex: number, indexInParent: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, xmlns?: string | null): AttachedVode<S> | undefined {
514
+ function render<S extends PatchableState>(
515
+ state: S, parent: Element,
516
+ childIndex: number, indexInParent: number,
517
+ oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>,
518
+ xmlns: string | null,
519
+ unmounts: (MountFunction<S> | null)[], unmountStart: number
520
+ ): AttachedVode<S> | undefined {
510
521
  try {
511
522
  // unwrap component if it is memoized
512
523
  newVode = remember(state, newVode, oldVode) as ChildVode<S>;
513
524
 
514
525
  const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
515
526
  if (newVode === oldVode || (!oldVode && isNoVode)) {
516
- return oldVode;
527
+ return oldVode as AttachedVode<S>;
517
528
  }
518
529
 
519
530
  const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
@@ -521,7 +532,17 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
521
532
 
522
533
  // falsy|text|element(A) -> undefined
523
534
  if (isNoVode) {
524
- (<any>oldNode)?.onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
535
+ if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
536
+ const start = (<any>oldVode).unmountStart;
537
+ const count = (<any>oldVode).unmountCount;
538
+ for (let i = count - 1; i >= 0; i--) {
539
+ const fn = unmounts[start + i];
540
+ if (fn) {
541
+ state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
542
+ unmounts[start + i] = null;
543
+ }
544
+ }
545
+ }
525
546
  oldNode?.remove();
526
547
  return undefined;
527
548
  }
@@ -545,13 +566,23 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
545
566
  if ((<Text>oldNode).nodeValue !== <string>newVode) {
546
567
  (<Text>oldNode).nodeValue = <string>newVode;
547
568
  }
548
- return oldVode;
569
+ return oldVode as AttachedVode<S>;
549
570
  }
550
571
  // falsy|element -> text
551
572
  if (isText && (!oldNode || !oldIsText)) {
552
573
  const text = document.createTextNode(newVode as string)
553
574
  if (oldNode) {
554
- (<any>oldNode).onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
575
+ if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
576
+ const start = (<any>oldVode).unmountStart;
577
+ const count = (<any>oldVode).unmountCount;
578
+ for (let i = count - 1; i >= 0; i--) {
579
+ const fn = unmounts[start + i];
580
+ if (fn) {
581
+ state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
582
+ unmounts[start + i] = null;
583
+ }
584
+ }
585
+ }
555
586
  oldNode.replaceWith(text);
556
587
  } else {
557
588
  let inserted = false;
@@ -597,9 +628,21 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
597
628
  }
598
629
 
599
630
  if (oldNode) {
600
- (<any>oldNode).onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
631
+ if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
632
+ const start = (<any>oldVode).unmountStart;
633
+ const count = (<any>oldVode).unmountCount;
634
+ for (let i = count - 1; i >= 0; i--) {
635
+ const fn = unmounts[start + i];
636
+ if (fn) {
637
+ state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
638
+ unmounts[start + i] = null;
639
+ }
640
+ }
641
+ }
642
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
601
643
  oldNode.replaceWith(newNode);
602
644
  } else {
645
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
603
646
  let inserted = false;
604
647
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
605
648
  const nextSibling = parent.childNodes[i];
@@ -614,20 +657,28 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
614
657
  }
615
658
  }
616
659
 
660
+ let totalChildUnmounts = 0;
661
+ let childUnmountStart = unmountStart + 1;
617
662
  const newKids = children(newVode);
618
663
  if (newKids) {
619
664
  const childOffset = !!properties ? 2 : 1;
620
665
  let indexP = 0;
621
666
  for (let i = 0; i < newKids.length; i++) {
622
667
  const child = newKids[i];
623
- // render child in xml mode to prevent using the dom properties
624
- const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null);
625
- (<Vode<S>>newVode!)[i + childOffset] = <Vode<S>>attached;
626
- if (attached) indexP++;
668
+ const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null, unmounts, childUnmountStart);
669
+ (<Vode<S>>newVode!)[i + childOffset] = <Vode<S> | undefined>attached;
670
+ if (attached) {
671
+ indexP++;
672
+ const childUnmounts = (<any>attached).unmountCount || 0;
673
+ totalChildUnmounts += childUnmounts;
674
+ childUnmountStart += childUnmounts;
675
+ }
627
676
  }
628
677
  }
629
678
 
630
679
  (<any>newNode).onMount && state.patch((<any>newNode).onMount(newNode));
680
+ (<any>newVode).unmountCount = 1 + totalChildUnmounts;
681
+ (<any>newVode).unmountStart = unmountStart;
631
682
  return <AttachedVode<S>>newVode;
632
683
  }
633
684
 
@@ -659,6 +710,11 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
659
710
  (<any>newVode).node.removeAttribute('catch');
660
711
  }
661
712
 
713
+ // own unmount slot (always reserved per element)
714
+ unmounts[unmountStart] = properties?.onUnmount ?? null;
715
+
716
+ let totalChildUnmounts = 0;
717
+ let childUnmountStart = unmountStart + 1;
662
718
  const newKids = children(newVode);
663
719
  const oldKids = children(oldVode) as AttachedVode<S>[];
664
720
  if (newKids) {
@@ -667,20 +723,26 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
667
723
  for (let i = 0; i < newKids.length; i++) {
668
724
  const child = newKids[i];
669
725
  const oldChild = oldKids && oldKids[i];
670
-
671
- const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
726
+ const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns, unmounts, childUnmountStart);
672
727
  (<Vode<S>>newVode)[i + childOffset] = <Vode<S>>attached;
673
- if (attached) indexP++;
728
+ if (attached) {
729
+ indexP++;
730
+ const childUnmounts = (<any>attached).unmountCount || 0;
731
+ totalChildUnmounts += childUnmounts;
732
+ childUnmountStart += childUnmounts;
733
+ }
674
734
  }
675
735
  }
676
736
 
677
737
  if (oldKids) {
678
738
  const newKidsCount = newKids ? newKids.length : 0;
679
739
  for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
680
- render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns);
740
+ render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns, unmounts, (<any>oldKids[i]).unmountStart);
681
741
  }
682
742
  }
683
743
 
744
+ (<any>newVode).unmountCount = 1 + totalChildUnmounts;
745
+ (<any>newVode).unmountStart = unmountStart;
684
746
  return <AttachedVode<S>>newVode;
685
747
  }
686
748
  } catch (error) {
@@ -693,7 +755,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
693
755
  return render(state, parent, childIndex, indexInParent,
694
756
  hydrate(((<AttachedVode<S>>newVode)?.node || oldVode?.node) as Element, true) as AttachedVode<S>,
695
757
  handledVode,
696
- xmlns);
758
+ xmlns, unmounts, unmountStart);
697
759
  } else {
698
760
  throw error;
699
761
  }
@@ -730,9 +792,31 @@ function remember<S>(state: S, present: any, past: any): ChildVode<S> | Attached
730
792
  }
731
793
  if (same) return past;
732
794
  }
733
- const newRender = unwrap(present, state);
795
+
796
+ const result = present(state);
797
+
798
+ if (typeof result === "function" && result?.__memo) {
799
+ const resultMemo = result.__memo;
800
+ if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
801
+ let same = true;
802
+ for (let i = 0; i < resultMemo.length; i++) {
803
+ if (resultMemo[i] !== pastMemo[i]) {
804
+ same = false;
805
+ break;
806
+ }
807
+ }
808
+ if (same) return past;
809
+ }
810
+ const innerRender = result(state);
811
+ if (typeof innerRender === "object") {
812
+ innerRender.__memo = resultMemo;
813
+ }
814
+ return innerRender;
815
+ }
816
+
817
+ const newRender = typeof result === "function" ? unwrap(result, state) : result;
734
818
  if (typeof newRender === "object") {
735
- (<any>newRender).__memo = present?.__memo;
819
+ (<any>newRender).__memo = result?.__memo || present?.__memo;
736
820
  }
737
821
  return newRender;
738
822
  }