├── src ├── oby.ts ├── methods │ ├── SS.ts │ ├── batch.ts │ ├── store.ts │ ├── tick.ts │ ├── S.ts │ ├── is_store.ts │ ├── resolve.ts │ ├── untrack.ts │ ├── is_batching.ts │ ├── is_observable.ts │ ├── is_server.ts │ ├── wrap_element.ts │ ├── html.ts │ ├── render.ts │ ├── h.ts │ ├── create_context.ts │ ├── render_to_string.ts │ ├── index.ts │ ├── create_directive.ts │ ├── lazy.ts │ ├── create_element.ts │ └── hmr.ts ├── hooks │ ├── use_memo.ts │ ├── use_root.ts │ ├── use_effect.ts │ ├── use_boolean.ts │ ├── use_cleanup.ts │ ├── use_disposed.ts │ ├── use_readonly.ts │ ├── use_selector.ts │ ├── use_suspended.ts │ ├── use_suspense.ts │ ├── use_untracked.ts │ ├── use_abort_signal.ts │ ├── use_promise.ts │ ├── use_cheap_disposed.ts │ ├── use_animation_loop.ts │ ├── use_animation_frame.ts │ ├── use_interval.ts │ ├── use_render_effect.ts │ ├── use_timeout.ts │ ├── use_idle_loop.ts │ ├── use_idle_callback.ts │ ├── use_microtask.ts │ ├── use_fetch.ts │ ├── use_context.ts │ ├── use_abort_controller.ts │ ├── use_guarded.ts │ ├── use_scheduler.ts │ ├── index.ts │ └── use_resource.ts ├── components │ ├── fragment.ts │ ├── ternary.ts │ ├── error_boundary.ts │ ├── index.ts │ ├── dynamic.ts │ ├── suspense.ts │ ├── for.ts │ ├── if.ts │ ├── switch.ts │ ├── portal.ts │ ├── suspense.manager.ts │ ├── suspense.context.ts │ ├── suspense.collector.ts │ └── keep_alive.ts ├── singleton.ts ├── jsx │ └── runtime.ts ├── utils │ ├── creators.ts │ ├── classlist.ts │ ├── fragment.ts │ ├── lang.ts │ ├── htm.ts │ └── resolvers.ts ├── index.ts └── constants.ts ├── resources ├── discord │ ├── button.png │ └── button.afphoto ├── logo │ ├── logo.afdesign │ ├── png │ │ ├── logo.png │ │ ├── logo-dark.png │ │ ├── logo-light.png │ │ ├── logo-dark-rounded.png │ │ └── logo-light-rounded.png │ └── svg │ │ ├── logo.svg │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ ├── logo-dark-rounded.svg │ │ └── logo-light-rounded.svg ├── banner │ ├── banner.afdesign │ ├── png │ │ ├── banner.png │ │ ├── banner-dark.png │ │ ├── banner-light.png │ │ ├── banner-dark-rounded.png │ │ └── banner-light-rounded.png │ └── svg │ │ ├── banner.svg │ │ ├── banner-light.svg │ │ ├── banner-dark.svg │ │ └── banner-light-rounded.svg ├── collective │ ├── button.png │ └── button.afphoto └── playground │ ├── button.png │ └── button.afphoto ├── demo ├── ssr_esbuild │ ├── public │ │ ├── favicon.ico │ │ ├── css │ │ │ └── index.css │ │ ├── scss │ │ │ └── index.scss │ │ └── index.html │ ├── tsconfig.json │ ├── src │ │ ├── pages │ │ │ ├── home.tsx │ │ │ ├── 404.tsx │ │ │ ├── user.tsx │ │ │ ├── search.tsx │ │ │ ├── scrolling.tsx │ │ │ ├── loader.tsx │ │ │ └── counter.tsx │ │ ├── index.tsx │ │ └── app │ │ │ ├── index.tsx │ │ │ └── routes.tsx │ ├── package.json │ └── server │ │ └── index.tsx ├── boxes │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.css │ ├── vite.ts │ └── index.tsx ├── clock │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── vite.ts │ ├── index.css │ └── index.tsx ├── counter │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.tsx │ └── vite.ts ├── hmr │ ├── tsconfig.json │ ├── index.tsx │ ├── index.html │ ├── package.json │ ├── components │ │ ├── app.tsx │ │ ├── counter.tsx │ │ └── button.tsx │ └── vite.ts ├── html │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.tsx │ └── vite.ts ├── spiral │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── vite.ts │ ├── index.css │ └── index.tsx ├── uibench │ ├── tsconfig.json │ ├── package.json │ ├── index.html │ ├── vite.ts │ └── index.tsx ├── benchmark │ ├── tsconfig.json │ ├── package.json │ ├── index.html │ ├── vite.ts │ └── index.tsx ├── creation │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── vite.ts │ └── index.tsx ├── hyperscript │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.tsx │ └── vite.ts ├── playground │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.css │ └── vite.ts ├── triangle │ ├── tsconfig.json │ ├── package.json │ ├── index.html │ ├── index.css │ ├── vite.ts │ └── index.tsx ├── emoji_counter │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── vite.ts │ └── index.tsx ├── store_counter │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ ├── index.tsx │ └── vite.ts └── standalone │ └── index.html ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── license └── package.json /src/oby.ts: -------------------------------------------------------------------------------- 1 | 2 | /* EXPORT */ 3 | 4 | export * from 'oby'; 5 | -------------------------------------------------------------------------------- /resources/discord/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/discord/button.png -------------------------------------------------------------------------------- /resources/logo/logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/logo.afdesign -------------------------------------------------------------------------------- /resources/logo/png/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/png/logo.png -------------------------------------------------------------------------------- /resources/banner/banner.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/banner.afdesign -------------------------------------------------------------------------------- /resources/banner/png/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/png/banner.png -------------------------------------------------------------------------------- /resources/collective/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/collective/button.png -------------------------------------------------------------------------------- /resources/discord/button.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/discord/button.afphoto -------------------------------------------------------------------------------- /resources/logo/png/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/png/logo-dark.png -------------------------------------------------------------------------------- /resources/logo/png/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/png/logo-light.png -------------------------------------------------------------------------------- /resources/playground/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/playground/button.png -------------------------------------------------------------------------------- /demo/ssr_esbuild/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/demo/ssr_esbuild/public/favicon.ico -------------------------------------------------------------------------------- /resources/collective/button.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/collective/button.afphoto -------------------------------------------------------------------------------- /resources/playground/button.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/playground/button.afphoto -------------------------------------------------------------------------------- /resources/banner/png/banner-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/png/banner-dark.png -------------------------------------------------------------------------------- /resources/banner/png/banner-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/png/banner-light.png -------------------------------------------------------------------------------- /resources/logo/png/logo-dark-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/png/logo-dark-rounded.png -------------------------------------------------------------------------------- /src/methods/SS.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {get} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default get; 9 | -------------------------------------------------------------------------------- /resources/logo/png/logo-light-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/logo/png/logo-light-rounded.png -------------------------------------------------------------------------------- /src/hooks/use_memo.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {memo} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default memo; 9 | -------------------------------------------------------------------------------- /src/hooks/use_root.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {root} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default root; 9 | -------------------------------------------------------------------------------- /src/methods/batch.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {batch} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default batch; 9 | -------------------------------------------------------------------------------- /src/methods/store.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {store} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /src/methods/tick.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {tick} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default tick; 9 | -------------------------------------------------------------------------------- /resources/banner/png/banner-dark-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/png/banner-dark-rounded.png -------------------------------------------------------------------------------- /resources/banner/png/banner-light-rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vobyjs/voby/HEAD/resources/banner/png/banner-light-rounded.png -------------------------------------------------------------------------------- /src/hooks/use_effect.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {effect} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default effect; 9 | -------------------------------------------------------------------------------- /src/hooks/use_boolean.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {boolean} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default boolean; 9 | -------------------------------------------------------------------------------- /src/hooks/use_cleanup.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {cleanup} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default cleanup; 9 | -------------------------------------------------------------------------------- /src/methods/S.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {observable} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default observable; 9 | -------------------------------------------------------------------------------- /src/methods/is_store.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {isStore} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default isStore; 9 | -------------------------------------------------------------------------------- /src/methods/resolve.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {resolve} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default resolve; 9 | -------------------------------------------------------------------------------- /src/methods/untrack.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {untrack} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default untrack; 9 | -------------------------------------------------------------------------------- /src/hooks/use_disposed.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {disposed} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default disposed; 9 | -------------------------------------------------------------------------------- /src/hooks/use_readonly.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {readonly} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default readonly; 9 | -------------------------------------------------------------------------------- /src/hooks/use_selector.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {selector} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default selector; 9 | -------------------------------------------------------------------------------- /src/hooks/use_suspended.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {suspended} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default suspended; 9 | -------------------------------------------------------------------------------- /src/hooks/use_suspense.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {suspense} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default suspense; 9 | -------------------------------------------------------------------------------- /src/hooks/use_untracked.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {untracked} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default untracked; 9 | -------------------------------------------------------------------------------- /src/methods/is_batching.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {isBatching} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default isBatching; 9 | -------------------------------------------------------------------------------- /src/methods/is_observable.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {isObservable} from '~/oby'; 5 | 6 | /* EXPORT */ 7 | 8 | export default isObservable; 9 | -------------------------------------------------------------------------------- /demo/boxes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/clock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/hmr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/html/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/spiral/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/uibench/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/creation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/hyperscript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/triangle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/emoji_counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/store_counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "voby" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /demo/hmr/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {render} from 'voby'; 5 | import App from './components/app'; 6 | 7 | /* MAIN */ 8 | 9 | render ( , document.getElementById ( 'app' ) ); 10 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const PageHome = (): JSX.Element => { 5 | 6 | return

Home

; 7 | 8 | }; 9 | 10 | /* EXPORT */ 11 | 12 | export default PageHome; 13 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/public/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | h1 { 8 | text-decoration: underline; 9 | } 10 | 11 | main { 12 | padding: 0 20px 20px 20px; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.err 3 | *.log 4 | ._* 5 | .cache 6 | .fseventsd 7 | .DocumentRevisions* 8 | .DS_Store 9 | .TemporaryItems 10 | .Trashes 11 | Thumbs.db 12 | 13 | dist 14 | node_modules 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const Page404 = (): JSX.Element => { 5 | 6 | return

404 - Not found

; 7 | 8 | }; 9 | 10 | /* EXPORT */ 11 | 12 | export default Page404; 13 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/public/scss/index.scss: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | h1 { 9 | text-decoration: underline; 10 | } 11 | 12 | main { 13 | padding: 0 20px 20px 20px; 14 | } 15 | -------------------------------------------------------------------------------- /demo/hmr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HMR 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/creation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creation 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/hyperscript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HyperScript 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/emoji_counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Emoji Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import livereload from 'tiny-livereload/fetch'; 5 | import {render} from 'voby'; 6 | import App from './app'; 7 | 8 | /* MAIN */ 9 | 10 | livereload (); 11 | 12 | render ( , document.getElementById ( 'app' ) ); 13 | -------------------------------------------------------------------------------- /demo/store_counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Store Counter 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsex/tsconfig.json", 3 | "compilerOptions": { 4 | "isolatedModules": false, 5 | "noImplicitAny": false, 6 | "noPropertyAccessFromIndexSignature": false, 7 | "noUnusedParameters": false, 8 | "target": "es2020" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/fragment.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import type {Child} from '~/types'; 5 | 6 | /* MAIN */ 7 | 8 | const Fragment = ({ children }: { children: Child }): Child => { 9 | 10 | return children; 11 | 12 | }; 13 | 14 | /* EXPORT */ 15 | 16 | export default Fragment; 17 | -------------------------------------------------------------------------------- /demo/boxes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boxes 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/clock/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clock 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/clock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "clock", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "html", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/spiral/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spiral 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "counter", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/creation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "creation", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/spiral/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "spiral", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/triangle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "triangle", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/uibench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "uibench", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "benchmark", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/emoji_counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "emoji-counter", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/hyperscript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "hyperscript", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/store_counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "store-counter", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/triangle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sierpinski Triangle 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "playground", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev --port 3003", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SSR - Esbuild 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/boxes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "boxes", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "three": "0.111.0", 11 | "vite": "^5.2.7", 12 | "voby": "*" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/methods/is_server.ts: -------------------------------------------------------------------------------- 1 | 2 | /* HELPERS */ 3 | 4 | const IS_BROWSER = !!globalThis.CDATASection?.toString?.().match ( /^\s*function\s+CDATASection\s*\(\s*\)\s*\{\s*\[native code\]\s*\}\s*$/ ); 5 | 6 | /* MAIN */ 7 | 8 | const isServer = (): boolean => { 9 | 10 | return !IS_BROWSER; 11 | 12 | }; 13 | 14 | /* EXPORT */ 15 | 16 | export default isServer; 17 | -------------------------------------------------------------------------------- /src/methods/wrap_element.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_UNTRACKED_UNWRAPPED} from '~/constants'; 5 | 6 | /* MAIN */ 7 | 8 | const wrapElement = ( element: T ): T => { 9 | 10 | element[SYMBOL_UNTRACKED_UNWRAPPED] = true; 11 | 12 | return element; 13 | 14 | }; 15 | 16 | /* EXPORT */ 17 | 18 | export default wrapElement; 19 | -------------------------------------------------------------------------------- /src/singleton.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import isServer from '~/methods/is_server'; 5 | 6 | /* MAIN */ 7 | 8 | if ( !isServer () ) { 9 | 10 | const isLoaded = !!globalThis.VOBY; 11 | 12 | if ( isLoaded ) { 13 | 14 | throw new Error ( 'Voby has already been loaded' ); 15 | 16 | } else { 17 | 18 | globalThis.VOBY = true; 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/user.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {useParams} from 'voby-simple-router'; 5 | 6 | /* MAIN */ 7 | 8 | const PageUser = (): JSX.Element => { 9 | 10 | const params = useParams (); 11 | const name = () => params ().name; 12 | 13 | return

User: {name}

; 14 | 15 | }; 16 | 17 | /* EXPORT */ 18 | 19 | export default PageUser; 20 | -------------------------------------------------------------------------------- /demo/boxes/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | overflow: hidden; 5 | } 6 | 7 | .controls { 8 | position: fixed; 9 | left: 20px; 10 | top: 20px; 11 | right: 20px; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .controls input { 18 | flex-grow: 1; 19 | } 20 | 21 | .controls label { 22 | padding-left: 10px; 23 | } 24 | -------------------------------------------------------------------------------- /demo/hmr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "hmr", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --force --config vite.ts --mode dev", 7 | "prod": "vite --force --config vite.ts" 8 | }, 9 | "dependencies": { 10 | "vite": "^5.2.7", 11 | "voby": "*" 12 | }, 13 | "devDependencies": { 14 | "voby-vite": "^1.2.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/search.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {useSearchParams} from 'voby-simple-router'; 5 | 6 | /* MAIN */ 7 | 8 | const PageSearch = (): JSX.Element => { 9 | 10 | const params = useSearchParams (); 11 | const query = () => params ().get ( 'q' ); 12 | 13 | return

Search: {query}

; 14 | 15 | }; 16 | 17 | /* EXPORT */ 18 | 19 | export default PageSearch; 20 | -------------------------------------------------------------------------------- /src/hooks/use_abort_signal.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useAbortController from '~/hooks/use_abort_controller'; 5 | import type {ArrayMaybe} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const useAbortSignal = ( signals: ArrayMaybe = [] ): AbortSignal => { 10 | 11 | return useAbortController ( signals ).signal; 12 | 13 | }; 14 | 15 | /* EXPORT */ 16 | 17 | export default useAbortSignal; 18 | -------------------------------------------------------------------------------- /src/hooks/use_promise.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useResource from '~/hooks/use_resource'; 5 | import $$ from '~/methods/SS'; 6 | import type {FunctionMaybe, Resource} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | const usePromise = ( promise: FunctionMaybe> ): Resource => { 11 | 12 | return useResource ( () => $$(promise) ); 13 | 14 | }; 15 | 16 | /* EXPORT */ 17 | 18 | export default usePromise; 19 | -------------------------------------------------------------------------------- /src/hooks/use_cheap_disposed.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {useCleanup} from '~/hooks'; 5 | 6 | /* MAIN */ 7 | 8 | const useCheapDisposed = (): (() => boolean) => { 9 | 10 | let disposed = false; 11 | 12 | const get = (): boolean => disposed; 13 | const set = (): boolean => disposed = true; 14 | 15 | useCleanup ( set ); 16 | 17 | return get; 18 | 19 | }; 20 | 21 | /* EXPORT */ 22 | 23 | export default useCheapDisposed; 24 | -------------------------------------------------------------------------------- /src/components/ternary.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {ternary} from '~/oby'; 5 | import type {Child, FunctionMaybe, ObservableReadonly} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const Ternary = ({ when, children }: { when: FunctionMaybe, children: [Child, Child] }): ObservableReadonly => { 10 | 11 | return ternary ( when, children[0], children[1] ); 12 | 13 | }; 14 | 15 | /* EXPORT */ 16 | 17 | export default Ternary; 18 | -------------------------------------------------------------------------------- /demo/uibench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UI Bench 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/hmr/components/app.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, hmr} from 'voby'; 5 | import Counter from './counter'; 6 | 7 | /* MAIN */ 8 | 9 | const App = (): JSX.Element => { 10 | 11 | const value = $(0); 12 | 13 | return ( 14 | <> 15 |

HMR

16 | 17 | 18 | ); 19 | 20 | }; 21 | 22 | /* EXPORT */ 23 | 24 | export default hmr ( import.meta.hot?.accept?.bind ( import.meta.hot ), App ); 25 | // export default App; 26 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/scrolling.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const PageScrolling = (): JSX.Element => { 5 | 6 | return ( 7 | <> 8 |

Scrolling

9 | {new Array ( 100 ).fill ( 0 ).map ( ( _, idx ) => ( 10 | {idx} 11 | ))} 12 | 13 | ); 14 | 15 | }; 16 | 17 | /* EXPORT */ 18 | 19 | export default PageScrolling; 20 | -------------------------------------------------------------------------------- /demo/triangle/index.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background: #ffffff; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-size: 15px; 6 | line-height: 1.7; 7 | margin: 0; 8 | padding: 30px; 9 | } 10 | 11 | .container { 12 | position: absolute; 13 | transform-origin: 0 0; 14 | left: 50%; 15 | top: 50%; 16 | width: 10px; 17 | height: 10px; 18 | } 19 | 20 | .dot { 21 | position: absolute; 22 | font: normal 10px sans-serif; 23 | text-align: center; 24 | cursor: pointer; 25 | } 26 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/loader.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {Suspense} from 'voby'; 5 | import {useLoader} from 'voby-simple-router'; 6 | 7 | /* MAIN */ 8 | 9 | const PageLoader = (): JSX.Element => { 10 | 11 | const resource = useLoader (); 12 | const value = () => resource ().value; 13 | 14 | return ( 15 | Loading...}> 16 |

Loaded: {value}

17 |
18 | ); 19 | 20 | }; 21 | 22 | /* EXPORT */ 23 | 24 | export default PageLoader; 25 | -------------------------------------------------------------------------------- /demo/benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Benchmark 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/use_animation_loop.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import type {Disposer, ObservableMaybe} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const useAnimationLoop = ( callback: ObservableMaybe ): Disposer => { 10 | 11 | return useScheduler ({ 12 | callback, 13 | loop: true, 14 | cancel: cancelAnimationFrame, 15 | schedule: requestAnimationFrame 16 | }); 17 | 18 | }; 19 | 20 | /* EXPORT */ 21 | 22 | export default useAnimationLoop; 23 | -------------------------------------------------------------------------------- /src/hooks/use_animation_frame.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import type {Disposer, ObservableMaybe} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const useAnimationFrame = ( callback: ObservableMaybe ): Disposer => { 10 | 11 | return useScheduler ({ 12 | callback, 13 | once: true, 14 | cancel: cancelAnimationFrame, 15 | schedule: requestAnimationFrame 16 | }); 17 | 18 | }; 19 | 20 | /* EXPORT */ 21 | 22 | export default useAnimationFrame; 23 | -------------------------------------------------------------------------------- /demo/playground/index.css: -------------------------------------------------------------------------------- 1 | 2 | .darkred, 3 | [data-color="darkred"], 4 | [data-darkred] { 5 | color: darkred; 6 | stroke: darkred; 7 | } 8 | 9 | .darkblue, 10 | [data-color="darkblue"], 11 | [data-darkblue] { 12 | color: darkblue; 13 | stroke: darkblue; 14 | } 15 | 16 | .red, 17 | [data-color="red"], 18 | [data-red] { 19 | color: red; 20 | stroke: red; 21 | } 22 | 23 | .blue, 24 | [data-color="blue"], 25 | [data-blue] { 26 | color: blue; 27 | stroke: blue; 28 | } 29 | 30 | .bold { 31 | font-weight: bold; 32 | } 33 | 34 | .italic { 35 | font-style: italic; 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/use_interval.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import $$ from '~/methods/SS'; 6 | import type {Callback, Disposer, FunctionMaybe, ObservableMaybe} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | const useInterval = ( callback: ObservableMaybe, ms?: FunctionMaybe ): Disposer => { 11 | 12 | return useScheduler ({ 13 | callback, 14 | cancel: clearInterval, 15 | schedule: callback => setInterval ( callback, $$(ms) ) 16 | }); 17 | 18 | }; 19 | 20 | /* EXPORT */ 21 | 22 | export default useInterval; 23 | -------------------------------------------------------------------------------- /src/hooks/use_render_effect.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useEffect from '~/hooks/use_effect'; 5 | import type {Disposer, EffectFunction, EffectOptions} from '~/types'; 6 | 7 | /* HELPERS */ 8 | 9 | const options: EffectOptions = { 10 | sync: 'init' 11 | }; 12 | 13 | /* MAIN */ 14 | 15 | // This function exists for convenience, and to avoid creating unnecessary options objects 16 | 17 | const useRenderEffect = ( fn: EffectFunction ): Disposer => { 18 | 19 | return useEffect ( fn, options ); 20 | 21 | }; 22 | 23 | /* EXPORT */ 24 | 25 | export default useRenderEffect; 26 | -------------------------------------------------------------------------------- /demo/html/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, html, render} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const Counter = (): JSX.Element => { 9 | 10 | const value = $(0); 11 | 12 | const increment = () => value ( prev => prev + 1 ); 13 | const decrement = () => value ( prev => prev - 1 ); 14 | 15 | return html` 16 |

Counter

17 |

${value}

18 | 19 | 20 | `; 21 | 22 | }; 23 | 24 | /* RENDER */ 25 | 26 | render ( Counter, document.getElementById ( 'app' ) ); 27 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/pages/counter.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const PageCounter = (): JSX.Element => { 9 | 10 | const count = $(0); 11 | const increment = () => count ( prev => prev + 1 ); 12 | const decrement = () => count ( prev => prev - 1 ); 13 | 14 | return ( 15 | <> 16 |

Counter

17 |

{count}

18 | 19 | 20 | 21 | ); 22 | 23 | }; 24 | 25 | /* EXPORT */ 26 | 27 | export default PageCounter; 28 | -------------------------------------------------------------------------------- /src/jsx/runtime.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import './types'; 5 | import Fragment from '~/components/fragment'; 6 | import createElement from '~/methods/create_element'; 7 | import type {Component, Element} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const jsx =

( component: Component

, props?: P | null, key?: unknown ): Element => { 12 | 13 | props = ( key !== undefined ) ? { ...props, key } as any : props; //TSC 14 | 15 | return createElement

( component, props ); 16 | 17 | }; 18 | 19 | /* EXPORT */ 20 | 21 | export {jsx, jsx as jsxs, jsx as jsxDEV, Fragment}; 22 | -------------------------------------------------------------------------------- /demo/hyperscript/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, h, render} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const Counter = (): JSX.Element => { 9 | 10 | const value = $(0); 11 | 12 | const increment = () => value ( prev => prev + 1 ); 13 | const decrement = () => value ( prev => prev - 1 ); 14 | 15 | return [ 16 | h ( 'h1', 'Counter' ), 17 | h ( 'p', value ), 18 | h ( 'button', { onClick: increment }, '+' ), 19 | h ( 'button', { onClick: decrement }, '-' ) 20 | ]; 21 | 22 | }; 23 | 24 | /* RENDER */ 25 | 26 | render ( Counter, document.getElementById ( 'app' ) ); 27 | -------------------------------------------------------------------------------- /src/hooks/use_timeout.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import $$ from '~/methods/SS'; 6 | import type {Callback, Disposer, FunctionMaybe, ObservableMaybe} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | const useTimeout = ( callback: ObservableMaybe, ms?: FunctionMaybe ): Disposer => { 11 | 12 | return useScheduler ({ 13 | callback, 14 | once: true, 15 | cancel: clearTimeout, 16 | schedule: callback => setTimeout ( callback, $$(ms) ) 17 | }); 18 | 19 | }; 20 | 21 | /* EXPORT */ 22 | 23 | export default useTimeout; 24 | -------------------------------------------------------------------------------- /demo/counter/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, render} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const Counter = (): JSX.Element => { 9 | 10 | const value = $(0); 11 | 12 | const increment = () => value ( prev => prev + 1 ); 13 | const decrement = () => value ( prev => prev - 1 ); 14 | 15 | return ( 16 | <> 17 |

Counter

18 |

{value}

19 | 20 | 21 | 22 | ); 23 | 24 | }; 25 | 26 | /* RENDER */ 27 | 28 | render ( , document.getElementById ( 'app' ) ); 29 | -------------------------------------------------------------------------------- /src/hooks/use_idle_loop.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import $$ from '~/methods/SS'; 6 | import type {Disposer, FunctionMaybe, ObservableMaybe} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | const useIdleLoop = ( callback: ObservableMaybe, options?: FunctionMaybe ): Disposer => { 11 | 12 | return useScheduler ({ 13 | callback, 14 | loop: true, 15 | cancel: cancelIdleCallback, 16 | schedule: callback => requestIdleCallback ( callback, $$(options) ) 17 | }); 18 | 19 | }; 20 | 21 | /* EXPORT */ 22 | 23 | export default useIdleLoop; 24 | -------------------------------------------------------------------------------- /src/components/error_boundary.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import untrack from '~/methods/untrack'; 5 | import {tryCatch} from '~/oby'; 6 | import {isFunction} from '~/utils/lang'; 7 | import type {Callback, Child, FN, ObservableReadonly} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const ErrorBoundary = ({ fallback, children }: { fallback: Child | FN<[{ error: Error, reset: Callback }], Child>, children: Child }): ObservableReadonly => { 12 | 13 | return tryCatch ( children, props => untrack ( () => isFunction ( fallback ) ? fallback ( props ) : fallback ) ); 14 | 15 | }; 16 | 17 | /* EXPORT */ 18 | 19 | export default ErrorBoundary; 20 | -------------------------------------------------------------------------------- /demo/store_counter/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {render, store} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const Counter = (): JSX.Element => { 9 | 10 | const state = store ({ 11 | value: 0 12 | }); 13 | 14 | const increment = () => state.value += 1; 15 | const decrement = () => state.value -= 1; 16 | 17 | return ( 18 | <> 19 |

Store Counter

20 |

{() => state.value}

21 | 22 | 23 | 24 | ); 25 | 26 | }; 27 | 28 | /* RENDER */ 29 | 30 | render ( , document.getElementById ( 'app' ) ); 31 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Dynamic from '~/components/dynamic'; 5 | import ErrorBoundary from '~/components/error_boundary'; 6 | import For from '~/components/for'; 7 | import Fragment from '~/components/fragment'; 8 | import If from '~/components/if'; 9 | import KeepAlive from '~/components/keep_alive'; 10 | import Portal from '~/components/portal'; 11 | import Suspense from '~/components/suspense'; 12 | import Switch from '~/components/switch'; 13 | import Ternary from '~/components/ternary'; 14 | 15 | /* EXPORT */ 16 | 17 | export {Dynamic, ErrorBoundary, For, Fragment, If, KeepAlive, Portal, Suspense, Switch, Ternary}; 18 | -------------------------------------------------------------------------------- /src/hooks/use_idle_callback.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useScheduler from '~/hooks/use_scheduler'; 5 | import $$ from '~/methods/SS'; 6 | import type {Disposer, FunctionMaybe, ObservableMaybe} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | const useIdleCallback = ( callback: ObservableMaybe, options?: FunctionMaybe ): Disposer => { 11 | 12 | return useScheduler ({ 13 | callback, 14 | once: true, 15 | cancel: cancelIdleCallback, 16 | schedule: callback => requestIdleCallback ( callback, $$(options) ) 17 | }); 18 | 19 | }; 20 | 21 | /* EXPORT */ 22 | 23 | export default useIdleCallback; 24 | -------------------------------------------------------------------------------- /src/utils/creators.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import type {ComponentIntrinsicElement, FN} from '~/types'; 5 | 6 | /* MAIN */ 7 | 8 | const createComment: FN<[], Comment> = document.createComment.bind ( document, '' ); 9 | 10 | const createHTMLNode: FN<[ComponentIntrinsicElement], HTMLElement> = document.createElement.bind ( document ); 11 | 12 | const createSVGNode: FN<[ComponentIntrinsicElement], Element> = document.createElementNS.bind ( document, 'http://www.w3.org/2000/svg' ); 13 | 14 | const createText: FN<[any], Text> = document.createTextNode.bind ( document ); 15 | 16 | /* EXPORT */ 17 | 18 | export {createComment, createHTMLNode, createSVGNode, createText}; 19 | -------------------------------------------------------------------------------- /src/hooks/use_microtask.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useCheapDisposed from '~/hooks/use_cheap_disposed'; 5 | import {with as _with} from '~/oby'; 6 | import type {Callback} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | //TODO: Maybe port this to oby 11 | //TODO: Maybe special-case this to use one shared mirotask per microtask 12 | 13 | const useMicrotask = ( fn: Callback ): void => { 14 | 15 | const disposed = useCheapDisposed (); 16 | const runWithOwner = _with (); 17 | 18 | queueMicrotask ( () => { 19 | 20 | if ( disposed () ) return; 21 | 22 | runWithOwner ( fn ); 23 | 24 | }); 25 | 26 | }; 27 | 28 | /* EXPORT */ 29 | 30 | export default useMicrotask; 31 | -------------------------------------------------------------------------------- /src/methods/html.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import htm from 'htm'; 5 | import createElement from '~/methods/create_element'; 6 | import {assign} from '~/utils/lang'; 7 | import type {Child, ComponentsMap, Element, Props} from '~/types'; 8 | 9 | /* HELPERS */ 10 | 11 | const registry: ComponentsMap = {}; 12 | const h = ( type: string, props?: Props | null, ...children: Child[] ): Element => createElement ( registry[type] || type, props, ...children ); 13 | const register = ( components: ComponentsMap ): void => void assign ( registry, components ); 14 | 15 | /* MAIN */ 16 | 17 | const html = assign ( htm.bind ( h ), { register } ); 18 | 19 | /* EXPORT */ 20 | 21 | export default html; 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import '~/singleton'; 5 | import '~/jsx/types'; 6 | import type {Context, Directive, DirectiveOptions, EffectOptions, FunctionMaybe, MemoOptions, Observable, ObservableLike, ObservableReadonly, ObservableReadonlyLike, ObservableMaybe, ObservableOptions, Resource, StoreOptions} from '~/types'; 7 | 8 | /* EXPORT */ 9 | 10 | export * from '~/components'; 11 | export * from '~/jsx/runtime'; 12 | export * from '~/hooks'; 13 | export * from '~/methods'; 14 | export type {Context, Directive, DirectiveOptions, EffectOptions, FunctionMaybe, MemoOptions, Observable, ObservableLike, ObservableReadonly, ObservableReadonlyLike, ObservableMaybe, ObservableOptions, Resource, StoreOptions}; 15 | -------------------------------------------------------------------------------- /demo/boxes/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/clock/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/html/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/benchmark/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/counter/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/creation/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/playground/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/spiral/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/triangle/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/uibench/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/emoji_counter/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/hyperscript/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/store_counter/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | 8 | /* MAIN */ 9 | 10 | const config = defineConfig ({ 11 | resolve: { 12 | alias: { 13 | '~': path.resolve ( '../../src' ), 14 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 15 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 16 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 17 | } 18 | } 19 | }); 20 | 21 | /* EXPORT */ 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /demo/hmr/components/counter.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {hmr} from 'voby'; 5 | import Button from './button'; 6 | 7 | /* MAIN */ 8 | 9 | const Counter = ({ value, onChange }: { value: () => number, onChange: ( value: number ) => void }): JSX.Element => { 10 | 11 | const increment = () => onChange ( value () + 1 ); 12 | const decrement = () => onChange ( value () - 1 ); 13 | 14 | return ( 15 | <> 16 |

{value}

17 | 18 | 19 | 20 | ); 21 | 22 | }; 23 | 24 | /* EXPORT */ 25 | 26 | export default hmr ( import.meta.hot?.accept?.bind ( import.meta.hot ), Counter ); 27 | // export default Counter; 28 | -------------------------------------------------------------------------------- /demo/hmr/components/button.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, hmr} from 'voby'; 5 | 6 | /* MAIN */ 7 | 8 | const Button = (): JSX.Element => { 9 | 10 | throw new Error ( 'Unimplemented' ); 11 | 12 | }; 13 | 14 | /* UTILITIES */ 15 | 16 | Button.Repeat = ({ label, onClick }: { label: string, onClick: () => void }): JSX.Element => { 17 | 18 | const value = $(label); 19 | 20 | const click = (): void => { 21 | value ( prev => prev + label ); 22 | onClick (); 23 | }; 24 | 25 | return ( 26 | 29 | ); 30 | 31 | }; 32 | 33 | /* EXPORT */ 34 | 35 | export default hmr ( import.meta.hot?.accept?.bind ( import.meta.hot ), Button ); 36 | // export default Button; 37 | -------------------------------------------------------------------------------- /src/hooks/use_fetch.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useAbortSignal from '~/hooks/use_abort_signal'; 5 | import useResolved from '~/hooks/use_resolved'; 6 | import useResource from '~/hooks/use_resource'; 7 | import type {FunctionMaybe, Resource} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const useFetch = ( request: FunctionMaybe, init?: FunctionMaybe ): Resource => { 12 | 13 | return useResource ( () => { 14 | 15 | return useResolved ( [request, init], ( request, init = {} ) => { 16 | 17 | const signal = useAbortSignal ( init.signal || [] ); 18 | 19 | init.signal = signal; 20 | 21 | return fetch ( request, init ); 22 | 23 | }); 24 | 25 | }); 26 | 27 | }; 28 | 29 | /* EXPORT */ 30 | 31 | export default useFetch; 32 | -------------------------------------------------------------------------------- /src/methods/render.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useRoot from '~/hooks/use_root'; 5 | import useUntracked from '~/hooks/use_untracked'; 6 | import {setChild} from '~/utils/setters'; 7 | import type {Child, Disposer} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const render = ( child: Child, parent?: Element | null ): Disposer => { 12 | 13 | if ( !parent || !( parent instanceof HTMLElement ) ) throw new Error ( 'Invalid parent node' ); 14 | 15 | parent.textContent = ''; 16 | 17 | return useRoot ( dispose => { 18 | 19 | setChild ( parent, useUntracked ( child ) ); 20 | 21 | return (): void => { 22 | 23 | dispose (); 24 | 25 | parent.textContent = ''; 26 | 27 | }; 28 | 29 | }); 30 | 31 | }; 32 | 33 | /* EXPORT */ 34 | 35 | export default render; 36 | -------------------------------------------------------------------------------- /src/hooks/use_context.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {CONTEXTS_DATA} from '~/constants'; 5 | import {context} from '~/oby'; 6 | import {isNil} from '~/utils/lang'; 7 | import type {Context, ContextWithDefault} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | function useContext ( Context: ContextWithDefault ): T; 12 | function useContext ( Context: Context ): T | undefined; 13 | function useContext ( Context: ContextWithDefault | Context ): T | undefined { 14 | 15 | const {symbol, defaultValue} = CONTEXTS_DATA.get ( Context ) || { symbol: Symbol () }; 16 | const valueContext = context ( symbol ); 17 | const value = isNil ( valueContext ) ? defaultValue : valueContext; 18 | 19 | return value; 20 | 21 | } 22 | 23 | /* EXPORT */ 24 | 25 | export default useContext; 26 | -------------------------------------------------------------------------------- /demo/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single-file HTML 6 | 7 | 8 |
9 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/methods/h.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import createElement from '~/methods/create_element'; 5 | import {isArray, isObject} from '~/utils/lang'; 6 | import type {Child, Component, Element} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | function h

( component: Component

, child: Child ): Element; 11 | function h

( component: Component

, props?: P | null, ...children: Child[] ): Element; 12 | function h

( component: Component

, props?: Child | P | null, ...children: Child[] ): Element { 13 | 14 | if ( children.length || ( isObject ( props ) && !isArray ( props ) ) ) { 15 | 16 | return createElement ( component, props as any, ...children ); //TSC 17 | 18 | } else { 19 | 20 | return createElement ( component, null, props as Child ); //TSC 21 | 22 | } 23 | 24 | } 25 | 26 | /* EXPORT */ 27 | 28 | export default h; 29 | -------------------------------------------------------------------------------- /demo/hmr/vite.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import path from 'node:path'; 5 | import process from 'node:process'; 6 | import {defineConfig} from 'vite'; 7 | // import voby from 'voby-vite'; 8 | 9 | /* MAIN */ 10 | 11 | const config = defineConfig ({ 12 | // plugins: [ 13 | // voby ({ 14 | // hmr: { 15 | // enabled: true 16 | // } 17 | // }) 18 | // ], 19 | resolve: { 20 | alias: { 21 | '~': path.resolve ( '../../src' ), 22 | 'voby/jsx-dev-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-dev-runtime', 23 | 'voby/jsx-runtime': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src/jsx/runtime' ) : 'voby/jsx-runtime', 24 | 'voby': process.argv.includes ( 'dev' ) ? path.resolve ( '../../src' ) : 'voby' 25 | } 26 | } 27 | }); 28 | 29 | /* EXPORT */ 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /src/components/dynamic.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useMemo from '~/hooks/use_memo'; 5 | import createElement from '~/methods/create_element'; 6 | import resolve from '~/methods/resolve'; 7 | import $$ from '~/methods/SS'; 8 | import {isFunction} from '~/utils/lang'; 9 | import type {Child, Component, FunctionMaybe} from '~/types'; 10 | 11 | /* MAIN */ 12 | 13 | const Dynamic =

({ component, props, children }: { component: Component

, props?: FunctionMaybe

, children?: Child }): Child => { 14 | 15 | if ( isFunction ( component ) || isFunction ( props ) ) { 16 | 17 | return useMemo ( () => { 18 | 19 | return resolve ( createElement

( $$(component, false), $$(props), children ) ); 20 | 21 | }); 22 | 23 | } else { 24 | 25 | return createElement

( component, props, children ); 26 | 27 | } 28 | 29 | }; 30 | 31 | /* EXPORT */ 32 | 33 | export default Dynamic; 34 | -------------------------------------------------------------------------------- /src/hooks/use_abort_controller.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useCleanup from '~/hooks/use_cleanup'; 5 | import useEventListener from '~/hooks/use_event_listener'; 6 | import {castArray} from '~/utils/lang'; 7 | import type {ArrayMaybe} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const useAbortController = ( signals: ArrayMaybe = [] ): AbortController => { 12 | 13 | signals = castArray ( signals ); 14 | 15 | const controller = new AbortController (); 16 | const abort = controller.abort.bind ( controller ); 17 | const aborted = signals.some ( signal => signal.aborted ); 18 | 19 | if ( aborted ) { 20 | 21 | abort (); 22 | 23 | } else { 24 | 25 | signals.forEach ( signal => useEventListener ( signal, 'abort', abort ) ); 26 | 27 | useCleanup ( abort ); 28 | 29 | } 30 | 31 | return controller; 32 | 33 | }; 34 | 35 | /* EXPORT */ 36 | 37 | export default useAbortController; 38 | -------------------------------------------------------------------------------- /src/methods/create_context.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {CONTEXTS_DATA} from '~/constants'; 5 | import resolve from '~/methods/resolve'; 6 | import {context} from '~/oby'; 7 | import type {Child, Context, ContextWithDefault} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | function createContext ( defaultValue: T ): ContextWithDefault; 12 | function createContext ( defaultValue?: T ): Context; 13 | function createContext ( defaultValue?: T ): ContextWithDefault | Context { 14 | 15 | const symbol = Symbol (); 16 | 17 | const Provider = ({ value, children }: { value: T, children: Child }): Child => { 18 | 19 | return context ( { [symbol]: value }, () => { 20 | 21 | return resolve ( children ); 22 | 23 | }); 24 | 25 | }; 26 | 27 | const Context = {Provider}; 28 | 29 | CONTEXTS_DATA.set ( Context, { symbol, defaultValue } ); 30 | 31 | return Context; 32 | 33 | } 34 | 35 | /* EXPORT */ 36 | 37 | export default createContext; 38 | -------------------------------------------------------------------------------- /src/components/suspense.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import SuspenseContext from '~/components/suspense.context'; 5 | import useMemo from '~/hooks/use_memo'; 6 | import useSuspense from '~/hooks/use_suspense'; 7 | import resolve from '~/methods/resolve'; 8 | import $$ from '~/methods/SS'; 9 | import {suspense as _suspense, ternary} from '~/oby'; 10 | import type {Child, FunctionMaybe, ObservableReadonly} from '~/types'; 11 | 12 | /* MAIN */ 13 | 14 | const Suspense = ({ when, fallback, children }: { when?: FunctionMaybe, fallback?: Child, children: Child }): ObservableReadonly => { 15 | 16 | return SuspenseContext.wrap ( suspense => { 17 | 18 | const condition = useMemo ( () => !!$$(when) || suspense.active () ); 19 | 20 | const childrenSuspended = useSuspense ( condition, () => resolve ( children ) ); 21 | 22 | return ternary ( condition, fallback, childrenSuspended ); 23 | 24 | }); 25 | 26 | }; 27 | 28 | /* EXPORT */ 29 | 30 | export default Suspense; 31 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_OBSERVABLE, SYMBOL_OBSERVABLE_FROZEN, SYMBOL_OBSERVABLE_READABLE, SYMBOL_UNCACHED, SYMBOL_UNTRACKED, SYMBOL_UNTRACKED_UNWRAPPED} from '~/oby'; 5 | import type {ContextData, Context, DirectiveData} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const CONTEXTS_DATA = new WeakMap, ContextData> (); 10 | 11 | const DIRECTIVES: Record> = {}; 12 | 13 | const SYMBOL_SUSPENSE = Symbol ( 'Suspense' ); 14 | 15 | const SYMBOL_SUSPENSE_COLLECTOR = Symbol ( 'Suspense.Collector' ); 16 | 17 | const SYMBOL_TEMPLATE_ACCESSOR = Symbol ( 'Template.Accessor' ); 18 | 19 | const SYMBOLS_DIRECTIVES: Record = {}; 20 | 21 | /* EXPORT */ 22 | 23 | export {SYMBOL_OBSERVABLE, SYMBOL_OBSERVABLE_FROZEN, SYMBOL_OBSERVABLE_READABLE, SYMBOL_UNCACHED, SYMBOL_UNTRACKED, SYMBOL_UNTRACKED_UNWRAPPED}; 24 | export {CONTEXTS_DATA, DIRECTIVES, SYMBOL_SUSPENSE, SYMBOL_SUSPENSE_COLLECTOR, SYMBOL_TEMPLATE_ACCESSOR, SYMBOLS_DIRECTIVES}; 25 | -------------------------------------------------------------------------------- /demo/clock/index.css: -------------------------------------------------------------------------------- 1 | 2 | .clock { 3 | position: fixed; 4 | inset: 0; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .clock svg { 11 | aspect-ratio: 1; 12 | width: 100%; 13 | height: 100%; 14 | max-width: 80vw; 15 | max-height: 80vh; 16 | } 17 | 18 | .clock-face { 19 | stroke: #333333; 20 | fill: #ffffff; 21 | } 22 | 23 | .minor { 24 | stroke: #b4bfcc; 25 | stroke-width: 0.5; 26 | stroke-linecap: round; 27 | } 28 | 29 | .major { 30 | stroke: #333333; 31 | stroke-width: 0.75; 32 | stroke-linecap: round; 33 | } 34 | 35 | .hour { 36 | stroke: #333333; 37 | stroke-width: 1.75; 38 | stroke-linecap: round; 39 | } 40 | 41 | .minute { 42 | stroke: #333333; 43 | stroke-width: 1.25; 44 | stroke-linecap: round; 45 | } 46 | 47 | .second { 48 | stroke: #dd0000; 49 | stroke-width: 0.75; 50 | stroke-linecap: round; 51 | } 52 | 53 | .millisecond { 54 | stroke: #b4bfcc66; 55 | stroke-width: 3; 56 | stroke-linecap: round; 57 | } 58 | -------------------------------------------------------------------------------- /src/hooks/use_guarded.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useMemo from '~/hooks/use_memo'; 5 | import $$ from '~/methods/SS'; 6 | import {isNil} from '~/utils/lang'; 7 | import type {FunctionMaybe} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | //TODO: Maybe port this to oby, as "when" or "is" or "guarded" 12 | //TODO: Optimize this, checking if the value is actually potentially reactive 13 | 14 | const useGuarded = ( value: FunctionMaybe, guard: (( value: T ) => value is U) ): (() => U) => { 15 | 16 | let valueLast: U | undefined; 17 | 18 | const guarded = useMemo ( () => { 19 | 20 | const current = $$(value); 21 | 22 | if ( !guard ( current ) ) return valueLast; 23 | 24 | return valueLast = current; 25 | 26 | }); 27 | 28 | return (): U => { 29 | 30 | const current = guarded (); 31 | 32 | if ( isNil ( current ) ) throw new Error ( 'The value never passed the type guard' ); 33 | 34 | return current; 35 | 36 | }; 37 | 38 | }; 39 | 40 | /* EXPORT */ 41 | 42 | export default useGuarded; 43 | -------------------------------------------------------------------------------- /src/methods/render_to_string.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Portal from '~/components/portal'; 5 | import SuspenseCollector from '~/components/suspense.collector'; 6 | import useEffect from '~/hooks/use_effect'; 7 | import useRoot from '~/hooks/use_root'; 8 | import $$ from '~/methods/SS'; 9 | import type {Child} from '~/types'; 10 | 11 | /* MAIN */ 12 | 13 | //TODO: Implement this properly, without relying on JSDOM or stuff like that 14 | 15 | const renderToString = ( child: Child ): Promise => { 16 | 17 | return new Promise ( resolve => { 18 | 19 | useRoot ( dispose => { 20 | 21 | $$(SuspenseCollector.wrap ( suspenses => { 22 | 23 | const {portal} = Portal ({ children: child }).metadata; 24 | 25 | useEffect ( () => { 26 | 27 | if ( suspenses.active () ) return; 28 | 29 | resolve ( portal.innerHTML ); 30 | 31 | dispose (); 32 | 33 | }, { suspense: false } ); 34 | 35 | })); 36 | 37 | }); 38 | 39 | }); 40 | 41 | }; 42 | 43 | /* EXPORT */ 44 | 45 | export default renderToString; 46 | -------------------------------------------------------------------------------- /src/components/for.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {for as _for} from '~/oby'; 5 | import type {Child, FunctionMaybe, Indexed, ObservableReadonly} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | function For ({ values, fallback, pooled, unkeyed, children }: { values?: FunctionMaybe, fallback?: Child, pooled?: false, unkeyed?: false, children: (( value: T, index: FunctionMaybe ) => Child) }): ObservableReadonly; 10 | function For ({ values, fallback, pooled, unkeyed, children }: { values?: FunctionMaybe, fallback?: Child, pooled?: boolean, unkeyed: true, children: (( value: Indexed, index: FunctionMaybe ) => Child) }): ObservableReadonly; 11 | function For ({ values, fallback, pooled, unkeyed, children }: { values?: FunctionMaybe, fallback?: Child, pooled?: boolean, unkeyed?: boolean, children: (( value: T | Indexed, index: FunctionMaybe ) => Child) }): ObservableReadonly { 12 | 13 | return _for ( values, children, fallback, { pooled, unkeyed } as any ); //TSC 14 | 15 | } 16 | 17 | /* EXPORT */ 18 | 19 | export default For; 20 | -------------------------------------------------------------------------------- /demo/spiral/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | height: 100%; 5 | background: #222222; 6 | font: 100%/1.21 'Helvetica Neue',helvetica,sans-serif; 7 | text-rendering: optimizeSpeed; 8 | color: #888888; 9 | overflow: hidden; 10 | } 11 | 12 | #main { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | width: 100%; 17 | height: 100%; 18 | overflow: hidden; 19 | } 20 | 21 | .cursor { 22 | position: absolute; 23 | left: 0; 24 | top: 0; 25 | width: 8px; 26 | height: 8px; 27 | margin: -5px 0 0 -5px; 28 | border: 2px solid #FF0000; 29 | border-radius: 50%; 30 | transform-origin: 50% 50%; 31 | transition: all 250ms ease; 32 | transition-property: width, height, margin; 33 | pointer-events: none; 34 | overflow: hidden; 35 | font-size: 9px; 36 | line-height: 25px; 37 | text-indent: 15px; 38 | white-space: nowrap; 39 | } 40 | 41 | .cursor.label { 42 | position: absolute; 43 | left: 0; 44 | top: 0; 45 | z-index: 10; 46 | } 47 | 48 | .cursor.label { 49 | overflow: visible; 50 | } 51 | 52 | .cursor.big { 53 | width: 24px; 54 | height: 24px; 55 | margin: -13px 0 0 -13px; 56 | } 57 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/if.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import isObservable from '~/methods/is_observable'; 5 | import useGuarded from '~/hooks/use_guarded'; 6 | import useUntracked from '~/hooks/use_untracked'; 7 | import {ternary} from '~/oby'; 8 | import {isComponent, isFunction, isTruthy} from '~/utils/lang'; 9 | import type {Child, FunctionMaybe, ObservableReadonly, Truthy} from '~/types'; 10 | 11 | /* MAIN */ 12 | 13 | //TODO: Support an is/guard prop, maybe 14 | 15 | const If = ({ when, fallback, children }: { when: FunctionMaybe, fallback?: Child, children: Child | (( value: (() => Truthy) ) => Child) }): ObservableReadonly => { 16 | 17 | if ( isFunction ( children ) && !isObservable ( children ) && !isComponent ( children ) ) { // Calling the children function with an (() => Truthy) 18 | 19 | const truthy = useGuarded ( when, isTruthy ); 20 | 21 | return ternary ( when, useUntracked ( () => children ( truthy ) ), fallback ); 22 | 23 | } else { // Just passing the children along 24 | 25 | return ternary ( when, children as Child, fallback ); //TSC 26 | 27 | } 28 | 29 | }; 30 | 31 | /* EXPORT */ 32 | 33 | export default If; 34 | -------------------------------------------------------------------------------- /src/methods/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import $ from '~/methods/S'; 5 | import $$ from '~/methods/SS'; 6 | import batch from '~/methods/batch'; 7 | import createContext from '~/methods/create_context'; 8 | import createDirective from '~/methods/create_directive'; 9 | import createElement from '~/methods/create_element'; 10 | import h from '~/methods/h'; 11 | import hmr from '~/methods/hmr'; 12 | import html from '~/methods/html'; 13 | import isBatching from '~/methods/is_batching'; 14 | import isObservable from '~/methods/is_observable'; 15 | import isServer from '~/methods/is_server'; 16 | import isStore from '~/methods/is_store'; 17 | import lazy from '~/methods/lazy'; 18 | import render from '~/methods/render'; 19 | import renderToString from '~/methods/render_to_string'; 20 | import resolve from '~/methods/resolve'; 21 | import store from '~/methods/store'; 22 | import template from '~/methods/template'; 23 | import tick from '~/methods/tick'; 24 | import untrack from '~/methods/untrack'; 25 | 26 | /* EXPORT */ 27 | 28 | export {$, $$, batch, createContext, createDirective, createElement, h, hmr, html, isBatching, isObservable, isServer, isStore, lazy, render, renderToString, resolve, store, template, tick, untrack}; 29 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Routes from './routes'; 5 | import {Link, Route, Router} from 'voby-simple-router'; 6 | import type {RouterPath} from 'voby-simple-router'; 7 | 8 | /* MAIN */ 9 | 10 | const App = ({ path }: { path?: RouterPath }): JSX.Element => { 11 | 12 | return ( 13 | 14 |

42 |
43 | 44 |
45 | 46 | ); 47 | 48 | }; 49 | 50 | /* EXPORT */ 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /src/components/switch.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {switch as _switch} from '~/oby'; 5 | import {assign, castArray} from '~/utils/lang'; 6 | import type {Child, ChildWithMetadata, FunctionMaybe, ObservableReadonly} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | //TODO: Enforce children of Switch to be of type Switch.Case or Switch.Default 11 | 12 | const Switch = ({ when, fallback, children }: { when: FunctionMaybe, fallback?: Child, children: Child }): ObservableReadonly => { 13 | 14 | const childrenWithValues = castArray ( children ) as (() => ChildWithMetadata<[T, Child] | [Child]>)[]; //TSC 15 | const values = childrenWithValues.map ( child => child ().metadata ); 16 | 17 | return _switch ( when, values as any, fallback ); //TSC 18 | 19 | }; 20 | 21 | /* UTILITIES */ 22 | 23 | Switch.Case = ({ when, children }: { when: T, children: Child }): ChildWithMetadata<[T, Child]> => { 24 | 25 | const metadata: { metadata: [T, Child] } = { metadata: [when, children] }; 26 | 27 | return assign ( () => children, metadata ); 28 | 29 | }; 30 | 31 | Switch.Default = ({ children }: { children: Child }): ChildWithMetadata<[Child]> => { 32 | 33 | const metadata: { metadata: [Child] } = { metadata: [children] }; 34 | 35 | return assign ( () => children, metadata ); 36 | 37 | }; 38 | 39 | /* EXPORT */ 40 | 41 | export default Switch; 42 | -------------------------------------------------------------------------------- /src/hooks/use_scheduler.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useEffect from '~/hooks/use_effect'; 5 | import useSuspended from '~/hooks/use_suspended'; 6 | import $$ from '~/methods/SS'; 7 | import untrack from '~/methods/untrack'; 8 | import type {Disposer, FN, FunctionMaybe, ObservableMaybe} from '~/types'; 9 | 10 | /* MAIN */ 11 | 12 | const useScheduler = ({ loop, once, callback, cancel, schedule }: { loop?: FunctionMaybe, once?: boolean, callback: ObservableMaybe>, cancel: FN<[T]>, schedule: (( callback: FN<[U]> ) => T) }) : Disposer => { 13 | 14 | let executed = false; 15 | let suspended = useSuspended (); 16 | let tickId: T; 17 | 18 | const work = ( value: U ): void => { 19 | 20 | executed = true; 21 | 22 | if ( $$(loop) ) tick (); 23 | 24 | $$(callback, false)( value ); 25 | 26 | }; 27 | 28 | const tick = (): void => { 29 | 30 | tickId = untrack ( () => schedule ( work ) ); 31 | 32 | }; 33 | 34 | const dispose = (): void => { 35 | 36 | untrack ( () => cancel ( tickId ) ); 37 | 38 | }; 39 | 40 | useEffect ( () => { 41 | 42 | if ( once && executed ) return; 43 | 44 | if ( suspended () ) return; 45 | 46 | tick (); 47 | 48 | return dispose; 49 | 50 | }, { suspense: false } ); 51 | 52 | return dispose; 53 | 54 | }; 55 | 56 | /* EXPORT */ 57 | 58 | export default useScheduler; 59 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/src/app/routes.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Page404 from '../pages/404'; 5 | import PageCounter from '../pages/counter'; 6 | import PageHome from '../pages/home'; 7 | import PageLoader from '../pages/loader'; 8 | import PageScrolling from '../pages/scrolling'; 9 | import PageSearch from '../pages/search'; 10 | import PageUser from '../pages/user'; 11 | import {lazy} from 'voby'; 12 | import {Navigate} from 'voby-simple-router'; 13 | import type {RouterRoute} from 'voby-simple-router'; 14 | 15 | /* MAIN */ 16 | 17 | const Routes: RouterRoute[] = [ 18 | { 19 | path: '/', 20 | to: PageHome 21 | }, 22 | { 23 | path: '/counter', 24 | to: PageCounter 25 | }, 26 | { 27 | path: '/loader', 28 | // to: lazy ( () => import ( '../pages/loader' ) ), //FIXME: https://github.com/evanw/esbuild/issues/2983 29 | to: PageLoader, 30 | loader: () => new Promise ( resolve => setTimeout ( resolve, 1000, 123 ) ) 31 | }, 32 | { 33 | path: '/redirect', 34 | to: 35 | }, 36 | { 37 | path: '/scrolling', 38 | to: PageScrolling 39 | }, 40 | { 41 | path: '/search', 42 | to: PageSearch 43 | }, 44 | { 45 | path: '/user/:name', 46 | to: PageUser 47 | }, 48 | { 49 | path: '/404', 50 | to: Page404 51 | } 52 | ]; 53 | 54 | /* EXPORT */ 55 | 56 | export default Routes; 57 | -------------------------------------------------------------------------------- /src/utils/classlist.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {isString} from '~/utils/lang'; 5 | 6 | /* MAIN */ 7 | 8 | // This function exists to optimize memory usage in some cases, where the classList API won't be touched without sacrificing performance 9 | 10 | const classesToggle = ( element: HTMLElement, classes: string, force: null | undefined | boolean ): void => { 11 | 12 | const {className} = element; 13 | 14 | /* OPTIMIZED PATH */ 15 | 16 | if ( isString ( className ) ) { 17 | 18 | if ( !className ) { // Optimized addition/deletion 19 | 20 | if ( force ) { // Optimized addition 21 | 22 | element.className = classes; 23 | 24 | return; 25 | 26 | } else { // Optimized deletion, nothing to do really 27 | 28 | return; 29 | 30 | } 31 | 32 | } else if ( !force && className === classes ) { // Optimized deletion 33 | 34 | element.className = ''; 35 | 36 | return; 37 | 38 | } 39 | 40 | } 41 | 42 | /* REGULAR PATH */ 43 | 44 | if ( classes.includes ( ' ' ) ) { 45 | 46 | classes.split ( ' ' ).forEach ( cls => { 47 | 48 | if ( !cls.length ) return; 49 | 50 | element.classList.toggle ( cls, !!force ); 51 | 52 | }); 53 | 54 | } else { 55 | 56 | element.classList.toggle ( classes, !!force ); 57 | 58 | } 59 | 60 | }; 61 | 62 | /* EXPORT */ 63 | 64 | export {classesToggle}; 65 | -------------------------------------------------------------------------------- /src/methods/create_directive.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {DIRECTIVES, SYMBOLS_DIRECTIVES} from '~/constants'; 5 | import resolve from '~/methods/resolve'; 6 | import {context} from '~/oby'; 7 | import type {Child, DirectiveFunction, Directive, DirectiveData, DirectiveOptions, ExtractArray} from '~/types'; 8 | 9 | /* MAIN */ 10 | 11 | const createDirective = ( name: T, fn: DirectiveFunction>, options?: DirectiveOptions ): Directive> => { 12 | 13 | const immediate = !!options?.immediate; 14 | const data: DirectiveData> = { fn, immediate }; 15 | const symbol = ( SYMBOLS_DIRECTIVES[name] ||= Symbol () ); 16 | 17 | const Provider = ({ children }: { children: Child }): Child => { 18 | 19 | return context ( { [symbol]: data }, () => { 20 | 21 | return resolve ( children ); 22 | 23 | }); 24 | 25 | }; 26 | 27 | const ref = ( ...args: ExtractArray ) => { 28 | 29 | return ( element: Element ): void => { 30 | 31 | fn ( element, ...args ); 32 | 33 | }; 34 | 35 | }; 36 | 37 | const register = (): void => { 38 | 39 | if ( symbol in DIRECTIVES ) throw new Error ( 'Directive "name" is already registered' ); 40 | 41 | DIRECTIVES[symbol] = data; 42 | 43 | }; 44 | 45 | return {Provider, ref, register}; 46 | 47 | }; 48 | 49 | /* EXPORT */ 50 | 51 | export default createDirective; 52 | -------------------------------------------------------------------------------- /src/components/portal.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useBoolean from '~/hooks/use_boolean'; 5 | import useRenderEffect from '~/hooks/use_render_effect'; 6 | import render from '~/methods/render'; 7 | import $$ from '~/methods/SS'; 8 | import {createHTMLNode} from '~/utils/creators'; 9 | import {assign} from '~/utils/lang'; 10 | import type {Child, ChildWithMetadata, FunctionMaybe} from '~/types'; 11 | 12 | /* MAIN */ 13 | 14 | const Portal = ({ when = true, mount, wrapper, children }: { mount?: Child, when?: FunctionMaybe, wrapper?: Child, children: Child }): ChildWithMetadata<{ portal: HTMLElement }> => { 15 | 16 | const portal = $$(wrapper) || createHTMLNode ( 'div' ); 17 | 18 | if ( !( portal instanceof HTMLElement ) ) throw new Error ( 'Invalid wrapper node' ); 19 | 20 | const condition = useBoolean ( when ); 21 | 22 | useRenderEffect ( () => { 23 | 24 | if ( !$$(condition) ) return; 25 | 26 | const parent = $$(mount) || document.body; 27 | 28 | if ( !( parent instanceof Element ) ) throw new Error ( 'Invalid mount node' ); 29 | 30 | parent.insertBefore ( portal, null ); 31 | 32 | return (): void => { 33 | 34 | parent.removeChild ( portal ); 35 | 36 | }; 37 | 38 | }); 39 | 40 | useRenderEffect ( () => { 41 | 42 | if ( !$$(condition) ) return; 43 | 44 | return render ( children, portal ); 45 | 46 | }); 47 | 48 | return assign ( () => $$(condition) || children, { metadata: { portal } } ); 49 | 50 | }; 51 | 52 | /* EXPORT */ 53 | 54 | export default Portal; 55 | -------------------------------------------------------------------------------- /src/components/suspense.manager.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import SuspenseContext from '~/components/suspense.context'; 5 | import useCleanup from '~/hooks/use_cleanup'; 6 | import type {SuspenseData} from '~/types'; 7 | 8 | /* MAIN */ 9 | 10 | class SuspenseManager { 11 | 12 | /* VARIABLES */ 13 | 14 | private suspenses = new Map (); 15 | 16 | /* API */ 17 | 18 | change = ( suspense: SuspenseData, nr: number ): void => { 19 | 20 | const counter = this.suspenses.get ( suspense ) || 0; 21 | const counterNext = Math.max ( 0, counter + nr ); 22 | 23 | if ( counter === counterNext ) return; 24 | 25 | if ( counterNext ) { 26 | 27 | this.suspenses.set ( suspense, counterNext ); 28 | 29 | } else { 30 | 31 | this.suspenses.delete ( suspense ); 32 | 33 | } 34 | 35 | if ( nr > 0 ) { 36 | 37 | suspense.increment ( nr ); 38 | 39 | } else { 40 | 41 | suspense.decrement ( nr ); 42 | 43 | } 44 | 45 | }; 46 | 47 | suspend = (): void => { 48 | 49 | const suspense = SuspenseContext.get (); 50 | 51 | if ( !suspense ) return; 52 | 53 | this.change ( suspense, 1 ); 54 | 55 | useCleanup ( () => { 56 | 57 | this.change ( suspense, -1 ); 58 | 59 | }); 60 | 61 | }; 62 | 63 | unsuspend = (): void => { 64 | 65 | this.suspenses.forEach ( ( counter, suspense ) => { 66 | 67 | this.change ( suspense, - counter ); 68 | 69 | }); 70 | 71 | }; 72 | 73 | }; 74 | 75 | /* EXPORT */ 76 | 77 | export default SuspenseManager; 78 | -------------------------------------------------------------------------------- /src/components/suspense.context.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_SUSPENSE, SYMBOL_SUSPENSE_COLLECTOR} from '~/constants'; 5 | import useCleanup from '~/hooks/use_cleanup'; 6 | import useMemo from '~/hooks/use_memo'; 7 | import $ from '~/methods/S'; 8 | import {context, resolve} from '~/oby'; 9 | import type {SuspenseCollectorData, SuspenseData} from '~/types'; 10 | 11 | /* MAIN */ 12 | 13 | const SuspenseContext = { 14 | 15 | create: (): SuspenseData => { 16 | 17 | const count = $(0); 18 | const active = useMemo ( () => !!count () ); 19 | const increment = ( nr: number = 1 ) => count ( prev => prev + nr ); 20 | const decrement = ( nr: number = -1 ) => queueMicrotask ( () => count ( prev => prev + nr ) ); 21 | const data = { active, increment, decrement }; 22 | 23 | const collector = context ( SYMBOL_SUSPENSE_COLLECTOR ); 24 | 25 | if ( collector ) { 26 | 27 | collector?.register ( data ); 28 | 29 | useCleanup ( () => collector.unregister ( data ) ); 30 | 31 | } 32 | 33 | return data; 34 | 35 | }, 36 | 37 | get: (): SuspenseData | undefined => { 38 | 39 | return context ( SYMBOL_SUSPENSE ); 40 | 41 | }, 42 | 43 | wrap: ( fn: ( data: SuspenseData ) => T ) => { 44 | 45 | const data = SuspenseContext.create (); 46 | 47 | return context ( { [SYMBOL_SUSPENSE]: data }, () => { 48 | 49 | return resolve ( () => fn ( data ) ); 50 | 51 | }); 52 | 53 | } 54 | 55 | }; 56 | 57 | /* EXPORT */ 58 | 59 | export default SuspenseContext; 60 | -------------------------------------------------------------------------------- /src/methods/lazy.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useMemo from '~/hooks/use_memo'; 5 | import useResolved from '~/hooks/use_resolved'; 6 | import useResource from '~/hooks/use_resource'; 7 | import creatElement from '~/methods/create_element'; 8 | import resolve from '~/methods/resolve'; 9 | import {once} from '~/utils/lang'; 10 | import type {Child, LazyFetcher, LazyResult, ObservableReadonly} from '~/types'; 11 | 12 | /* MAIN */ 13 | 14 | const lazy =

( fetcher: LazyFetcher

): LazyResult

=> { 15 | 16 | const fetcherOnce = once ( fetcher ); 17 | 18 | const component = ( props: P ): ObservableReadonly => { 19 | 20 | const resource = useResource ( fetcherOnce ); 21 | 22 | return useMemo ( () => { 23 | 24 | return useResolved ( resource, ({ pending, error, value }) => { 25 | 26 | if ( pending ) return; 27 | 28 | if ( error ) throw error; 29 | 30 | const component = ( 'default' in value ) ? value.default : value; 31 | 32 | return resolve ( creatElement

( component, props ) ); 33 | 34 | }); 35 | 36 | }); 37 | 38 | }; 39 | 40 | component.preload = (): Promise => { 41 | 42 | return new Promise ( ( resolve, reject ) => { 43 | 44 | const resource = useResource ( fetcherOnce ); 45 | 46 | useResolved ( resource, ({ pending, error }) => { 47 | 48 | if ( pending ) return; 49 | 50 | if ( error ) return reject ( error ); 51 | 52 | return resolve (); 53 | 54 | }); 55 | 56 | }); 57 | 58 | }; 59 | 60 | return component; 61 | 62 | }; 63 | 64 | /* EXPORT */ 65 | 66 | export default lazy; 67 | -------------------------------------------------------------------------------- /src/components/suspense.collector.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_SUSPENSE_COLLECTOR} from '~/constants'; 5 | import useMemo from '~/hooks/use_memo'; 6 | import $ from '~/methods/S'; 7 | import {context, resolve} from '~/oby'; 8 | import type {SuspenseCollectorData, SuspenseData} from '~/types'; 9 | 10 | /* MAIN */ 11 | 12 | // Keeping track of all Suspense instances below it, needed in some cases 13 | 14 | const SuspenseCollector = { 15 | 16 | create: (): SuspenseCollectorData => { //TODO: Optimize this, some parts are unnecessarily slow, we just need a counter of active suspenses here really 17 | 18 | const parent = SuspenseCollector.get (); 19 | const suspenses = $( [] ); 20 | const active = useMemo ( () => suspenses ().some ( suspense => suspense.active () ) ); 21 | const register = ( suspense: SuspenseData ) => { parent?.register ( suspense ); suspenses ( prev => [...prev, suspense] ); }; 22 | const unregister = ( suspense: SuspenseData ) => { parent?.unregister ( suspense ); suspenses ( prev => prev.filter ( other => other !== suspense ) ); }; 23 | const data = { suspenses, active, register, unregister }; 24 | 25 | return data; 26 | 27 | }, 28 | 29 | get: (): SuspenseCollectorData | undefined => { 30 | 31 | return context ( SYMBOL_SUSPENSE_COLLECTOR ); 32 | 33 | }, 34 | 35 | wrap: ( fn: ( data: SuspenseCollectorData ) => T ) => { 36 | 37 | const data = SuspenseCollector.create (); 38 | 39 | return context ( { [SYMBOL_SUSPENSE_COLLECTOR]: data }, () => { 40 | 41 | return resolve ( () => fn ( data ) ); 42 | 43 | }); 44 | 45 | } 46 | 47 | }; 48 | 49 | /* EXPORT */ 50 | 51 | export default SuspenseCollector; 52 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useAbortController from '~/hooks/use_abort_controller'; 5 | import useAbortSignal from '~/hooks/use_abort_signal'; 6 | import useAnimationFrame from '~/hooks/use_animation_frame'; 7 | import useAnimationLoop from '~/hooks/use_animation_loop'; 8 | import useBoolean from '~/hooks/use_boolean'; 9 | import useCleanup from '~/hooks/use_cleanup'; 10 | import useContext from '~/hooks/use_context'; 11 | import useDisposed from '~/hooks/use_disposed'; 12 | import useEventListener from '~/hooks/use_event_listener'; 13 | import useEffect from '~/hooks/use_effect'; 14 | import useFetch from '~/hooks/use_fetch'; 15 | import useIdleCallback from '~/hooks/use_idle_callback'; 16 | import useIdleLoop from '~/hooks/use_idle_loop'; 17 | import useInterval from '~/hooks/use_interval'; 18 | import useMemo from '~/hooks/use_memo'; 19 | import useMicrotask from '~/hooks/use_microtask'; 20 | import usePromise from '~/hooks/use_promise'; 21 | import useReadonly from '~/hooks/use_readonly'; 22 | import useResolved from '~/hooks/use_resolved'; 23 | import useResource from '~/hooks/use_resource'; 24 | import useRoot from '~/hooks/use_root'; 25 | import useSelector from '~/hooks/use_selector'; 26 | import useSuspended from '~/hooks/use_suspended'; 27 | import useTimeout from '~/hooks/use_timeout'; 28 | import useUntracked from '~/hooks/use_untracked'; 29 | 30 | /* EXPORT */ 31 | 32 | export {useAbortController, useAbortSignal, useAnimationFrame, useAnimationLoop, useBoolean, useCleanup, useContext, useDisposed, useEventListener, useEffect, useFetch, useIdleCallback, useIdleLoop, useInterval, useMemo, useMicrotask, usePromise, useReadonly, useResolved, useResource, useRoot, useSelector, useSuspended, useTimeout, useUntracked}; 33 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "ssr-esbuild", 4 | "type": "module", 5 | "scripts": { 6 | "clean": "rm -rf dist", 7 | "dev:build:client": "esbuild ./src/index.tsx --bundle --outfile=dist/client/index.js --watch=forever --format=esm", 8 | "dev:build:server": "esbuild ./server/index.tsx --bundle --outfile=dist/server/index.js --watch=forever --format=esm --platform=node --packages=external", 9 | "dev:build:style": "sass ./public/scss/index.scss ./public/css/index.css --watch --no-source-map", 10 | "dev:build": "scex -bs dev:build:client dev:build:server dev:build:style", 11 | "dev:start": "node ./dist/server/index.js", 12 | "dev": "monex --delay 50 --name client server style start --watch none none none server --exec npm:dev:build:client npm:dev:build:server npm:dev:build:style npm:dev:start", 13 | "prod:build:client": "esbuild ./src/index.tsx --bundle --outfile=dist/client/index.js --format=esm --minify", 14 | "prod:build:server": "esbuild ./server/index.tsx --bundle --outfile=dist/server/index.js --format=esm --platform=node --minify", 15 | "prod:build:style": "sass ./public/scss/index.scss ./public/css/index.css --no-source-map --style compressed", 16 | "prod:build": "scex -bs clean prod:build:client prod:build:server prod:build:style", 17 | "prod:start": "NODE_ENV=production node ./dist/server/index.js", 18 | "prod": "scex -bs clean prod:build:client prod:build:server prod:build:style prod:start" 19 | }, 20 | "dependencies": { 21 | "linkedom-global": "^1.0.0", 22 | "noren": "^0.4.7", 23 | "tiny-livereload": "^1.3.0", 24 | "voby": "*", 25 | "voby-simple-router": "^1.4.3" 26 | }, 27 | "devDependencies": { 28 | "esbuild": "0.20.2", 29 | "monex": "^2.2.1", 30 | "sass": "^1.72.0", 31 | "scex": "^1.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo/ssr_esbuild/server/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import 'linkedom-global'; //TODO: Delete this dependency 5 | import fs from 'node:fs'; 6 | import path from 'node:path'; 7 | import process from 'node:process'; 8 | import {favicon, serveStatic} from 'noren/middlewares'; 9 | import Server from 'noren/node'; 10 | import livereload from 'tiny-livereload/express'; 11 | import {renderToString} from 'voby'; 12 | import {useRouter} from 'voby-simple-router'; 13 | import Routes from '../src/app/routes'; 14 | import App from '../src/app'; 15 | 16 | /* HELPERS */ 17 | 18 | const INDEX_PATH = path.join ( process.cwd (), 'public', 'index.html' ); 19 | const INDEX_CONTENT = fs.readFileSync ( INDEX_PATH, 'utf8' ); 20 | const IS_PRODUCTION = ( process.env.NODE_ENV === 'production' ); 21 | 22 | /* MAIN */ 23 | 24 | const app = new Server (); 25 | const router = useRouter ( Routes ); 26 | 27 | app.use ( favicon ( './public/favicon.ico' ) ); 28 | app.use ( serveStatic ( './public' ) ); 29 | app.use ( serveStatic ( './dist/client' ) ); 30 | app.use ( livereload ( './dist/client', './public/css', 0 ) ); 31 | 32 | app.get ( '*', async ( req, res ) => { 33 | 34 | if ( router.route ( req.path ) ) { // Route found 35 | 36 | if ( IS_PRODUCTION ) { // Using SSR 37 | 38 | try { 39 | 40 | const app = await renderToString ( ); 41 | const page = INDEX_CONTENT.replace ( '

', `
${app}
` ); 42 | 43 | res.html ( page ); 44 | 45 | } catch ( error: unknown ) { 46 | 47 | res.status ( 500 ); 48 | 49 | console.error ( error ); 50 | 51 | } 52 | 53 | } else { // Not using SSR 54 | 55 | res.html ( INDEX_CONTENT ); 56 | 57 | } 58 | 59 | } else { // Route not found 60 | 61 | res.status ( 404 ); 62 | 63 | } 64 | 65 | }); 66 | 67 | app.listen ( 3000, () => { 68 | 69 | console.log ( `Listening on: http://localhost:3000` ); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /demo/creation/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$$, h, createElement, template, Dynamic} from 'voby'; 5 | 6 | /* HELPERS */ 7 | 8 | const delay = ms => { 9 | return new Promise ( resolve => { 10 | setTimeout ( resolve, ms ); 11 | }); 12 | }; 13 | 14 | /* MAIN */ 15 | 16 | const testDocumentCreateElement = () => { 17 | return document.createElement ( 'div' ); 18 | }; 19 | 20 | const testCloneNode = (() => { 21 | const node = document.createElement ( 'div' ); 22 | return () => { 23 | return node.cloneNode ( true ); 24 | }; 25 | })(); 26 | 27 | const testH = () => { 28 | return $$(h ( 'button' )); 29 | }; 30 | 31 | const testCreateElement = () => { 32 | return $$(createElement ( 'button' )); 33 | }; 34 | 35 | const testJSX = () => { 36 | return $$( 21 | ); 22 | 23 | }; 24 | 25 | const Container = ({ children }: { children: JSX.Children }): JSX.Element => { 26 | 27 | return ( 28 |
29 | {children} 30 |
31 | ); 32 | 33 | }; 34 | 35 | const HStack = ({ children }: { children: JSX.Children }): JSX.Element => { 36 | 37 | return ( 38 |
39 | {children} 40 |
41 | ); 42 | 43 | }; 44 | 45 | const VStack = ({ children }: { children: JSX.Children }): JSX.Element => { 46 | 47 | return ( 48 |
49 | {children} 50 |
51 | ); 52 | 53 | }; 54 | 55 | const Emojis = ({ value }: { value: Observable }): JSX.Element => { 56 | 57 | const value2sign = ( value: number ) => Math.sign ( value ) < 0 ? MINUS : ''; 58 | const value2chunks = ( value: number ) => ( value <= 5 ) ? [value] : [...value2chunks ( value - 5), 5]; 59 | const chunk2emoji = ( chunk: number ) => EMOJIS[chunk]; 60 | 61 | const sign = () => value2sign ( value () ); 62 | const emojis = () => value2chunks ( Math.abs ( value () ) ).map ( chunk2emoji ).join ( '' ); 63 | 64 | return ( 65 |
66 | {sign}{emojis} 67 |
68 | ); 69 | 70 | }; 71 | 72 | const EmojiCounter = (): JSX.Element => { 73 | 74 | const value = $(2); 75 | 76 | const increment = () => value ( prev => prev + 1 ); 77 | const decrement = () => value ( prev => prev - 1 ); 78 | 79 | return ( 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | 91 | }; 92 | 93 | /* RENDER */ 94 | 95 | render ( , document.getElementById ( 'app' ) ); 96 | -------------------------------------------------------------------------------- /demo/triangle/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {$, render, useInterval, useAnimationLoop, useMemo} from 'voby'; 5 | import type {Observable, ObservableReadonly} from 'voby'; 6 | 7 | /* HELPERS */ 8 | 9 | const RADIUS = 25; 10 | 11 | /* MAIN */ 12 | 13 | const useSeconds = (): Observable => { 14 | 15 | const seconds = $(0); 16 | 17 | useInterval ( () => { 18 | 19 | // Artificially long blocking delay 20 | const future = performance.now () + 0.8; 21 | while ( performance.now () < future ) {} 22 | 23 | seconds ( ( seconds () % 9 ) + 1 ); 24 | 25 | }, 1000 ); 26 | 27 | return seconds; 28 | 29 | }; 30 | 31 | const useElapsed = (): Observable => { 32 | 33 | const elapsed = $(0); 34 | const start = Date.now (); 35 | 36 | useAnimationLoop ( () => elapsed ( Date.now () - start ) ); 37 | 38 | return elapsed; 39 | 40 | }; 41 | 42 | const useScale = ( elapsed: Observable ): ObservableReadonly => { 43 | 44 | return useMemo ( () => { 45 | 46 | const e = elapsed () / 1000 % 10; 47 | 48 | return 1 + ( e > 5 ? 10 - e : e ) / 10; 49 | 50 | }); 51 | 52 | }; 53 | 54 | const Dot = ({ x, y, s, text }: { x: number, y: number, s: number, text: Observable }): JSX.Element => { 55 | 56 | const hovering = $(false); 57 | 58 | const onMouseEnter = () => hovering ( true ); 59 | const onMouseLeave = () => hovering ( false ); 60 | 61 | s = s * 1.3; 62 | 63 | const width = s; 64 | const height = s; 65 | const left = x; 66 | const top = y; 67 | const borderRadius = s / 2; 68 | const lineHeight = `${s}px`; 69 | const background = () => hovering () ? '#ffff00' : '#61dafb'; 70 | const style = { width, height, left, top, borderRadius, lineHeight, background }; 71 | 72 | return ( 73 |
74 | {() => hovering () ? `**${text ()}**` : text ()} 75 |
76 | ); 77 | 78 | }; 79 | 80 | const Triangle = ({ x, y, s, seconds }: { x: number, y: number; s: number, seconds: Observable }): JSX.Element => { 81 | 82 | if ( s <= RADIUS ) { 83 | 84 | return ; 85 | 86 | } else { 87 | 88 | s = s / 2; 89 | 90 | return ( 91 | <> 92 | 93 | 94 | 95 | 96 | ); 97 | 98 | } 99 | 100 | }; 101 | 102 | const SierpinskiTriangle = (): JSX.Element => { 103 | 104 | const seconds = useSeconds (); 105 | const elapsed = useElapsed (); 106 | const scale = useScale ( elapsed ); 107 | 108 | const transform = () => `scaleX(${scale () / 3}) scaleY(.5) translateZ(0.1px)`; 109 | 110 | return ( 111 |
112 | 113 |
114 | ); 115 | 116 | }; 117 | 118 | /* RENDER */ 119 | 120 | render ( , document.getElementById ( 'app' ) ); 121 | -------------------------------------------------------------------------------- /resources/logo/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/utils/fragment.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import type {FragmentNode, FragmentFragment, Fragment} from '~/types'; 5 | 6 | /* HELPERS */ 7 | 8 | const NOOP_CHILDREN: Node[] = []; 9 | 10 | /* MAIN */ 11 | 12 | const FragmentUtils = { 13 | 14 | make: (): Fragment => { 15 | 16 | return { 17 | values: undefined, 18 | length: 0 19 | }; 20 | 21 | }, 22 | 23 | makeWithNode: ( node: Node ): FragmentNode => { 24 | 25 | return { 26 | values: node, 27 | length: 1 28 | }; 29 | 30 | }, 31 | 32 | makeWithFragment: ( fragment: Fragment ): FragmentFragment => { 33 | 34 | return { 35 | values: fragment, 36 | fragmented: true, 37 | length: 1 38 | }; 39 | 40 | }, 41 | 42 | getChildrenFragmented: ( thiz: Fragment, children: Node[] = [] ): Node[] => { 43 | 44 | const {values, length} = thiz; 45 | 46 | if ( !length ) return children; 47 | 48 | if ( values instanceof Array ) { 49 | 50 | for ( let i = 0, l = values.length; i < l; i++ ) { 51 | 52 | const value = values[i]; 53 | 54 | if ( value instanceof Node ) { 55 | 56 | children.push ( value ); 57 | 58 | } else { 59 | 60 | FragmentUtils.getChildrenFragmented ( value, children ); 61 | 62 | } 63 | 64 | } 65 | 66 | } else { 67 | 68 | if ( values instanceof Node ) { 69 | 70 | children.push ( values ); 71 | 72 | } else { 73 | 74 | FragmentUtils.getChildrenFragmented ( values, children ); 75 | 76 | } 77 | 78 | } 79 | 80 | return children; 81 | 82 | }, 83 | 84 | getChildren: ( thiz: Fragment ): Node | Node[] => { 85 | 86 | if ( !thiz.length ) return NOOP_CHILDREN; 87 | 88 | if ( !thiz.fragmented ) return thiz.values; 89 | 90 | if ( thiz.length === 1 ) return FragmentUtils.getChildren ( thiz.values ); 91 | 92 | return FragmentUtils.getChildrenFragmented ( thiz ); 93 | 94 | }, 95 | 96 | pushFragment: ( thiz: Fragment, fragment: Fragment ): void => { 97 | 98 | FragmentUtils.pushValue ( thiz, fragment ); 99 | 100 | thiz.fragmented = true; 101 | 102 | }, 103 | 104 | pushNode: ( thiz: Fragment, node: Node ): void => { 105 | 106 | FragmentUtils.pushValue ( thiz, node ); 107 | 108 | }, 109 | 110 | pushValue: ( thiz: Fragment, value: Node | Fragment ): void => { 111 | 112 | const {values, length} = thiz as any; //TSC 113 | 114 | if ( length === 0 ) { 115 | 116 | thiz.values = value; 117 | 118 | } else if ( length === 1 ) { 119 | 120 | thiz.values = [values, value]; 121 | 122 | } else { 123 | 124 | values.push ( value ); 125 | 126 | } 127 | 128 | thiz.length += 1; 129 | 130 | }, 131 | 132 | replaceWithNode: ( thiz: Fragment, node: Node ): void => { 133 | 134 | thiz.values = node; 135 | delete thiz.fragmented; 136 | thiz.length = 1; 137 | 138 | }, 139 | 140 | replaceWithFragment: ( thiz: Fragment, fragment: Fragment ): void => { 141 | 142 | thiz.values = fragment.values; 143 | thiz.fragmented = fragment.fragmented; 144 | thiz.length = fragment.length; 145 | 146 | } 147 | 148 | }; 149 | 150 | /* EXPORT */ 151 | 152 | export default FragmentUtils; 153 | -------------------------------------------------------------------------------- /resources/logo/svg/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/logo/svg/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/logo/svg/logo-dark-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/logo/svg/logo-light-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/use_resource.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import SuspenseManager from '~/components/suspense.manager'; 5 | import useCheapDisposed from '~/hooks/use_cheap_disposed'; 6 | import useReadonly from '~/hooks/use_readonly'; 7 | import useRenderEffect from '~/hooks/use_render_effect'; 8 | import $ from '~/methods/S'; 9 | import $$ from '~/methods/SS'; 10 | import {assign, castError, isPromise} from '~/utils/lang'; 11 | import type {ObservableMaybe, PromiseMaybe, ResourceStaticPending, ResourceStaticRejected, ResourceStaticResolved, ResourceStatic, ResourceFunction, Resource} from '~/types'; 12 | 13 | /* MAIN */ 14 | 15 | //TODO: Maybe port this to oby, as "from" 16 | //TODO: Option for returning the resource as a store, where also the returned value gets wrapped in a store 17 | //FIXME: SSR demo: toggling back and forth between /home and /loader is buggy, /loader gets loaded with no data, which is wrong 18 | 19 | const useResource = ( fetcher: (() => ObservableMaybe>) ): Resource => { 20 | 21 | const pending = $(true); 22 | const error = $(); 23 | const value = $(); 24 | const latest = $(); 25 | 26 | const {suspend, unsuspend} = new SuspenseManager (); 27 | const resourcePending: ResourceStaticPending = { pending: true, get value (): undefined { return void suspend () }, get latest (): T | undefined { return latest () ?? void suspend () } }; 28 | const resourceRejected: ResourceStaticRejected = { pending: false, get error (): Error { return error ()! }, get value (): never { throw error ()! }, get latest (): never { throw error ()! } }; 29 | const resourceResolved: ResourceStaticResolved = { pending: false, get value (): T { return value ()! }, get latest (): T { return value ()! } }; 30 | const resourceFunction: ResourceFunction = { pending: () => pending (), error: () => error (), value: () => resource ().value, latest: () => resource ().latest }; 31 | const resource = $>( resourcePending ); 32 | 33 | useRenderEffect ( () => { 34 | 35 | const disposed = useCheapDisposed (); 36 | 37 | const onPending = (): void => { 38 | 39 | pending ( true ); 40 | error ( undefined ); 41 | value ( undefined ); 42 | resource ( resourcePending ); 43 | 44 | }; 45 | 46 | const onResolve = ( result: T ): void => { 47 | 48 | if ( disposed () ) return; 49 | 50 | pending ( false ); 51 | error ( undefined ); 52 | value ( () => result ); 53 | latest ( () => result ); 54 | resource ( resourceResolved ); 55 | 56 | }; 57 | 58 | const onReject = ( exception: unknown ): void => { 59 | 60 | if ( disposed () ) return; 61 | 62 | pending ( false ); 63 | error ( castError ( exception ) ); 64 | value ( undefined ); 65 | latest ( undefined ); 66 | resource ( resourceRejected ); 67 | 68 | }; 69 | 70 | const onFinally = (): void => { 71 | 72 | if ( disposed () ) return; 73 | 74 | unsuspend (); 75 | 76 | }; 77 | 78 | const fetch = (): void => { 79 | 80 | try { 81 | 82 | const value = $$(fetcher ()); 83 | 84 | if ( isPromise ( value ) ) { 85 | 86 | onPending (); 87 | 88 | value.then ( onResolve, onReject ).finally ( onFinally ); 89 | 90 | } else { 91 | 92 | onResolve ( value ); 93 | onFinally (); 94 | 95 | } 96 | 97 | } catch ( error: unknown ) { 98 | 99 | onReject ( error ); 100 | onFinally (); 101 | 102 | } 103 | 104 | }; 105 | 106 | fetch (); 107 | 108 | }); 109 | 110 | return assign ( useReadonly ( resource ), resourceFunction ); 111 | 112 | }; 113 | 114 | /* EXPORT */ 115 | 116 | export default useResource; 117 | -------------------------------------------------------------------------------- /src/methods/hmr.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import useMemo from '~/hooks/use_memo'; 5 | import $ from '~/methods/S'; 6 | import resolve from '~/methods/resolve'; 7 | import untrack from '~/methods/untrack'; 8 | import {isFunction} from '~/utils/lang'; 9 | import type {Observable, ObservableReadonly} from '~/types'; 10 | 11 | /* HELPERS */ 12 | 13 | const COMPONENT_RE = /^_?[A-Z][a-zA-Z0-9$_-]*$/; 14 | const SYMBOL_AS = '__hmr_as__'; 15 | const SYMBOL_COLD_COMPONENT = Symbol ( 'HMR.Cold' ); 16 | const SYMBOL_HOT_COMPONENT = Symbol ( 'HMR.Hot' ); 17 | const SYMBOL_HOT_ID = Symbol ( 'HMR.ID' ); 18 | const SOURCES = new WeakMap<{}, Observable>(); 19 | 20 | /* MAIN */ 21 | 22 | //TODO: This seems excessively complicated, maybe it can be simplified somewhat? 23 | //TODO: Make this work better when a nested component is added/removed too 24 | 25 | const hmr = ( accept: Function | undefined, component: T ): T => { 26 | 27 | if ( accept ) { // Making the component hot 28 | 29 | /* CHECK */ 30 | 31 | const cached = component[SYMBOL_HOT_COMPONENT]; 32 | 33 | if ( cached ) return cached; // Already hot 34 | 35 | const isProvider = !isFunction ( component ) && ( 'Provider' in component ); 36 | 37 | if ( isProvider ) return component; // Context/Directive providers are not hot-reloadable 38 | 39 | /* HELPERS */ 40 | 41 | const createHotComponent = ( path: string[] ): any => { 42 | 43 | return ( ...args: A ): ObservableReadonly => { 44 | 45 | return useMemo ( () => { 46 | 47 | const component = path.reduce ( ( component, key ) => component[key], SOURCES.get ( id () )?.() || source () ); 48 | const result = resolve ( untrack ( () => component ( ...args ) ) ); 49 | 50 | return result; 51 | 52 | }); 53 | 54 | }; 55 | 56 | }; 57 | 58 | const createHotComponentDeep = ( component: T, path: string[] ): T => { 59 | 60 | const cached = component[SYMBOL_HOT_COMPONENT]; 61 | 62 | if ( cached ) return cached; 63 | 64 | const hot = component[SYMBOL_HOT_COMPONENT] = createHotComponent ( path ); 65 | 66 | for ( const key in component ) { 67 | 68 | const value = component[key]; 69 | 70 | if ( isFunction ( value ) && COMPONENT_RE.test ( key ) ) { // A component 71 | 72 | hot[key] = createHotComponentDeep ( value, [...path, key] ); 73 | 74 | } else { // Something else 75 | 76 | hot[key] = value; 77 | 78 | } 79 | 80 | } 81 | 82 | return hot; 83 | 84 | }; 85 | 86 | const onAccept = ( module: { default: T } ): void => { 87 | 88 | const hot = module[component[SYMBOL_AS]] || module[component.name] || module.default; 89 | 90 | if ( !hot ) return console.error ( `[hmr] Failed to handle update for "${component.name}" component:\n\n`, component ); 91 | 92 | const cold = hot[SYMBOL_COLD_COMPONENT] || hot; 93 | 94 | hot[SYMBOL_HOT_ID]?.( id () ); 95 | SOURCES.get ( id () )?.( () => cold ); 96 | 97 | }; 98 | 99 | /* MAIN */ 100 | 101 | const id = $({}); 102 | const source = $(component); 103 | 104 | SOURCES.set ( id (), source ); 105 | 106 | const cold = component[SYMBOL_COLD_COMPONENT] || component; 107 | const hot = createHotComponentDeep ( component, [] ); 108 | 109 | cold[SYMBOL_HOT_COMPONENT] = hot; 110 | hot[SYMBOL_COLD_COMPONENT] = cold; 111 | hot[SYMBOL_HOT_COMPONENT] = hot; 112 | hot[SYMBOL_HOT_ID] = id; 113 | 114 | accept ( onAccept ); 115 | 116 | /* RETURN */ 117 | 118 | return hot; 119 | 120 | } else { // Returning the component as is 121 | 122 | return component; 123 | 124 | } 125 | 126 | }; 127 | 128 | /* EXPORT */ 129 | 130 | export default hmr; 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voby", 3 | "repository": "github:fabiospampinato/voby", 4 | "description": "A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.", 5 | "version": "0.58.1", 6 | "type": "module", 7 | "sideEffects": false, 8 | "main": "dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./jsx-runtime": { 16 | "import": "./dist/jsx/runtime.js", 17 | "types": "./dist/jsx/runtime.d.ts" 18 | }, 19 | "./jsx-dev-runtime": { 20 | "import": "./dist/jsx/runtime.js", 21 | "types": "./dist/jsx/runtime.d.ts" 22 | } 23 | }, 24 | "typesVersions": { 25 | "*": { 26 | "jsx-runtime": [ 27 | "./dist/jsx/runtime.d.ts" 28 | ], 29 | "jsx-dev-runtime": [ 30 | "./dist/jsx/runtime.d.ts" 31 | ] 32 | } 33 | }, 34 | "scripts": { 35 | "clean": "tsex clean", 36 | "compile": "tsex compile", 37 | "compile:watch": "tsex compile --watch", 38 | "dev:benchmark": "cd demo/benchmark && npm i && npm update && npm run dev", 39 | "prod:benchmark": "cd demo/benchmark && npm i && npm update && npm run prod", 40 | "dev:boxes": "cd demo/boxes && npm i && npm update && npm run dev", 41 | "prod:boxes": "cd demo/boxes && npm i && npm update && npm run prod", 42 | "dev:clock": "cd demo/clock && npm i && npm update && npm run dev", 43 | "prod:clock": "cd demo/clock && npm i && npm update && npm run prod", 44 | "dev:counter": "cd demo/counter && npm i && npm update && npm run dev", 45 | "prod:counter": "cd demo/counter && npm i && npm update && npm run prod", 46 | "dev:creation": "cd demo/creation && npm i && npm update && npm run dev", 47 | "prod:creation": "cd demo/creation && npm i && npm update && npm run prod", 48 | "dev:emoji_counter": "cd demo/emoji_counter && npm i && npm update && npm run dev", 49 | "prod:emoji_counter": "cd demo/emoji_counter && npm i && npm update && npm run prod", 50 | "dev:hmr": "cd demo/hmr && npm i && npm update && npm run dev", 51 | "prod:hmr": "cd demo/hmr && npm i && npm update && npm run prod", 52 | "dev:html": "cd demo/html && npm i && npm update && npm run dev", 53 | "prod:html": "cd demo/html && npm i && npm update && npm run prod", 54 | "dev:hyperscript": "cd demo/hyperscript && npm i && npm update && npm run dev", 55 | "prod:hyperscript": "cd demo/hyperscript && npm i && npm update && npm run prod", 56 | "dev:playground": "cd demo/playground && npm i && npm update && npm run dev", 57 | "prod:playground": "cd demo/playground && npm i && npm update && npm run prod", 58 | "dev:spiral": "cd demo/spiral && npm i && npm update && npm run dev", 59 | "prod:spiral": "cd demo/spiral && npm i && npm update && npm run prod", 60 | "dev:ssr_esbuild": "cd demo/ssr_esbuild && npm i && npm update && npm run dev", 61 | "prod:ssr_esbuild": "cd demo/ssr_esbuild && npm i && npm update && npm run prod", 62 | "dev:standalone": "cd demo/standalone && open index.html", 63 | "prod:standalone": "cd demo/standalone && open index.html", 64 | "dev:store_counter": "cd demo/store_counter && npm i && npm update && npm run dev", 65 | "prod:store_counter": "cd demo/store_counter && npm i && npm update && npm run prod", 66 | "dev:triangle": "cd demo/triangle && npm i && npm update && npm run dev", 67 | "prod:triangle": "cd demo/triangle && npm i && npm update && npm run prod", 68 | "dev:uibench": "cd demo/uibench && npm i && npm update && npm run dev", 69 | "prod:uibench": "cd demo/uibench && npm i && npm update && npm run prod", 70 | "dev": "npm run dev:playground", 71 | "prod": "npm run prod:playground", 72 | "prepublishOnly": "tsex prepare" 73 | }, 74 | "keywords": [ 75 | "ui", 76 | "framework", 77 | "reactive", 78 | "observable", 79 | "signal", 80 | "fast", 81 | "performant", 82 | "performance", 83 | "small", 84 | "fine-grained", 85 | "updates" 86 | ], 87 | "dependencies": { 88 | "htm": "^3.1.1", 89 | "oby": "^15.1.1" 90 | }, 91 | "devDependencies": { 92 | "@types/node": "^18.19.28", 93 | "tsex": "^3.2.1", 94 | "typescript": "^5.4.3" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/lang.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_OBSERVABLE_FROZEN, SYMBOL_OBSERVABLE_READABLE, SYMBOL_TEMPLATE_ACCESSOR, SYMBOL_UNTRACKED, SYMBOL_UNTRACKED_UNWRAPPED} from '~/constants'; 5 | import type {ComponentFunction, Falsy, TemplateActionProxy, Truthy} from '~/types'; 6 | 7 | /* MAIN */ 8 | 9 | const {assign} = Object; 10 | 11 | const castArray = ( value: T[] | T ): T[] => { 12 | 13 | return isArray ( value ) ? value : [value]; 14 | 15 | }; 16 | 17 | const castError = ( exception: unknown ): Error => { 18 | 19 | if ( isError ( exception ) ) return exception; 20 | 21 | if ( isString ( exception ) ) return new Error ( exception ); 22 | 23 | return new Error ( 'Unknown error' ); 24 | 25 | }; 26 | 27 | const flatten = ( arr: T[] ) => { 28 | 29 | for ( let i = 0, l = arr.length; i < l; i++ ) { 30 | 31 | if ( !isArray ( arr[i] ) ) continue; 32 | 33 | return arr.flat ( Infinity ); 34 | 35 | } 36 | 37 | return arr; 38 | 39 | }; 40 | 41 | const indexOf = (() => { 42 | 43 | const _indexOf = Array.prototype.indexOf; 44 | 45 | return ( arr: ArrayLike, value: T ): number => { 46 | 47 | return _indexOf.call ( arr, value ); 48 | 49 | }; 50 | 51 | })(); 52 | 53 | const {isArray} = Array; 54 | 55 | const isBoolean = ( value: unknown ): value is boolean => { 56 | 57 | return typeof value === 'boolean'; 58 | 59 | }; 60 | 61 | const isComponent = ( value: unknown ): value is ComponentFunction => { 62 | 63 | return isFunction ( value ) && ( SYMBOL_UNTRACKED_UNWRAPPED in value ); 64 | 65 | }; 66 | 67 | const isError = ( value: unknown ): value is Error => { 68 | 69 | return value instanceof Error; 70 | 71 | }; 72 | 73 | const isFalsy = ( value: T ): value is Falsy => { 74 | 75 | return !value; 76 | 77 | }; 78 | 79 | const isFunction = ( value: unknown ): value is (( ...args: any[] ) => any) => { 80 | 81 | return typeof value === 'function'; 82 | 83 | }; 84 | 85 | const isFunctionReactive = ( value: Function ): boolean => { 86 | 87 | return !( SYMBOL_UNTRACKED in value || SYMBOL_UNTRACKED_UNWRAPPED in value || SYMBOL_OBSERVABLE_FROZEN in value || value[SYMBOL_OBSERVABLE_READABLE]?.parent?.disposed ); 88 | 89 | }; 90 | 91 | const isNil = ( value: unknown ): value is null | undefined => { 92 | 93 | return value === null || value === undefined; 94 | 95 | }; 96 | 97 | const isNode = ( value: unknown ): value is Node => { 98 | 99 | return value instanceof Node; 100 | 101 | }; 102 | 103 | const isObject = ( value: unknown ): value is object => { 104 | 105 | return typeof value === 'object' && value !== null; 106 | 107 | }; 108 | 109 | const isPromise = ( value: unknown ): value is Promise => { 110 | 111 | return value instanceof Promise; 112 | 113 | }; 114 | 115 | const isString = ( value: unknown ): value is string => { 116 | 117 | return typeof value === 'string'; 118 | 119 | }; 120 | 121 | const isSVG = ( value: Element ): value is SVGElement => { 122 | 123 | return !!value['isSVG']; 124 | 125 | }; 126 | 127 | const isSVGElement = (() => { 128 | 129 | const svgRe = /^(t(ext$|s)|s[vwy]|g)|^set|tad|ker|p(at|s)|s(to|c$|ca|k)|r(ec|cl)|ew|us|f($|e|s)|cu|n[ei]|l[ty]|[GOP]/; //URL: https://regex101.com/r/Ck4kFp/1 130 | const svgCache = {}; 131 | 132 | return ( element: string ): boolean => { 133 | 134 | const cached = svgCache[element]; 135 | 136 | return ( cached !== undefined ) ? cached : ( svgCache[element] = !element.includes ( '-' ) && svgRe.test ( element ) ); 137 | 138 | }; 139 | 140 | })(); 141 | 142 | const isTemplateAccessor = ( value: unknown ): value is TemplateActionProxy => { 143 | 144 | return isFunction ( value ) && ( SYMBOL_TEMPLATE_ACCESSOR in value ); 145 | 146 | }; 147 | 148 | const isTruthy = ( value: T ): value is Truthy => { 149 | 150 | return !!value; 151 | 152 | }; 153 | 154 | const isVoidChild = ( value: unknown ): value is null | undefined | symbol | boolean => { 155 | 156 | return value === null || value === undefined || typeof value === 'boolean' || typeof value === 'symbol'; 157 | 158 | }; 159 | 160 | const noop = (): void => { 161 | 162 | return; 163 | 164 | }; 165 | 166 | const once = ( fn: () => T ): (() => T) => { 167 | 168 | let called = false; 169 | let result: T; 170 | 171 | return (): T => { 172 | 173 | if ( !called ) { 174 | 175 | called = true; 176 | result = fn (); 177 | 178 | } 179 | 180 | return result; 181 | 182 | }; 183 | 184 | }; 185 | 186 | /* EXPORT */ 187 | 188 | export {assign, castArray, castError, flatten, indexOf, isArray, isBoolean, isComponent, isError, isFalsy, isFunction, isFunctionReactive, isNil, isNode, isObject, isPromise, isString, isSVG, isSVGElement, isTemplateAccessor, isTruthy, isVoidChild, noop, once}; 189 | -------------------------------------------------------------------------------- /src/utils/htm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | // @ts-nocheck //TODO 15 | 16 | /* MAIN */ 17 | 18 | // This is just a slightly customized version of htm: dynamic props are marked as such, indexes of dynamics are remembered //TODO 19 | 20 | const MODE_SLASH = 0; 21 | const MODE_TEXT = 1; 22 | const MODE_WHITESPACE = 2; 23 | const MODE_TAGNAME = 3; 24 | const MODE_COMMENT = 4; 25 | const MODE_PROP_SET = 5; 26 | const MODE_PROP_APPEND = 6; 27 | 28 | const htm = function(statics) { 29 | const fields = arguments; 30 | const h = this; 31 | 32 | let mode = MODE_TEXT; 33 | let buffer = ''; 34 | let quote = ''; 35 | let current = [0]; 36 | let char, propName; 37 | 38 | const commit = (field?) => { 39 | if (mode === MODE_TEXT && (field || (buffer = buffer.replace(/^\s*\n\s*|\s*\n\s*$/g,'')))) { 40 | current.push(field ? fields[field] : buffer); 41 | } 42 | else if (mode === MODE_TAGNAME && (field || buffer)) { 43 | current[1] = field ? fields[field] : buffer; 44 | mode = MODE_WHITESPACE; 45 | } 46 | else if (mode === MODE_WHITESPACE && buffer === '...' && field) { 47 | current[2] = Object.assign(current[2] || {}, fields[field]); 48 | } 49 | else if (mode === MODE_WHITESPACE && buffer && !field) { 50 | (current[2] = current[2] || {})[buffer] = true; 51 | } 52 | else if (mode >= MODE_PROP_SET) { 53 | const prefix = field ? '$' : ''; 54 | if (mode === MODE_PROP_SET) { 55 | (current[2] = current[2] || {})[prefix + propName] = field ? buffer ? (buffer + fields[field]) : fields[field] : buffer; 56 | mode = MODE_PROP_APPEND; 57 | } 58 | else if (field || buffer) { 59 | current[2][prefix + propName] += field ? buffer + fields[field] : buffer; 60 | } 61 | } 62 | 63 | buffer = ''; 64 | }; 65 | 66 | for (let i=0; i' 90 | if (buffer === '--' && char === '>') { 91 | mode = MODE_TEXT; 92 | buffer = ''; 93 | } 94 | else { 95 | buffer = char + buffer[0]; 96 | } 97 | } 98 | else if (quote) { 99 | if (char === quote) { 100 | quote = ''; 101 | } 102 | else { 103 | buffer += char; 104 | } 105 | } 106 | else if (char === '"' || char === "'") { 107 | quote = char; 108 | } 109 | else if (char === '>') { 110 | commit(); 111 | mode = MODE_TEXT; 112 | } 113 | else if (!mode) { 114 | // Ignore everything until the tag ends 115 | } 116 | else if (char === '=') { 117 | mode = MODE_PROP_SET; 118 | propName = buffer; 119 | buffer = ''; 120 | } 121 | else if (char === '/' && (mode < MODE_PROP_SET || statics[i][j+1] === '>')) { 122 | commit(); 123 | if (mode === MODE_TAGNAME) { 124 | current = current[0]; 125 | } 126 | mode = current; 127 | (current = current[0]).push(h.apply(null, mode.slice(1))); 128 | mode = MODE_SLASH; 129 | } 130 | else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') { 131 | // 132 | commit(); 133 | mode = MODE_WHITESPACE; 134 | } 135 | else { 136 | buffer += char; 137 | } 138 | 139 | if (mode === MODE_TAGNAME && buffer === '!--') { 140 | mode = MODE_COMMENT; 141 | current = current[0]; 142 | } 143 | } 144 | } 145 | commit(); 146 | 147 | return current.length > 2 ? current.slice(1) : current[1]; 148 | }; 149 | 150 | /* EXPORT */ 151 | 152 | export default htm; 153 | -------------------------------------------------------------------------------- /src/utils/resolvers.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {SYMBOL_UNCACHED} from '~/constants'; 5 | import isObservable from '~/methods/is_observable'; 6 | import useRenderEffect from '~/hooks/use_render_effect'; 7 | import $$ from '~/methods/SS'; 8 | import {createText} from '~/utils/creators'; 9 | import {isArray, isFunction, isFunctionReactive, isString} from '~/utils/lang'; 10 | import type {Classes, ObservableMaybe, Styles} from '~/types'; 11 | 12 | /* MAIN */ 13 | 14 | const resolveChild = ( value: ObservableMaybe, setter: (( value: T | T[], dynamic: boolean ) => void), _dynamic: boolean = false ): void => { 15 | 16 | if ( isFunction ( value ) ) { 17 | 18 | if ( !isFunctionReactive ( value ) ) { 19 | 20 | resolveChild ( value (), setter, _dynamic ); 21 | 22 | } else { 23 | 24 | useRenderEffect ( () => { 25 | 26 | resolveChild ( value (), setter, true ); 27 | 28 | }); 29 | 30 | } 31 | 32 | } else if ( isArray ( value ) ) { 33 | 34 | const [values, hasObservables] = resolveArraysAndStatics ( value ); 35 | 36 | values[SYMBOL_UNCACHED] = value[SYMBOL_UNCACHED]; // Preserving this special symbol 37 | 38 | setter ( values, hasObservables || _dynamic ); 39 | 40 | } else { 41 | 42 | setter ( value, _dynamic ); 43 | 44 | } 45 | 46 | }; 47 | 48 | const resolveClass = ( classes: Classes, resolved: Record = {} ): Record => { 49 | 50 | if ( isString ( classes ) ) { 51 | 52 | classes.split ( /\s+/g ).filter ( Boolean ).filter ( cls => { 53 | 54 | resolved[cls] = true; 55 | 56 | }); 57 | 58 | } else if ( isFunction ( classes ) ) { 59 | 60 | resolveClass ( classes (), resolved ); 61 | 62 | } else if ( isArray ( classes ) ) { 63 | 64 | classes.forEach ( cls => { 65 | 66 | resolveClass ( cls as Classes, resolved ); //TSC 67 | 68 | }); 69 | 70 | } else if ( classes ) { 71 | 72 | for ( const key in classes ) { 73 | 74 | const value = classes[key]; 75 | const isActive = !!$$(value); 76 | 77 | if ( !isActive ) continue; 78 | 79 | resolved[key] = true; 80 | 81 | } 82 | 83 | } 84 | 85 | return resolved; 86 | 87 | }; 88 | 89 | const resolveStyle = ( styles: Styles, resolved: Record | string = {} ): Record | string => { 90 | 91 | if ( isString ( styles ) ) { //TODO: split into the individual styles, to be able to merge them with other styles 92 | 93 | return styles; 94 | 95 | } else if ( isFunction ( styles ) ) { 96 | 97 | return resolveStyle ( styles (), resolved ); 98 | 99 | } else if ( isArray ( styles ) ) { 100 | 101 | styles.forEach ( style => { 102 | 103 | resolveStyle ( style as Styles, resolved ); //TSC 104 | 105 | }); 106 | 107 | } else if ( styles ) { 108 | 109 | for ( const key in styles ) { 110 | 111 | const value = styles[key]; 112 | 113 | resolved[key] = $$(value); 114 | 115 | } 116 | 117 | } 118 | 119 | return resolved; 120 | 121 | }; 122 | 123 | const resolveArraysAndStatics = (() => { 124 | 125 | // This function does 3 things: 126 | // 1. It deeply flattens the array, only if actually needed though (!) 127 | // 2. It resolves statics, it's important to resolve them soon enough or they will be re-created multiple times (!) 128 | // 3. It checks if we found any Observables along the way, avoiding looping over the array another time in the future 129 | 130 | const DUMMY_RESOLVED = []; 131 | 132 | const resolveArraysAndStaticsInner = ( values: any[], resolved: any[], hasObservables: boolean ): [any[], boolean] => { 133 | 134 | for ( let i = 0, l = values.length; i < l; i++ ) { 135 | 136 | const value = values[i]; 137 | const type = typeof value; 138 | 139 | if ( type === 'string' || type === 'number' || type === 'bigint' ) { // Static 140 | 141 | if ( resolved === DUMMY_RESOLVED ) resolved = values.slice ( 0, i ); 142 | 143 | resolved.push ( createText ( value ) ); 144 | 145 | } else if ( type === 'object' && isArray ( value ) ) { // Array 146 | 147 | if ( resolved === DUMMY_RESOLVED ) resolved = values.slice ( 0, i ); 148 | 149 | hasObservables = resolveArraysAndStaticsInner ( value, resolved, hasObservables )[1]; 150 | 151 | } else if ( type === 'function' && isObservable ( value ) ) { // Observable 152 | 153 | if ( resolved !== DUMMY_RESOLVED ) resolved.push ( value ); 154 | 155 | hasObservables = true; 156 | 157 | } else { // Something else 158 | 159 | if ( resolved !== DUMMY_RESOLVED ) resolved.push ( value ); 160 | 161 | } 162 | 163 | } 164 | 165 | if ( resolved === DUMMY_RESOLVED ) resolved = values; 166 | 167 | return [resolved, hasObservables]; 168 | 169 | }; 170 | 171 | return ( values: any[] ): [any[], boolean] => { 172 | 173 | return resolveArraysAndStaticsInner ( values, DUMMY_RESOLVED, false ); 174 | 175 | }; 176 | 177 | })(); 178 | 179 | /* EXPORT */ 180 | 181 | export {resolveChild, resolveClass, resolveStyle, resolveArraysAndStatics}; 182 | -------------------------------------------------------------------------------- /demo/uibench/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {render, store, tick, For, Switch, Ternary} from 'voby'; 5 | 6 | /* TYPES */ 7 | 8 | type AnimItem = { 9 | id: number, 10 | time: number 11 | }; 12 | 13 | type AnimState = { 14 | items: AnimItem[] 15 | }; 16 | 17 | type TableItem = { 18 | id: number, 19 | active: boolean, 20 | props: string[] 21 | }; 22 | 23 | type TableState = { 24 | items: TableItem[] 25 | }; 26 | 27 | type TreeItem = { 28 | id: number, 29 | container: boolean, 30 | children: TreeItem[] 31 | }; 32 | 33 | type TreeState = { 34 | root: TreeItem 35 | }; 36 | 37 | type State = { 38 | location: 'anim' | 'table' | 'tree' | 'unknown', 39 | anim: AnimState, 40 | table: TableState, 41 | tree: TreeState 42 | }; 43 | 44 | type Results = Record; 45 | 46 | /* STATE */ 47 | 48 | const state = store({ 49 | location: 'unknown', 50 | anim: { 51 | items: [] 52 | }, 53 | table: { 54 | items: [] 55 | }, 56 | tree: { 57 | root: { 58 | id: 0, 59 | container: true, 60 | children: [] 61 | } 62 | } 63 | }); 64 | 65 | /* MAIN */ 66 | 67 | const Anim = ({ state }: { state: AnimState }): JSX.Element => { 68 | 69 | return ( 70 |
71 | 72 | {item => { 73 | const id = () => item ().id; 74 | const borderRadius = () => item ().time % 10; 75 | const background = () => `rgba(0,0,0,${0.5 + ( ( item ().time % 10 ) / 10 )})`; 76 | return
; 77 | }} 78 | 79 |
80 | ); 81 | 82 | }; 83 | 84 | const Table = ({ state }: { state: TableState }): JSX.Element => { 85 | 86 | const onClick = ( event: MouseEvent ): void => { 87 | console.log ( 'Clicked' + event.target?.textContent ); 88 | event.stopPropagation (); 89 | }; 90 | 91 | return ( 92 | 93 | 94 | 95 | {item => { 96 | const id = () => item ().id; 97 | const className = () => item ().active ? 'TableRow active' : 'TableRow'; 98 | const content = () => `#${item ().id}`; 99 | return ( 100 | 101 | 104 | item ().props} unkeyed> 105 | {text => ( 106 | 109 | )} 110 | 111 | 112 | ); 113 | }} 114 | 115 | 116 |
102 | {content} 103 | 107 | {text} 108 |
117 | ); 118 | 119 | }; 120 | 121 | const TreeNode = ({ item }: { item: () => TreeItem }): JSX.Element => { 122 | 123 | return ( 124 |
    125 | item ().children} unkeyed> 126 | {item => ( 127 | item ().container}> 128 | 129 | 130 | 131 | )} 132 | 133 |
134 | ); 135 | 136 | }; 137 | 138 | const TreeLeaf = ({ item }: { item: () => TreeItem }): JSX.Element => { 139 | 140 | return ( 141 |
  • 142 | {() => item ().id} 143 |
  • 144 | ); 145 | 146 | }; 147 | 148 | const Tree = ({ state }: { state: TreeState }): JSX.Element => { 149 | 150 | return ( 151 |
    152 | state.root} /> 153 |
    154 | ); 155 | 156 | }; 157 | 158 | const App = ({ state }: { state: State }): JSX.Element => { 159 | 160 | return ( 161 |
    162 | state.location}> 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | ); 175 | 176 | }; 177 | 178 | const Results = ({ results }: { results: Results }): JSX.Element => { 179 | 180 | const elapsed = Object.values ( results ).flat ().reduce ( ( acc, elapsed ) => acc + elapsed, 0 ); 181 | 182 | console.log ( elapsed ); 183 | 184 | return ( 185 |
    186 |       {JSON.stringify ( results, undefined, 2 )}
    187 |     
    188 | ); 189 | 190 | }; 191 | 192 | /* RENDER */ 193 | 194 | render ( , document.body ); 195 | 196 | /* UI BENCH */ 197 | 198 | const normalize = value => ({ // Removing major custom classes, which won't be reconciled properly 199 | location: value.location, 200 | anim: { 201 | items: value.anim.items 202 | }, 203 | table: { 204 | items: value.table.items 205 | }, 206 | tree: { 207 | root: value.tree.root 208 | } 209 | }); 210 | 211 | const onUpdate = stateNext => { 212 | store.reconcile ( state, normalize ( stateNext ) ); 213 | tick (); 214 | }; 215 | 216 | const onFinish = results => { 217 | render ( , document.body ); 218 | }; 219 | 220 | globalThis.uibench.init ( 'Voby', '*' ); 221 | globalThis.uibench.run ( onUpdate, onFinish ); 222 | -------------------------------------------------------------------------------- /demo/boxes/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {BoxBufferGeometry} from 'three/src/geometries/BoxGeometry'; 5 | import {DirectionalLight} from 'three/src/lights/DirectionalLight'; 6 | import {Mesh} from 'three/src/objects/Mesh'; 7 | import {MeshNormalMaterial} from 'three/src/materials/MeshNormalMaterial'; 8 | import {PerspectiveCamera} from 'three/src/cameras/PerspectiveCamera'; 9 | import {Scene} from 'three/src/scenes/Scene'; 10 | import {WebGLRenderer} from 'three/src/renderers/WebGLRenderer'; 11 | 12 | import {$, render, useAnimationLoop, useEffect, useMemo} from 'voby'; 13 | import type {Observable, ObservableReadonly} from 'voby'; 14 | 15 | /* TYPES */ 16 | 17 | type Rotation = Observable<[number, number, number]>; 18 | 19 | /* HELPERS */ 20 | 21 | const COUNT_INITIAL = 100; 22 | const COUNT_MIN = 1; 23 | const COUNT_MAX = 10000; 24 | const SPEED = 0.01; 25 | 26 | /* MAIN */ 27 | 28 | const useIdleInput = ( callback: (( event: Event ) => void) ) => { 29 | 30 | let pending = false; 31 | 32 | return ( event: Event ): void => { 33 | 34 | if ( pending ) return; 35 | 36 | pending = true; 37 | 38 | setTimeout ( () => { 39 | 40 | pending = false; 41 | 42 | callback ( event ); 43 | 44 | }, 50 ); 45 | 46 | }; 47 | 48 | }; 49 | 50 | const useRotations = ( count: Observable ): ObservableReadonly => { 51 | 52 | const getRandom = (): number => Math.random () * 360; 53 | const getRotation = (): Rotation => $([ getRandom (), getRandom (), getRandom () ]); 54 | const rotations = useMemo ( () => Array ( count () ).fill ( 0 ).map ( getRotation ) ); 55 | 56 | useAnimationLoop ( () => { 57 | 58 | rotations ().forEach ( rotation => { 59 | 60 | const [x, y, z] = rotation (); 61 | 62 | rotation ([ x + SPEED, y + SPEED, z + SPEED ]); 63 | 64 | }); 65 | 66 | }); 67 | 68 | return rotations; 69 | 70 | }; 71 | 72 | const ThreeScene = ( camera: PerspectiveCamera, light: DirectionalLight, meshes: ObservableReadonly ): HTMLCanvasElement => { 73 | 74 | const scene = new Scene (); 75 | 76 | scene.add ( light ); 77 | 78 | const renderer = new WebGLRenderer ({ antialias: true }); 79 | 80 | renderer.setPixelRatio ( window.devicePixelRatio ); 81 | renderer.setClearColor ( 0xffffff ); 82 | 83 | useEffect ( () => { 84 | 85 | scene.remove.apply ( scene, scene.children.slice ( 2 ) ); 86 | 87 | meshes ().forEach ( mesh => scene.add ( mesh ) ); 88 | 89 | }); 90 | 91 | useAnimationLoop ( () => { 92 | 93 | renderer.render ( scene, camera ); 94 | 95 | }); 96 | 97 | useEffect ( () => { 98 | 99 | const onResize = () => { 100 | 101 | camera.aspect = window.innerWidth / window.innerHeight; 102 | camera.updateProjectionMatrix (); 103 | 104 | renderer.setSize ( window.innerWidth, window.innerHeight ); 105 | 106 | }; 107 | 108 | onResize (); 109 | 110 | window.addEventListener ( 'resize', onResize ); 111 | 112 | return () => { 113 | 114 | window.removeEventListener ( 'resize', onResize ); 115 | 116 | }; 117 | 118 | }); 119 | 120 | return renderer.domElement; 121 | 122 | }; 123 | 124 | const ThreePerspectiveCamera = ( location: [number, number, number] ): PerspectiveCamera => { 125 | 126 | const aspect = window.innerWidth / window.innerHeight; 127 | 128 | const camera = new PerspectiveCamera ( 106, aspect, 1, 1000 ); 129 | 130 | camera.position.set ( ...location ); 131 | 132 | return camera; 133 | 134 | }; 135 | 136 | const ThreeDirectionalLight = ( direction: [number, number, number] ): DirectionalLight => { 137 | 138 | const light = new DirectionalLight ( 0x000000 ); 139 | 140 | light.position.set ( ...direction ); 141 | 142 | return light; 143 | 144 | }; 145 | 146 | const ThreeMesh = ( rotation: Rotation ): Mesh => { 147 | 148 | const material = new MeshNormalMaterial (); 149 | const geometry = new BoxBufferGeometry ( 2, 2, 2 ); 150 | const mesh = new Mesh ( geometry, material ); 151 | 152 | useEffect ( () => { 153 | 154 | mesh.rotation.set ( ...rotation () ); 155 | 156 | }); 157 | 158 | return mesh; 159 | 160 | }; 161 | 162 | const Controls = ({ count, onInput }: { count: Observable, onInput: (( event: Event ) => void) }): JSX.Element => { 163 | 164 | return ( 165 |
    166 | 167 | 168 |
    169 | ); 170 | 171 | }; 172 | 173 | const Rotations = ({ rotations }: { rotations: ObservableReadonly }): JSX.Element => { 174 | 175 | const camera = ThreePerspectiveCamera ([ 0, 0, 3.2 ]); 176 | const light = ThreeDirectionalLight ([ -5, 0, -10 ]); 177 | const meshes = useMemo ( () => rotations ().map ( ThreeMesh ) ); 178 | const scene = ThreeScene ( camera, light, meshes ); 179 | 180 | return ( 181 |
    182 | {scene} 183 |
    184 | ); 185 | 186 | }; 187 | 188 | const App = (): JSX.Element => { 189 | 190 | const count = $(COUNT_INITIAL); 191 | const rotations = useRotations ( count ); 192 | 193 | const onInput = useIdleInput ( event => { 194 | 195 | count ( parseInt ( event.target.value ) ); 196 | 197 | }); 198 | 199 | return ( 200 |
    201 | 202 | 203 |
    204 | ); 205 | 206 | }; 207 | 208 | /* RENDER */ 209 | 210 | render ( , document.getElementById ( 'app' ) ); 211 | -------------------------------------------------------------------------------- /demo/benchmark/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {createElement, Fragment} from 'voby'; 5 | import {$, render, template, useSelector, For} from 'voby'; 6 | import type {FunctionMaybe, Observable, ObservableMaybe} from 'voby'; 7 | 8 | /* TYPES */ 9 | 10 | type IDatum = { 11 | id: number, 12 | label: Observable 13 | }; 14 | 15 | /* HELPERS */ 16 | 17 | const rand = ( max: number ): number => { 18 | return Math.round ( Math.random () * 1000 ) % max; 19 | }; 20 | 21 | const buildData = (() => { 22 | const adjectives = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', 'cheap', 'expensive', 'fancy']; 23 | const colors = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange']; 24 | const nouns = ['table', 'chair', 'house', 'bbq', 'desk', 'car', 'pony', 'cookie', 'sandwich', 'burger', 'pizza', 'mouse', 'keyboard']; 25 | let uuid = 1; 26 | return ( length: number ): IDatum[] => { 27 | const data: IDatum[] = new Array ( length ); 28 | for ( let i = 0; i < length; i++ ) { 29 | const id = uuid++; 30 | const adjective = adjectives[rand ( adjectives.length )]; 31 | const color = colors[rand ( colors.length )]; 32 | const noun = nouns[rand ( nouns.length )]; 33 | const label = $(`${adjective} ${color} ${noun}`); 34 | const datum = { id, label }; 35 | data[i] = datum; 36 | }; 37 | return data; 38 | }; 39 | })(); 40 | 41 | /* MODEL */ 42 | 43 | const Model = new class { 44 | 45 | /* STATE */ 46 | 47 | data: Observable = $( [] ); 48 | selected: Observable = $( -1 ); 49 | 50 | /* API */ 51 | 52 | run0 = (): void => { 53 | this.runWith ( 0 ); 54 | }; 55 | 56 | run1000 = (): void => { 57 | this.runWith ( 1000 ); 58 | }; 59 | 60 | run10000 = (): void => { 61 | this.runWith ( 10000 ); 62 | }; 63 | 64 | runWith = ( length: number ): void => { 65 | this.data ( buildData ( length ) ); 66 | }; 67 | 68 | add = (): void => { 69 | this.data ( data => [...data, ...buildData ( 1000 )] ); 70 | }; 71 | 72 | update = (): void => { 73 | const data = this.data (); 74 | for ( let i = 0, l = data.length; i < l; i += 10 ) { 75 | data[i].label ( label => label + ' !!!' ); 76 | } 77 | }; 78 | 79 | swapRows = (): void => { 80 | const data = this.data ().slice (); 81 | if ( data.length <= 998 ) return; 82 | const datum1 = data[1]; 83 | const datum998 = data[998]; 84 | data[1] = datum998; 85 | data[998] = datum1; 86 | this.data ( data ); 87 | }; 88 | 89 | remove = ( id: number ): void => { 90 | this.data ( data => { 91 | const idx = data.findIndex ( datum => datum.id === id ); 92 | return [...data.slice ( 0, idx ), ...data.slice ( idx + 1 )]; 93 | }); 94 | }; 95 | 96 | select = ( id: number ): void => { 97 | this.selected ( id ); 98 | }; 99 | 100 | }; 101 | 102 | /* COMPONENTS */ 103 | 104 | const Button = ({ id, text, onClick }: { id: FunctionMaybe, text: FunctionMaybe, onClick: ObservableMaybe<(( event: MouseEvent ) => void)> }): JSX.Element => ( 105 |
    106 | 109 |
    110 | ); 111 | 112 | const Row = template (({ id, label, className, onSelect, onRemove }: { id: FunctionMaybe, label: FunctionMaybe, className: FunctionMaybe>>, onSelect: ObservableMaybe<(( event: MouseEvent ) => void)>, onRemove: ObservableMaybe<(( event: MouseEvent ) => void)> }): JSX.Element => ( 113 |
    114 | 117 | 122 | 127 | 129 | )); 130 | 131 | const Rows = ({ data, isSelected }: { data: FunctionMaybe, isSelected: ( id: number ) => FunctionMaybe }): JSX.Element => ( 132 | 133 | {( datum: IDatum ) => { 134 | const {id, label} = datum; 135 | const selected = isSelected ( id ); 136 | const className = { danger: selected }; 137 | const onSelect = () => Model.select ( id ); 138 | const onRemove = () => Model.remove ( id ); 139 | const props = {id, label, className, onSelect, onRemove}; 140 | return Row ( props ); 141 | }} 142 | 143 | // 144 | // {( datum: () => IDatum ) => { 145 | // const id = () => datum ().id; 146 | // const label = () => datum ().label (); 147 | // const selected = () => Model.selected () === id (); 148 | // const className = { danger: selected }; 149 | // const onSelect = () => Model.select ( id () ); 150 | // const onRemove = () => Model.remove ( id () ); 151 | // const props = {id, label, className, onSelect, onRemove}; 152 | // return Row ( props ); 153 | // }} 154 | // 155 | ); 156 | 157 | const App = (): JSX.Element => ( 158 |
    159 |
    160 |
    161 |
    162 |

    Voby

    163 |
    164 |
    165 |
    166 |
    173 |
    174 |
    175 |
    176 |
    115 | {id} 116 | 118 | 119 | {label} 120 | 121 | 123 | 124 | 125 | 126 | 128 |
    177 | 178 | 179 | 180 |
    181 | 182 |
    183 | ); 184 | 185 | /* RENDER */ 186 | 187 | render ( , document.getElementById ( 'app' ) ); 188 | -------------------------------------------------------------------------------- /resources/banner/svg/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/banner/svg/banner-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/banner/svg/banner-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/banner/svg/banner-light-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------