DrSensor / nusa

incremental runtime that bring both simplicity and power into webdev (buildless, cross-language, data-driven)
MIT License
4 stars 0 forks source link

Procedural UI (Island/Widget/SPA) #32

Open DrSensor opened 1 year ago

DrSensor commented 1 year ago

Ergonomic Island

single accessor(value)

Behind the scene, accessor(value) is just a syntactic sugar for new Accessor(value) where

const defineAccessor = () => @allocate(Infinity) class {
  accessor value
  constructor(value) { this.value = value }
  valueOf() { return this.value }
}

which the Accessor class is unique per scopes of Component

let prevC1_a, currentC2_a
function C1() {
  const a = accessor()
  const b = accessor()

  // each variables are differ from others
  assert(a.prototype !== b.prototype)

  // but each same variables is constructed by the same class
  if (prevC1_a) assert(
    prevC1_a.prototype === a.prototype
  )
  prevC1_a = a

  // however, it will be different if it came from different scopes although it's in the same order
  if (currentC2_a) assert(
    currentC2_a.prototype !== a.prototype
  )
}

function C2() {
  const a = accessor()
  const b = accessor()
  currentC2_a = a
}

Warning: so yeah. The accessor must always be on top and in the same order. Not dynamically created in if, in for, in ternary operator, created after await, created inside callback, etc. Think of it like super() constructor.

example

const total = accessor(0) // non-iterate()able, behave like signal()

function Button({ value } = {}) {
  const count = accessor(value ?? 0) // iterate()able

  // increment all <Button> every seconds
  runOnce(() => setInterval(() =>
    iterate(count, (at, i, len) => {
      if (+total < 200) {
        at[i]++
        total++
      }
    })
  ), 1e3))

  total.value += count
  const increment = () => 
    total.value += count.value += 1

  return <button on:click={increment}>
    count: {count}
  </button>
}

function Island() {
  return <>
    total: {total}
    {Array.from(
      { length: 100 },
      (i) => <Button value={i} />,
    )}
  </>
}

export default class {
  @build(Island)
  host // <render-scope :=host>
}

async function and generator

Inspired from Crank and Tonic

In async generator, each yield and return will replace previous yield. However, normal generator function will behave the same way as Array of \<Component>.

async function IPAddress() {
  const res = await fetch("https://api.ipify.org")
  const address = await res.text()
  return <span class="rainbow">{address}</span>
}

async function *Island() {
  yield <span class="spinner">Loading...</span>

  const addr = await fetch("https://api.ipify.org").then(as => as.text())
  yield <span class="red">Your IP {addr}</span>

  yield <span class="green">
    {await fetch("https://api.ipify.org")
      .then(as => as.text())} is
  </span>

  // render/print same 100 IP address 😂
  return await Promise.all(Array.from(
    { length: 100 },
    () => <IPAddress />,
  ))
}

export default class {
  @build(IPAdress)
  ip // <slot :=ip>Loading...</slot>

  @build(Island)
  stressIP // <div :=stressIP />
}

Behind the scene, async function is same as async generator where it yield new Comment(function.name) before the function is awaiting.

Basically, that \<IPAdress> is same as:

async function *IPAddress() {
  yield new Comment("IPAddress")
  const res = await fetch("https://api.ipify.org")
  const address = await res.text()
  return <span class="rainbow">{address}</span>
}

exposing local data when creating children

Expose local variable for some operation similar on how Svelte directive let:variable being used.

function ColorManager({ children }) {
  const colors = ['red', 'green', 'blue']
  if (!("colors" in this)) this.colors = colors
  return <>
    ...
    children
  </>
}

<ColorManager>{({ colors }) =>
  <ul>{
    colors.map(color =>
      <li>{color}</li>
    )
  }</ul>
}</ColorManager>;

non-JSX DOM builder/factory

Although this feature can be replaced by integrating with other SPA framework, most of them rely on compiler/transformer so there is no harm to experiment new approach.

Hint: check my previous prototype in different repo

Also, always think if certain approach is performance. Think about if JS engine:

  • Cause deopt like switching from IC to MapRecord
  • Can opt like auto inline and dce
REJECTED ```ts type Children = Array< | Element | Primitive | Text > interface HTML { [key: string]: ( $1?: | Record> & { $children: | Children | Primitive | Text } | Children | Primitive | Text | ($: SelfElement) => void, $2?: | Children // if $1 not Array | ($: SelfElement) => void // if $1 not function ) => HTMLElement } export const html = new Proxy, svg = new Proxy function text( value: Primitive | Signal ): Text function text( str: string[], val: Primitive | Signal, ): Text export function text( $1: (Primitive | Signal) | string[], $2?: Primitive | Signal, ): Text { } export let on = {} export function attrs( $: Record> ) { } export function props( $: Record ) { } ``` ```js import { defer, use } from "wiles/std" import { html, svg, text, on } from "wiles/dom/fun-runtime" const { div, button, h3 } = html function Component() { const [c, c_] = use(Counter) function stopAt10 (e) { if (c.count++ === 10) msg([h3(10)]) // auto unbind c.count btn(e.target.type = "submit") //on.click = null // specific abort listener of e.target this.abort$() // self abort stopAt10 } } let msg defer(() => assert(msg() instanceof HTMLDivElement)) return div([//
"click to count", btn = button({ value: c_.count }, ($) => {// msg = div(text`count ${c_.count}`), ])//
} ``` > **Note**: something not allowed in micro/macro-task inside `el(?, $ => {/*here/*})` > * add event listener via `on.event = () => {}` > * append children via `html.el()` or `text()` > > but it still ok if doing thats inside `on.event` and `el(?, $ => {/*here/*})`