thi-ng / umbrella

⛱ Broadly scoped ecosystem & mono-repository of 199 TypeScript projects (and ~180 examples) for general purpose, functional, data driven development
https://thi.ng
Apache License 2.0
3.38k stars 150 forks source link

[rdom]: updating state inside a $replace that no longer has it's parent #410

Open maxsei opened 1 year ago

maxsei commented 1 year ago

It seems that continuously updating the state of a $replace component that has a $replace inside of it causes the inner $replace to get into an invalid state because the parent no longer exists. Is this intended behavior and/or misuse of rdom, or can this problem be solved within rdom itself?

import { defAtom } from "@thi.ng/atom";
import * as rx from "@thi.ng/rstream";
import * as tx from "@thi.ng/transducers";
import { ComponentLike } from "@thi.ng/rdom";
import { $compile, $replace } from "@thi.ng/rdom";

const root = document.getElementById("root");
if (!(root instanceof HTMLElement)) throw new Error("Root element not found");

const modal = defAtom<ComponentLike[]>([]);
const modalContainer = (body?: ComponentLike) =>
  body && [
    "div.absolute.absolute--fill.justify-center.items-center.flex.dn",
    { style: { "pointer-events": "none" } },
    body,
  ];

const window_of_5_random_numbers = ()=>rx
  .fromInterval(1000)
  .transform(
    tx.comp(
      tx.map(() => parseFloat(Math.random().toFixed(3))),
      tx.slidingWindow(5),
      tx.map((xx) => ["div", {}, xx.join(", ")]),
    ),
  )
  .subscribe(rx.trace("window_of_5_random_numbers"));

const rdom = $compile([
  "div.relative.vh-100",
  {},
  $replace(
    rx
      .fromAtom(modal)
      .subscribe(rx.trace("modal"))
      .map((x) => x[0])
      .map(modalContainer),
  ),
  [
    "button",
    {
      onclick: () => {
        modal.swap((prev) => [...prev, $replace(window_of_5_random_numbers())]);
      },
    },
    "Click Me to Push Random Numbers into the Modal",
  ],
  [
    "button",
    {
      onclick: () => {
        modal.swap((prev) => {
          prev.shift();
          return [...prev];
        });
      },
    },
    "click me to pop modal",
  ],
]);
rdom.mount(root);

After clicking the "Click Me to Push Random Numbers into the Modal" twice in a row

Uncaught (in promise) TypeError: this.parent is undefined
    mount sub.js:27
    mount compile.js:74
    update replace.js:60
    next sub.js:39
    add scheduler.js:41
    next sub.js:39
    dispatchTo subscription.js:219
    dispatch subscription.js:233
    dispatchXformVals subscription.js:267
    dispatchXform subscription.js:247
    next subscription.js:163
    dispatchTo subscription.js:219
    dispatch subscription.js:233
    dispatchXformVals subscription.js:267
    dispatchXform subscription.js:247
    next subscription.js:163
    dispatchTo subscription.js:219
    dispatch subscription.js:233
    next subscription.js:163
    dispatchTo subscription.js:219
    dispatch subscription.js:233
    next subscription.js:163
    fromAtom atom.js:44
    notifyWatches iwatch.js:25
    reset atom.js:37
    swap atom.js:47
    onclick main.ts:43
    setAttrib dom.js:275
    $attribs dom.js:261
    $el dom.js:130
    $treeTag dom.js:76
    $treeElem dom.js:58
    $tree dom.js:44
    mount compile.js:96
    mount compile.js:74
    <anonymous> main.ts:61
sub.js:27:8
maxsei commented 1 year ago

It turns out you can avoid this problem entirely by wrapping the componentlike's inside the modal atom with Fn0. Perhaps this issue could be moved to discussions?

import { defAtom } from "@thi.ng/atom";
import * as rx from "@thi.ng/rstream";
import * as tx from "@thi.ng/transducers";
import { ComponentLike } from "@thi.ng/rdom";
import { $compile, $replace } from "@thi.ng/rdom";
import { Fn0 } from "@thi.ng/api";

const root = document.getElementById("root");
if (!(root instanceof HTMLElement)) throw new Error("Root element not found");

const modal = defAtom<Fn0<ComponentLike>[]>([]);
const modalContainer = (body?: Fn0<ComponentLike>) =>
  body && [
    "div.absolute.absolute--fill.justify-center.items-center.flex.dn",
    { style: { "pointer-events": "none" } },
    body(),
  ];

const rdom = $compile([
  "div.relative.vh-100",
  {},
  $replace(
    rx
      .fromAtom(modal)
      .subscribe(rx.trace("modal"))
      .map((x) => modalContainer(x[0])),
  ),
  [
    "button",
    {
      onclick: () => {
        const window_of_5_random_numbers = rx
          .fromInterval(1000)
          .transform(
            tx.comp(
              tx.map(() => parseFloat(Math.random().toFixed(3))),
              tx.slidingWindow(5),
              tx.map((xx) => ["div", {}, xx.join(", ")]),
            ),
          )
          .subscribe(rx.trace("window_of_5_random_numbers"));
        const next = () => $replace(window_of_5_random_numbers);
        modal.swap((prev) => [...prev, next]);
      },
    },
    "Click Me to Push Random Numbers into the Modal",
  ],
  [
    "button",
    {
      onclick: () => {
        modal.swap((prev) => {
          prev.shift();
          return [...prev];
        });
      },
    },
    "click me to pop modal",
  ],
]);
rdom.mount(root);