thi-ng / umbrella

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

[rdom] unexpected dispatch $replace after change route / reload layout #477

Closed bit-app-3000 closed 3 weeks ago

bit-app-3000 commented 1 month ago

https://github.com/thi-ng/umbrella/assets/42169423/d887122f-e34e-4555-b044-84a956a66144

// DropDown

import { $replace } from '@thi.ng/rdom'
import { fromViewUnsafe } from '@thi.ng/rstream'
import { states$, toggle, useSlot } from '../../modules/index.js'
import { Menu } from '../index.js'

const slot = x => {
  const { state } = x

  return [
    'dropdown',
    {
      onpointerdown: toggle(x)
    },
    ['button', { type: 'button' }, 'Drop Down'],
    state
      ? Menu(x)
      : null
  ]
}

export const Dropdown = seed => [
  'control', {},
  $replace(fromViewUnsafe(states$, { path: useSlot(seed), tx: slot }))
]

// Menu

import { $list } from '@thi.ng/rdom'
import { cancel, end, start } from '../../modules/slot.js'
import { Icon } from '../index.js'

const Item = ({ ico, label }) => ['li', {}, Icon({ id: ico }), label]

export const Menu = x => {
  // LOG('MENU:RDR')

  const { id, state, placement, data$ } = x

  return $list(
    data$,
    'menu',
    {

      id,
      state,
      placement,

      onanimationstart: start(x),
      onanimationcancel: cancel(x),
      onanimationend: end(x)
    },
    Item
  )
}
// State from Atom

export const states$ = defAtom({})

//  useSlot

import { defCursor } from '@thi.ng/atom'
import { states$ } from './hub.js'

export function useSlot (seed) {
  const { id } = seed

  if (!Reflect.has(states$.deref(), id)) {
    const cursor = defCursor(states$, id)
    seed.placement = 'bottom'
    seed.state = 'hidden'

    cursor.reset(seed)
  }
  return id
}
// Page / Layout 

import { data$ } from '../../modules/index.js'
import { Dropdown, Top } from '../index.js'

export const Dashboard = () =>
  [
    'main', {},
    Top(),
    ['header', {}, 'DASHBOARD'],
    [
      'section', {},
      [
        'bar', {},
        Dropdown({
          id: 'x1',
          data$
        })]]

  ]
postspectacular commented 1 month ago

@bit-app-3000 Can you please describe a little more where/what the issue is or rather what the expected behavior should be and what you feel is going wrong? I.e. a bit more context please... Sorry, I watched the video a few times, but can't really figure out what parts are wrong... Even better would be a codesandbox or upload the example somewhere so it can be debugged... thanks! :)

bit-app-3000 commented 1 month ago

thanks for the response

expected behavior : function (component) is executed (dispatched) once unexpected behavior : the number of function (component) calls accumulates

postspectacular commented 1 month ago

I'm sorry, @bit-app-3000 — I've downloaded the video and went through it step by step, but without a working example or at least a little description of what you're doing I don't even know which part (or component) I'm supposed to focus on (I'm guessing the dropdown?). I'm a busy with lots of things, but I really do want to help — though, that also requires from you, as the person asking for help, to provide a bit more context/details, please... thank you! 👍

bit-app-3000 commented 1 month ago

made an example

src: https://github.com/bit-app-3000/dummy

public: https://dummy-3xu.pages.dev

I hope it helps

postspectacular commented 1 month ago

Hey again — thank you for this, but again: Please describe and explain in a few sentences what I should be looking at here?

All I understand from the little (context) you provided so far is that some component is being unmounted/replaced multiple times (from the log messages it seems maybe the dropdown), but again if you cannot describe the problem a bit more, I unfortunately really don't know how I can help you... That example project of your has way too many files (most unrelated to the problem at hand) and from the title of this issue, I really just don't have enough information to understand what the actual problem is here... and it's not for a lack of trying!

Maybe I'm missing something obvious, so also grateful if anyone else would like to chip in here...

bit-app-3000 commented 1 month ago

unexpected dispatch $replace after change route / reload layout

dispatch dropdown after change route executed more than once

maybe not unsubscribe correctly

bit-app-3000 commented 1 month ago

you can see in the logs that the execution of the function is accumulating in this case a dropdown component

postspectacular commented 4 weeks ago

Hi @bit-app-3000 — so because your project has way too many files and I also couldn't get it to run locally, I still couldn't figure out what is going wrong in your case, but I've just created & uploaded a new example with a similar setup/task, i.e. a router & atom-based component switcher (incl. dynamic/reactive lists of images). I hope this much more stripped down example will help you figure out what might be wrong on your end, but I'm afraid that's all I can offer you here... obviously always happy to answer any other related questions

Demo: https://demo.thi.ng/umbrella/rdom-router/

Source: https://github.com/thi-ng/umbrella/blob/develop/examples/rdom-router/src/index.ts

Really hope that helps!

postspectacular commented 3 weeks ago

@bit-app-3000 I just updated the example with some more features and more comments (also requires a newer version of thi.ng/router, just published) :) Hth!

bit-app-3000 commented 3 weeks ago

unexpected behavior occurs when using the component function signature (hiccup)

[fn, arg1, arg2, ...]

if you use the direct call in the tree it works as it should

[tag, {},  fn(args)]

Please add an example using such a signature

["tag", {...}, "body", 23, function, [...]]
[function, arg1, arg2, ...]

https://github.com/thi-ng/umbrella/tree/develop/packages/hiccup#what-is-hiccup

postspectacular commented 3 weeks ago

Those should be working like this:

const myComponent = (...items: any[]) => [
    "div",
    { class: "custom" },
    ...items.map((x) => x.toUpperCase()).join(", "),
];

$compile([myComponent, "yabba", "dabba", "doo"]).mount(document.body);

Zero-arg functions in child/body positions... (only supported since rdom v1.5.0, released just now)

const random = () => ["li", {}, Math.floor(Math.random() * 100)];

$compile(["ul", {}, random, random, random]).mount(document.body);
bit-app-3000 commented 3 weeks ago

Hi @postspectacular

made a minimal example with unexpected behavior of the component

import { defAtom } from '@thi.ng/atom'
import { ConsoleLogger, ROOT } from '@thi.ng/logger'
import { $compile, $replace, $switch } from '@thi.ng/rdom'
import { EVENT_ROUTE_CHANGED, HTMLRouter } from '@thi.ng/router'
import { fromView } from '@thi.ng/rstream'
import { cycle } from '@thi.ng/transducers'

ROOT.set(new ConsoleLogger())

const routes = [
  { id: 'home', match: ['home'] },
  { id: 'about', match: ['about'] },
  { id: 'profile', match: ['profile'] }
]

const router = new HTMLRouter({
  routes,
  default: 'home',
  useFragment: true
})

const db = defAtom({
  route: { id: 'home' },
  x1: { state: false, label: '🤠' },
  x2: { state: false, label: '☠️' }
})

// Pop Component
const emojis = cycle(['🥳', '🙂‍', '️😏', '😒', '🙂‍', '️😞', '😔', '😕', '🙁'])

const toggle = id => () =>
  db.swapIn(id, last => ({
    ...last,
    state: !last.state,
    label: last.state ? emojis.next().value : last.label
  }))

const slot = x => {
  const { state, label } = x
  return state
    ? ['emoji', {}, label]
    : null
}

export const pop = (id, desc) =>
  [
    'pop', {},
    ['button', { onpointerdown: toggle(id) }, desc],
    $replace(fromView(db, { path: id, tx: slot }))
  ]

// PageNav
const nav = () => [
  'nav', {},
  ...routes.map(({ id }) => ['a', { href: router.format(id) }, id])
]

// PageContent
const container = (title, ...body) =>
  [
    'header', {}, nav(),
    ['main', {}, ['h1', {}, title], ...body]
  ]

const home = () =>
  container(
    'Home',
    [
      'div', {},
      // component behavior
      // expected
      ['p', {}, pop('x1', 'expected behavior')],
      // unexpected
      ['p', {}, [pop, 'x2', 'unexpected behavior']]
    ]
  )

const about = () => container('About', ['section', {}, 'About us'])
const profile = () => container('Profile', ['section', {}, 'Profile'])

router.addListener(EVENT_ROUTE_CHANGED, ({ value }) => db.resetIn('route', value))
router.start()

$compile(
  $switch(
    fromView(db, { path: ['route'] }),
    ({ id }) => id,
    { home, profile, about }
  )).mount(document.getElementById('app'))

I hope for your help

postspectacular commented 3 weeks ago

You know, it'd be really good of you in the future to please actually describe what is the unexpected behavior you're observing. You keep on having me guess and spend a lot of time trying to figure out which parts are unexpected — it's not really helpful!

From what I could figure, it seems in some circumstances components containing the [fn, arg...] form don't seem to properly unmount and hence when switching to another route and then back, the earlier non-cleared reactive fromView() subscription is still active, plus a new one is being created and so on...

About these embedded function forms, in general:

  1. Theyse forms are a legacy feature from the older thi.ng/hdom approach and are actually not that useful at all with rdom. I.e there's no real benefit of using [fn, arg1, arg2] vs calling fn(arg1, arg2) directly in rdom (or it can already be handled via other means).
  2. Because of the previous point, I've actually been tempted for a while to completely remove support for these forms in a future version of rdom. Their handling adds unnecessary complexity with no real gain (and obviously some edge cases are still not 100% right anyway)

So two more questions for you:

Can you please explain WHY you're intending to use these forms? WHAT is your specific need of using this embedded form compared to using normal function calls?

Thanks

bit-app-3000 commented 3 weeks ago

You know, it'd be really good of you in the future to please actually describe what is the unexpected behavior you're observing. You keep on having me guess and spend a lot of time trying to figure out which parts are unexpected — it's not really helpful!

From what I could figure, it seems in some circumstances components containing the [fn, arg...] form don't seem to properly unmount and hence when switching to another route and then back, the earlier non-cleared reactive fromView() subscription is still active, plus a new one is being created and so on...

About these embedded function forms, in general:

  1. Theyse forms are a legacy feature from the older thi.ng/hdom approach and are actually not that useful at all with rdom. I.e there's no real benefit of using [fn, arg1, arg2] vs calling fn(arg1, arg2) directly in rdom (or it can already be handled via other means).
  2. Because of the previous point, I've actually been tempted for a while to completely remove support for these forms in a future version of rdom. Their handling adds unnecessary complexity with no real gain (and obviously some edge cases are still not 100% right anyway)

So two more questions for you:

Can you please explain WHY you're intending to use these forms? WHAT is your specific need of using this embedded form compared to using normal function calls?

Thanks

@postspectacular plan to use in declarative design system components

// example.json

[
  "page", {},
  ["HeaderLayout"],
  "main", {},
  [
    "section", {},
    [
      "bar", {},
      [
        "PopOver", {
        "id": "x1",
        "state": "show",
        "placement": "top-end",
        "layout": ["Tooltip", "contentId"]
      }
      ],
      [
        "PopOver", {
        "id": "x2",
        "placement": "top",
        "layout": "Description"
      }
      ],
      [
        "PopOver", {
        "id": "x3",
        "placement": "top",
        "layout": ["h1", {}, "Tooltip"]
      }
      ],
    ]
  ],
  ["FooterLayout"]
]

Lower Case: html tag Camel Case: component definition ( Only first array element )

postspectacular commented 3 weeks ago

@bit-app-3000 that's very helpful to learn & a great use case — thank you!

I've updated the $compile() function to add checks for embedded function forms, call the function and then only compile the result... I'm doing some more testing and then release asap (your example above has no more problems now, as far as i can tell!)

bit-app-3000 commented 3 weeks ago

thx works as it should!

Regards