@wrnrlr/prelude 0.0.1 → 0.1.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.
- package/.github/workflows/publish.yml +46 -5
- package/deno.json +18 -9
- package/package.json +4 -6
- package/readme.md +163 -41
- package/src/constants.ts +2 -1
- package/src/{controlflow.js → controlflow.ts} +63 -50
- package/src/hyperscript.ts +7 -6
- package/src/mod.ts +19 -17
- package/src/reactive.ts +42 -14
- package/src/resource.js +184 -0
- package/src/router.js +65 -0
- package/src/runtime.ts +9 -8
- package/test/hyperscript.js +2 -2
- package/test/reactive.js +12 -4
- package/www/assets/css/presets.css +504 -0
- package/www/assets/css/style.css +90 -0
- package/www/demo.html +15 -0
- package/www/index.html +211 -0
- package/www/playground.html +183 -0
- package/www/public/example/admin.html +88 -0
- package/{example → www/public/example}/counter.html +1 -1
- package/{example → www/public/example}/greeting.html +1 -1
- package/{example → www/public/example}/show.html +1 -1
- package/{example → www/public/example}/todo.html +1 -1
- package/www/public/fonts/fab.ttf +0 -0
- package/www/public/fonts/fab.woff2 +0 -0
- package/www/public/fonts/far.ttf +0 -0
- package/www/public/fonts/far.woff2 +0 -0
- package/www/public/fonts/fas.ttf +0 -0
- package/www/public/fonts/fas.woff2 +0 -0
- package/www/public/logo.svg +16 -0
- package/www/typedoc.json +13 -0
- package/www/vite.config.js +106 -0
- package/example/paint.html +0 -22
- package/index.html +0 -230
- package/presets.css +0 -284
- package/public/logo.svg +0 -5
- package/test/runtime.js +0 -7
- package/typedoc.jsonc +0 -31
- /package/{public → www/public}/banner.svg +0 -0
    
        package/src/reactive.ts
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            // @ts-nocheck:
         | 
| 2 2 | 
             
            export interface EffectOptions {
         | 
| 3 3 | 
             
              name?: string;
         | 
| 4 4 | 
             
            }
         | 
| @@ -145,7 +145,7 @@ abstract class Observer { | |
| 145 145 |  | 
| 146 146 | 
             
            class Root extends Observer {
         | 
| 147 147 | 
             
              wrap<T>(fn: RootFn<T>): T {
         | 
| 148 | 
            -
                return  | 
| 148 | 
            +
                return observe(() => fn(this.dispose), this, false)!
         | 
| 149 149 | 
             
              }
         | 
| 150 150 | 
             
            }
         | 
| 151 151 |  | 
| @@ -165,7 +165,7 @@ class Computation<T = unknown> extends Observer { | |
| 165 165 | 
             
              private run = (): T => {
         | 
| 166 166 | 
             
                this.dispose()
         | 
| 167 167 | 
             
                this.parent?.observers.add(this)
         | 
| 168 | 
            -
                return  | 
| 168 | 
            +
                return observe(this.fn, this, true)!
         | 
| 169 169 | 
             
              }
         | 
| 170 170 |  | 
| 171 171 | 
             
              update = (): void => {
         | 
| @@ -279,21 +279,52 @@ export function useContext<T>(context: Context<T>): T { | |
| 279 279 | 
             
              return context.get()
         | 
| 280 280 | 
             
            }
         | 
| 281 281 |  | 
| 282 | 
            +
            export type S<T> = Getter<T> | Setter<T>
         | 
| 283 | 
            +
             | 
| 284 | 
            +
            /**
         | 
| 285 | 
            +
             | 
| 286 | 
            +
            @param s Signal
         | 
| 287 | 
            +
            @param k
         | 
| 288 | 
            +
            */
         | 
| 289 | 
            +
            export function wrap<T>(s:S<Array<T>>, k:number|(()=>number)): S<T>
         | 
| 290 | 
            +
            export function wrap<T>(s:S<Record<string,T>>, k:string|(()=>string)): S<T>
         | 
| 291 | 
            +
            export function wrap<T>(s:S<Array<T>>|S<Record<string,T>>, k:number|string|(()=>number)|(()=>string)): S<T> {
         | 
| 292 | 
            +
              const t = typeof k
         | 
| 293 | 
            +
              if (t === 'number') {
         | 
| 294 | 
            +
                return ((...a:T[]) => {
         | 
| 295 | 
            +
                  const b = (s as Getter<Array<T>>)()
         | 
| 296 | 
            +
                  return (a.length) ? (s as Setter<Array<T>>)((b as any).toSpliced(k as number, 1, a[0])).at(k as number) : b.at(k as number)
         | 
| 297 | 
            +
                }) as S<T>
         | 
| 298 | 
            +
              } else if (t === 'string') {
         | 
| 299 | 
            +
                return ((...a:T[]) => {
         | 
| 300 | 
            +
                  const b = (s as Getter<Record<string,T>>)()
         | 
| 301 | 
            +
                  return (a.length) ? (s as Setter<Record<string,T>>)({...b, [k as string]:a[0]})[k as string] : b[k as string]
         | 
| 302 | 
            +
                }) as S<T>
         | 
| 303 | 
            +
              } else if (t === 'function')
         | 
| 304 | 
            +
                return ((...a:T[]) => {
         | 
| 305 | 
            +
                  const i = (k as ()=>string|number)(), c = typeof i
         | 
| 306 | 
            +
                  if (c==='number') return a.length ? (s as Setter<Array<T>>)((old:any) => old.toSpliced(i, 1, a[0]))[i as number] : (s as Getter<Array<T>>)()[i as number]
         | 
| 307 | 
            +
                    else if (c === 'string') return a.length ? (s as Setter<Record<string,T>>)((b) => ({...b, [i]:a[0]}))[i as string] : (s as Getter<Record<string,T>>)()[i]
         | 
| 308 | 
            +
                  throw new Error('Cannot wrap signal')
         | 
| 309 | 
            +
                }) as S<T>
         | 
| 310 | 
            +
              throw new Error('Cannot wrap signal')
         | 
| 311 | 
            +
            }
         | 
| 312 | 
            +
             | 
| 282 313 | 
             
            export function getOwner(): Observer | undefined {
         | 
| 283 314 | 
             
              return OBSERVER
         | 
| 284 315 | 
             
            }
         | 
| 285 316 |  | 
| 286 317 | 
             
            export function runWithOwner<T>(observer: Observer|undefined, fn: ()=>T):T {
         | 
| 287 318 | 
             
              const tracking = observer instanceof Computation
         | 
| 288 | 
            -
              return  | 
| 319 | 
            +
              return observe(fn, observer, tracking)!
         | 
| 289 320 | 
             
            }
         | 
| 290 321 |  | 
| 291 322 | 
             
            /**
         | 
| 292 | 
            -
             Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link  | 
| 323 | 
            +
             Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link untrack}.
         | 
| 293 324 | 
             
             @group Reactive Primitive
         | 
| 294 325 | 
             
             */
         | 
| 295 326 | 
             
            export function onMount(fn: () => void) {
         | 
| 296 | 
            -
              effect(() =>  | 
| 327 | 
            +
              effect(() => untrack(fn));
         | 
| 297 328 | 
             
            }
         | 
| 298 329 |  | 
| 299 330 | 
             
            export function onCleanup(fn: Fn):void {
         | 
| @@ -327,20 +358,17 @@ export function batch<T>(fn: Fn<T>):T { | |
| 327 358 | 
             
            }
         | 
| 328 359 |  | 
| 329 360 | 
             
            /**
         | 
| 361 | 
            +
            Get the value of a signal without subscribing to future updates.
         | 
| 330 362 |  | 
| 331 363 | 
             
            @param fn
         | 
| 364 | 
            +
            @returns value returned from `fn`
         | 
| 332 365 | 
             
            @group Reactive Primitive
         | 
| 333 366 | 
             
            */
         | 
| 334 | 
            -
            export function  | 
| 335 | 
            -
              return  | 
| 367 | 
            +
            export function untrack<T>(fn: ()=>T):T {
         | 
| 368 | 
            +
              return observe(fn, OBSERVER, false)!
         | 
| 336 369 | 
             
            }
         | 
| 337 370 |  | 
| 338 | 
            -
             | 
| 339 | 
            -
             | 
| 340 | 
            -
             @param fn
         | 
| 341 | 
            -
             @group Reactive Primitive
         | 
| 342 | 
            -
             */
         | 
| 343 | 
            -
            function wrap<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
         | 
| 371 | 
            +
            function observe<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
         | 
| 344 372 | 
             
              const OBSERVER_PREV = OBSERVER;
         | 
| 345 373 | 
             
              const TRACKING_PREV = TRACKING;
         | 
| 346 374 | 
             
              OBSERVER = observer;
         | 
    
        package/src/resource.js
    ADDED
    
    | @@ -0,0 +1,184 @@ | |
| 1 | 
            +
            // @ts-nocheck:
         | 
| 2 | 
            +
            import {signal,effect,untrack,memo,batch,useContext,onCleanup} from './reactive.ts'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            const NO_INIT = {}
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            /**
         | 
| 7 | 
            +
            resource
         | 
| 8 | 
            +
            */
         | 
| 9 | 
            +
            export function resource(pSource,pFetcher,pOptions) {
         | 
| 10 | 
            +
              let source
         | 
| 11 | 
            +
              let fetcher
         | 
| 12 | 
            +
              let options
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              if ((arguments.length === 2 && typeof pFetcher === 'object') || arguments.length === 1) {
         | 
| 15 | 
            +
                source = true
         | 
| 16 | 
            +
                fetcher = pSource
         | 
| 17 | 
            +
                options = (pFetcher || {})
         | 
| 18 | 
            +
              } else {
         | 
| 19 | 
            +
                source = pSource
         | 
| 20 | 
            +
                fetcher = pFetcher
         | 
| 21 | 
            +
                options = pOptions || {}
         | 
| 22 | 
            +
              }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              let pr = null,
         | 
| 25 | 
            +
                initP = NO_INIT,
         | 
| 26 | 
            +
                scheduled = false,
         | 
| 27 | 
            +
                resolved = 'initialValue' in options
         | 
| 28 | 
            +
              const dynamic = source.call && memo(source)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              const contexts = new Set(),
         | 
| 31 | 
            +
                value = signal(options.initialValue),
         | 
| 32 | 
            +
                error = signal(undefined),
         | 
| 33 | 
            +
                track = signal(undefined, {equals: false}),
         | 
| 34 | 
            +
                state = signal(resolved ? 'ready' : 'unresolved')
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              function loadEnd(p, v, error, key) {
         | 
| 37 | 
            +
                if (pr === p) {
         | 
| 38 | 
            +
                  pr = null
         | 
| 39 | 
            +
                  key !== undefined && (resolved = true)
         | 
| 40 | 
            +
                  initP = NO_INIT
         | 
| 41 | 
            +
                  completeLoad(v, error)
         | 
| 42 | 
            +
                }
         | 
| 43 | 
            +
                return v
         | 
| 44 | 
            +
              }
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              function completeLoad(v, err) {
         | 
| 47 | 
            +
                batch(() => {
         | 
| 48 | 
            +
                  if (err === undefined) value(() => v)
         | 
| 49 | 
            +
                  state(err !== undefined ? 'errored' : resolved ? 'ready' : 'unresolved')
         | 
| 50 | 
            +
                  error(err)
         | 
| 51 | 
            +
                  for (const c of contexts.keys()) c.decrement()
         | 
| 52 | 
            +
                  contexts.clear()
         | 
| 53 | 
            +
                }, false)
         | 
| 54 | 
            +
              }
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              function read() {
         | 
| 57 | 
            +
                const v = value(),
         | 
| 58 | 
            +
                  err = error();
         | 
| 59 | 
            +
                if (err !== undefined && !pr) throw err;
         | 
| 60 | 
            +
                return v;
         | 
| 61 | 
            +
              }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              function load(refetching = true) {
         | 
| 64 | 
            +
                if (refetching !== false && scheduled) return;
         | 
| 65 | 
            +
                scheduled = false;
         | 
| 66 | 
            +
                const lookup = dynamic ? dynamic() : (source);
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                if (lookup == null || lookup === false) {
         | 
| 69 | 
            +
                  loadEnd(pr, untrack(value));
         | 
| 70 | 
            +
                  return;
         | 
| 71 | 
            +
                }
         | 
| 72 | 
            +
                const p = initP !== NO_INIT ? (initP)
         | 
| 73 | 
            +
                  : untrack(() => fetcher(lookup, {value: value(), refetching}))
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                if (!isPromise(p)) {
         | 
| 76 | 
            +
                  loadEnd(pr, p, undefined, lookup);
         | 
| 77 | 
            +
                  return p
         | 
| 78 | 
            +
                }
         | 
| 79 | 
            +
                pr = p;
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                if ('value' in p) {
         | 
| 82 | 
            +
                  if ((p).status === "success") loadEnd(pr, p.value, undefined, lookup);
         | 
| 83 | 
            +
                  else loadEnd(pr, undefined, undefined, lookup);
         | 
| 84 | 
            +
                  return p;
         | 
| 85 | 
            +
                }
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                scheduled = true
         | 
| 88 | 
            +
                queueMicrotask(() => (scheduled = false));
         | 
| 89 | 
            +
                batch(() => {
         | 
| 90 | 
            +
                  state(resolved ? 'refreshing' : 'pending')
         | 
| 91 | 
            +
                  track()
         | 
| 92 | 
            +
                }, false);
         | 
| 93 | 
            +
                return p.then(
         | 
| 94 | 
            +
                  v => loadEnd(p, v, undefined, lookup),
         | 
| 95 | 
            +
                  e => loadEnd(p, undefined, castError(e), lookup)
         | 
| 96 | 
            +
                );
         | 
| 97 | 
            +
              }
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              Object.defineProperties(read, {
         | 
| 100 | 
            +
                state: { get: () => state() },
         | 
| 101 | 
            +
                error: { get: () => error() },
         | 
| 102 | 
            +
                loading: {
         | 
| 103 | 
            +
                  get() {
         | 
| 104 | 
            +
                    const s = state();
         | 
| 105 | 
            +
                    return s === 'pending' || s === 'refreshing';
         | 
| 106 | 
            +
                  }
         | 
| 107 | 
            +
                },
         | 
| 108 | 
            +
                latest: {
         | 
| 109 | 
            +
                  get() {
         | 
| 110 | 
            +
                    if (!resolved) return read();
         | 
| 111 | 
            +
                    const err = error();
         | 
| 112 | 
            +
                    if (err && !pr) throw err;
         | 
| 113 | 
            +
                    return value();
         | 
| 114 | 
            +
                  }
         | 
| 115 | 
            +
                }
         | 
| 116 | 
            +
              })
         | 
| 117 | 
            +
             | 
| 118 | 
            +
              if (dynamic) effect(() => load(false));
         | 
| 119 | 
            +
              else load(false);
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              return read
         | 
| 122 | 
            +
            }
         | 
| 123 | 
            +
             | 
| 124 | 
            +
             | 
| 125 | 
            +
             function isPromise(v) {
         | 
| 126 | 
            +
               return v && typeof v === 'object' && 'then' in v
         | 
| 127 | 
            +
             }
         | 
| 128 | 
            +
             | 
| 129 | 
            +
             /**
         | 
| 130 | 
            +
            Creates and handles an AbortSignal**
         | 
| 131 | 
            +
            ```ts
         | 
| 132 | 
            +
            const [signal, abort, filterAbortError] =  makeAbortable({ timeout: 10000 });
         | 
| 133 | 
            +
            const fetcher = (url) => fetch(url, {signal:signal()}).catch(filterAbortError); // filters abort errors
         | 
| 134 | 
            +
            ```
         | 
| 135 | 
            +
            Returns an accessor for the signal and the abort callback.
         | 
| 136 | 
            +
             | 
| 137 | 
            +
            Options are optional and include:
         | 
| 138 | 
            +
            - `timeout`: time in Milliseconds after which the fetcher aborts automatically
         | 
| 139 | 
            +
            - `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
         | 
| 140 | 
            +
            */
         | 
| 141 | 
            +
             export function makeAbortable(options) {
         | 
| 142 | 
            +
               let controller, timeout
         | 
| 143 | 
            +
               const abort = (reason) => {
         | 
| 144 | 
            +
                 timeout && clearTimeout(timeout);
         | 
| 145 | 
            +
                 controller?.abort(reason);
         | 
| 146 | 
            +
               }
         | 
| 147 | 
            +
               const signal = () => {
         | 
| 148 | 
            +
                 if (!options.noAutoAbort && controller?.signal.aborted === false) abort("retry");
         | 
| 149 | 
            +
                 controller = new AbortController();
         | 
| 150 | 
            +
                 if (options.timeout) timeout = setTimeout(() => abort("timeout"), options.timeout)
         | 
| 151 | 
            +
                 return controller.signal;
         | 
| 152 | 
            +
               }
         | 
| 153 | 
            +
               const error = err => {
         | 
| 154 | 
            +
                 if (err.name === "AbortError") return undefined
         | 
| 155 | 
            +
                 throw err;
         | 
| 156 | 
            +
               }
         | 
| 157 | 
            +
               return [signal, abort, error]
         | 
| 158 | 
            +
             }
         | 
| 159 | 
            +
             | 
| 160 | 
            +
             /**
         | 
| 161 | 
            +
            Creates and handles an AbortSignal with automated cleanup
         | 
| 162 | 
            +
            ```ts
         | 
| 163 | 
            +
            const [signal, abort, filterAbortError] =
         | 
| 164 | 
            +
              createAbortable();
         | 
| 165 | 
            +
            const fetcher = (url) => fetch(url, { signal: signal() })
         | 
| 166 | 
            +
              .catch(filterAbortError); // filters abort errors
         | 
| 167 | 
            +
            ```
         | 
| 168 | 
            +
            Returns an accessor for the signal and the abort callback.
         | 
| 169 | 
            +
             | 
| 170 | 
            +
            Options are optional and include:
         | 
| 171 | 
            +
            - `timeout`: time in Milliseconds after which the fetcher aborts automatically
         | 
| 172 | 
            +
            - `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
         | 
| 173 | 
            +
            */
         | 
| 174 | 
            +
             | 
| 175 | 
            +
             export function abortable(options) {
         | 
| 176 | 
            +
               const [signal, abort, filterAbortError] = makeAbortable(options);
         | 
| 177 | 
            +
               onCleanup(abort);
         | 
| 178 | 
            +
               return [signal, abort, filterAbortError];
         | 
| 179 | 
            +
             }
         | 
| 180 | 
            +
             | 
| 181 | 
            +
             function castError(err) {
         | 
| 182 | 
            +
               if (err instanceof Error) return err;
         | 
| 183 | 
            +
               return new Error(typeof err === "string" ? err : "Unknown error", { cause: err });
         | 
| 184 | 
            +
             }
         | 
    
        package/src/router.js
    ADDED
    
    | @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            // @ts-nocheck
         | 
| 2 | 
            +
            import {signal,effect,batch,untrack,memo,wrap,context,useContext,onMount} from './reactive.ts'
         | 
| 3 | 
            +
            // import {h} from './hyperscript.ts'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            const Ctx = context()
         | 
| 6 | 
            +
            const useRouter = () => useContext(Ctx)
         | 
| 7 | 
            +
            export const useNavigate = () => wrap(useRouter(),'navigate')
         | 
| 8 | 
            +
            export const useParams = () => wrap(useRouter(),'params')
         | 
| 9 | 
            +
            export const useSearch = () => wrap(useRouter(),'search')
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            export function Router(props) {
         | 
| 12 | 
            +
              const routes = props.children
         | 
| 13 | 
            +
              const NotFound = ()=>'Not found'
         | 
| 14 | 
            +
              const render = () => {console.log('render')}
         | 
| 15 | 
            +
              // const loaded = signal(false)
         | 
| 16 | 
            +
              const navigate = signal(null)
         | 
| 17 | 
            +
              const params = signal(null)
         | 
| 18 | 
            +
              const search = signal(null)
         | 
| 19 | 
            +
              onMount(()=>{
         | 
| 20 | 
            +
                window.addEventListener("popstate", (event) => {
         | 
| 21 | 
            +
                  batch(()=>{
         | 
| 22 | 
            +
                    const hash = parseHash(document.location.hash)
         | 
| 23 | 
            +
                    navigate(hash)
         | 
| 24 | 
            +
                  })
         | 
| 25 | 
            +
                })
         | 
| 26 | 
            +
                const hash = parseHash(document.location.hash)
         | 
| 27 | 
            +
                navigate(hash)
         | 
| 28 | 
            +
              })
         | 
| 29 | 
            +
              const children = memo(() => {
         | 
| 30 | 
            +
                let location = navigate()
         | 
| 31 | 
            +
                const route = routes.find(r=>r.path===location.pathname)
         | 
| 32 | 
            +
                return route?.component() || NotFound
         | 
| 33 | 
            +
              })
         | 
| 34 | 
            +
              return Ctx({navigate,params,search,children:()=>children})
         | 
| 35 | 
            +
            }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            function parseHash(s) {
         | 
| 38 | 
            +
              const res = {pathname:'/'}
         | 
| 39 | 
            +
              if (s[0]!=='#') return res
         | 
| 40 | 
            +
              s = s.substr(1)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              let i = s.indexOf('?')
         | 
| 43 | 
            +
              if (i===-1) i = s.length
         | 
| 44 | 
            +
              res.pathname += s.substr(0,i)
         | 
| 45 | 
            +
              if (res.pathname==='') res.pathname = '/'
         | 
| 46 | 
            +
              return res
         | 
| 47 | 
            +
            }
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            function parsePathname(s) {
         | 
| 50 | 
            +
              let i = s.indexOf('?', 0)
         | 
| 51 | 
            +
              if (i===-1) i = s.length
         | 
| 52 | 
            +
              return s.substr(0,i)
         | 
| 53 | 
            +
            }
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            function parseSearch(s) {
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            export function Route(props) {
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            export function A(props) {
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            }
         | 
    
        package/src/runtime.ts
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            // @ts-nocheck:
         | 
| 2 | 
            +
            import {effect,untrack,root} from './reactive.ts'
         | 
| 2 3 | 
             
            import {SVGNamespace,SVGElements,ChildProperties,getPropAlias,Properties,Aliases,DelegatedEvents} from './constants.ts'
         | 
| 3 4 | 
             
            import type {Window,Mountable,Elem,Node} from './constants.ts'
         | 
| 4 5 |  | 
| @@ -22,7 +23,7 @@ export type Runtime = { | |
| 22 23 | 
             
            }
         | 
| 23 24 |  | 
| 24 25 | 
             
            /**
         | 
| 25 | 
            -
             | 
| 26 | 
            +
            Create `Runtime` for `window`
         | 
| 26 27 | 
             
            @param window
         | 
| 27 28 | 
             
            @group Internal
         | 
| 28 29 | 
             
            */
         | 
| @@ -54,7 +55,7 @@ export function runtime(window:Window):Runtime { | |
| 54 55 | 
             
              function spread(node:Elem, props:any = {}, skipChildren:boolean) {
         | 
| 55 56 | 
             
                const prevProps:any = {}
         | 
| 56 57 | 
             
                if (!skipChildren) effect(() => (prevProps.children = insertExpression(node, props.children, prevProps.children)))
         | 
| 57 | 
            -
                effect(() => (props.ref?.call ?  | 
| 58 | 
            +
                effect(() => (props.ref?.call ? untrack(() => props.ref(node)) : (props.ref = node)))
         | 
| 58 59 | 
             
                effect(() => assign(node, props, true, prevProps, true))
         | 
| 59 60 | 
             
                return prevProps
         | 
| 60 61 | 
             
              }
         | 
| @@ -344,10 +345,10 @@ function style(node:Node, value:any, prev:any) { | |
| 344 345 | 
             
            }
         | 
| 345 346 |  | 
| 346 347 | 
             
            function toPropertyName(name:string):string {
         | 
| 347 | 
            -
              return name.toLowerCase().replace(/-([a-z])/g, (_: | 
| 348 | 
            +
              return name.toLowerCase().replace(/-([a-z])/g, (_:unknown, w:string) => w.toUpperCase());
         | 
| 348 349 | 
             
            }
         | 
| 349 350 |  | 
| 350 | 
            -
            function toggleClassKey(node: | 
| 351 | 
            +
            function toggleClassKey(node:Node, key:string, value:boolean) {
         | 
| 351 352 | 
             
              const classNames = key.trim().split(/\s+/)
         | 
| 352 353 | 
             
              for (let i = 0, nameLen = classNames.length; i < nameLen; i++)
         | 
| 353 354 | 
             
                node.classList.toggle(classNames[i], value)
         | 
| @@ -360,13 +361,13 @@ function appendNodes(parent:Node, array:Node[], marker:null|Node = null) { | |
| 360 361 |  | 
| 361 362 | 
             
            // Slightly modified version of: https://github.com/WebReflection/udomdiff/blob/master/index.js
         | 
| 362 363 | 
             
            function reconcileArrays(parentNode:Node, a:Node[], b:Node[]) {
         | 
| 363 | 
            -
               | 
| 364 | 
            -
             | 
| 364 | 
            +
              const bLength = b.length
         | 
| 365 | 
            +
              let aEnd = a.length,
         | 
| 365 366 | 
             
                bEnd = bLength,
         | 
| 366 367 | 
             
                aStart = 0,
         | 
| 367 368 | 
             
                bStart = 0,
         | 
| 368 | 
            -
                after = a[aEnd - 1].nextSibling,
         | 
| 369 369 | 
             
                map:Map<Node,number>|null = null;
         | 
| 370 | 
            +
              const after = a[aEnd - 1].nextSibling
         | 
| 370 371 |  | 
| 371 372 | 
             
              while (aStart < aEnd || bStart < bEnd) {
         | 
| 372 373 | 
             
                // common prefix
         | 
    
        package/test/hyperscript.js
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            import {runtime} from '../src/runtime.ts'
         | 
| 2 2 | 
             
            import {hyperscript} from '../src/hyperscript.ts'
         | 
| 3 3 | 
             
            import {signal,root} from '../src/reactive.ts'
         | 
| 4 | 
            -
            import {JSDOM} from ' | 
| 4 | 
            +
            import {JSDOM} from 'jsdom'
         | 
| 5 5 | 
             
            import {assertEquals} from '@std/assert'
         | 
| 6 6 |  | 
| 7 7 | 
             
            const {window} = new JSDOM('<!DOCTYPE html>', {runScripts:'dangerously'})
         | 
| @@ -36,7 +36,7 @@ testing('h with basic element', {skip:true}, async test => { | |
| 36 36 | 
             
              await test("number content", () => assertHTML(h('i',1), '<i>1</i>'))
         | 
| 37 37 | 
             
              await test("bigint content", () => assertHTML(h('i',2n), '<i>2</i>'))
         | 
| 38 38 | 
             
              await test("symbol content", () => assertHTML(h('i',Symbol('A')), '<i>Symbol(A)</i>'))
         | 
| 39 | 
            -
              await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
         | 
| 39 | 
            +
              // await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
         | 
| 40 40 | 
             
              await test("signal content", () => assertHTML(h('i',()=>1), '<i>1</i>'))
         | 
| 41 41 | 
             
              await test('array content', () => assertHTML(h('i',['A',1,2n]), '<i>A12</i>'))
         | 
| 42 42 | 
             
              await test('style attribute', () => assertHTML(h('hr',{style:'color:red'}), '<hr style="color: red;">'))
         | 
    
        package/test/reactive.js
    CHANGED
    
    | @@ -1,8 +1,7 @@ | |
| 1 1 | 
             
            import {assertEquals,assert} from '@std/assert'
         | 
| 2 2 | 
             
            import {describe,it} from '@std/testing/bdd'
         | 
| 3 3 |  | 
| 4 | 
            -
            import {signal,effect, | 
| 5 | 
            -
            import {wrap} from '../src/controlflow.js'
         | 
| 4 | 
            +
            import {signal,effect,untrack,batch,memo,context,useContext,root,wrap} from '../src/reactive.ts'
         | 
| 6 5 |  | 
| 7 6 | 
             
            describe('signal', () => {
         | 
| 8 7 | 
             
              const a = signal(1)
         | 
| @@ -14,6 +13,15 @@ describe('signal', () => { | |
| 14 13 | 
             
              assertEquals(a(null),null)
         | 
| 15 14 | 
             
            })
         | 
| 16 15 |  | 
| 16 | 
            +
            describe('signal with equals option', () => {
         | 
| 17 | 
            +
              const n = signal(0,{equals:false})
         | 
| 18 | 
            +
              let m = 0
         | 
| 19 | 
            +
              effect(() => m += 1 + n() )
         | 
| 20 | 
            +
              assertEquals(m,1)
         | 
| 21 | 
            +
              assertEquals(n(0),0)
         | 
| 22 | 
            +
              assertEquals(m,2)
         | 
| 23 | 
            +
            })
         | 
| 24 | 
            +
             | 
| 17 25 | 
             
            describe('effect', () => {
         | 
| 18 26 | 
             
              const n = signal(1)
         | 
| 19 27 | 
             
              let m = 0
         | 
| @@ -24,10 +32,10 @@ describe('effect', () => { | |
| 24 32 | 
             
              assertEquals(m,3)
         | 
| 25 33 | 
             
            })
         | 
| 26 34 |  | 
| 27 | 
            -
            describe(' | 
| 35 | 
            +
            describe('untrack',()=>{
         | 
| 28 36 | 
             
              const n = signal(1)
         | 
| 29 37 | 
             
              let m = 0
         | 
| 30 | 
            -
              effect(()=>m =  | 
| 38 | 
            +
              effect(()=>m = untrack(n))
         | 
| 31 39 | 
             
              assertEquals(m,1)
         | 
| 32 40 | 
             
              n(2)
         | 
| 33 41 | 
             
              assertEquals(m,1)
         |