): 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 $$();
37 | };
38 |
39 | const testDynamic = () => {
40 | return $$($$());
41 | };
42 |
43 | const testDynamicRaw = () => {
44 | return $$(Dynamic ({ component: 'button' }));
45 | };
46 |
47 | const testTemplate = (() => {
48 | const PROPS = {};
49 | const tmpl = template (() => {
50 | return h ( 'button' );
51 | });
52 | return () => {
53 | return $$(tmpl ( PROPS ));
54 | };
55 | })();
56 |
57 | const test = async () => {
58 |
59 | const tests: [string, Function][] = [
60 | ['document.createElement', testDocumentCreateElement],
61 | ['cloneNode', testCloneNode],
62 | ['h', testH],
63 | ['createElement', testCreateElement],
64 | ['jsx', testJSX],
65 | ['dynamic', testDynamic],
66 | ['dynamic.raw', testDynamicRaw],
67 | ['template', testTemplate]
68 | ];
69 |
70 | console.time ( 'total' );
71 |
72 | for ( const [name, fn] of tests ) {
73 |
74 | console.time ( name );
75 |
76 | for ( let i = 0; i < 1_000_000; i++ ) {
77 | fn ();
78 | }
79 |
80 | console.timeEnd ( name );
81 |
82 | // await delay ( 500 );
83 |
84 | }
85 |
86 | console.timeEnd ( 'total' );
87 |
88 | };
89 |
90 | test ();
91 |
--------------------------------------------------------------------------------
/src/methods/create_element.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import untrack from '~/methods/untrack';
5 | import wrapElement from '~/methods/wrap_element';
6 | import {createHTMLNode, createSVGNode} from '~/utils/creators';
7 | import {isFunction, isNode, isObject, isString, isSVGElement, isVoidChild} from '~/utils/lang';
8 | import {setChild, setProps} from '~/utils/setters';
9 | import type {Child, Component, Element} from '~/types';
10 |
11 | /* MAIN */
12 |
13 | // It's important to wrap components, so that they can be executed in the right order, from parent to child, rather than from child to parent in some cases
14 |
15 | const createElement = ( component: Component
, _props?: P | null, ..._children: Child[] ): Element => {
16 |
17 | const children = _children.length > 1 ? _children : ( _children.length > 0 ? _children[0] : undefined );
18 | const hasChildren = !isVoidChild ( children );
19 |
20 | if ( hasChildren && isObject ( _props ) && 'children' in _props ) {
21 |
22 | throw new Error ( 'Providing "children" both as a prop and as rest arguments is forbidden' );
23 |
24 | }
25 |
26 | if ( isFunction ( component ) ) {
27 |
28 | const props = hasChildren ? { ..._props, children } : _props;
29 |
30 | return wrapElement ( () => {
31 |
32 | return untrack ( () => component.call ( component, props as P ) ); //TSC
33 |
34 | });
35 |
36 | } else if ( isString ( component ) ) {
37 |
38 | const isSVG = isSVGElement ( component );
39 | const createNode = isSVG ? createSVGNode : createHTMLNode;
40 |
41 | return wrapElement ( (): Child => {
42 |
43 | const child = createNode ( component ) as HTMLElement; //TSC
44 |
45 | if ( isSVG ) child['isSVG'] = true;
46 |
47 | untrack ( () => {
48 |
49 | if ( _props ) {
50 | setProps ( child, _props );
51 | }
52 |
53 | if ( hasChildren ) {
54 | setChild ( child, children );
55 | }
56 |
57 | });
58 |
59 | return child;
60 |
61 | });
62 |
63 | } else if ( isNode ( component ) ) {
64 |
65 | return wrapElement ( () => component );
66 |
67 | } else {
68 |
69 | throw new Error ( 'Invalid component' );
70 |
71 | }
72 |
73 | };
74 |
75 | /* EXPORT */
76 |
77 | export default createElement;
78 |
--------------------------------------------------------------------------------
/demo/clock/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {$, render, useAnimationLoop} from 'voby';
5 | import type {Observable} from 'voby';
6 |
7 | /* HELPERS */
8 |
9 | const mapRange = ( start: number, end: number, increment: number, callback: (( nr: number ) => T) ): T[] => {
10 |
11 | const results: T[] = [];
12 |
13 | for ( let i = start; i < end; i += increment ) {
14 | results.push ( callback ( i ) );
15 | }
16 |
17 | return results;
18 |
19 | };
20 |
21 | const getMillisecondsSinceMidnight = (): number => {
22 |
23 | const now = Date.now ();
24 | const midnight = new Date ().setHours ( 0, 0, 0, 0 );
25 |
26 | return now - midnight;
27 |
28 | };
29 |
30 | /* MAIN */
31 |
32 | const useTime = () => {
33 |
34 | const time = $( getMillisecondsSinceMidnight () / 1000 );
35 | const tick = () => time ( getMillisecondsSinceMidnight () / 1000 );
36 |
37 | useAnimationLoop ( tick );
38 |
39 | return time;
40 |
41 | };
42 |
43 | const ClockFace = ({ time }: { time: Observable }): JSX.Element => {
44 |
45 | const abstract = ( rotate: number ) => `rotate(${(rotate * 360).toFixed ( 1 )})`;
46 | const millisecond = () => abstract ( time () % 1 );
47 | const second = () => abstract ( ( time () % 60 ) / 60 );
48 | const minute = () => abstract ( ( time () / 60 % 60 ) / 60 );
49 | const hour = () => abstract ( ( time () / 60 / 60 % 12 ) / 12 );
50 |
51 | return (
52 |
67 | );
68 |
69 | };
70 |
71 | const Clock = (): JSX.Element => {
72 |
73 | const time = useTime ();
74 |
75 | return (
76 |
77 |
78 |
79 | );
80 |
81 | };
82 |
83 | /* RENDER */
84 |
85 | render ( , document.getElementById ( 'app' ) );
86 |
--------------------------------------------------------------------------------
/demo/spiral/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {$, render, untrack, For, If, useAnimationLoop, useMemo} from 'voby';
5 | import type {Observable, ObservableReadonly} from 'voby';
6 |
7 | /* HELPERS */
8 |
9 | const COUNT = 400;
10 | const LOOPS = 6;
11 |
12 | /* MAIN */
13 |
14 | const Cursor = ({ big, label, x, y, color }: { big: Observable, label: boolean, x: ObservableReadonly, y: ObservableReadonly, color?: ObservableReadonly }): JSX.Element => {
15 |
16 | return (
17 |
18 |
19 | {x},{y}
20 |
21 |
22 | );
23 |
24 | };
25 |
26 | const Spiral = (): JSX.Element => {
27 |
28 | const x = $(0);
29 | const y = $(0);
30 | const big = $(false);
31 | const counter = $(0);
32 |
33 | window.addEventListener ( 'mousemove', ({ pageX, pageY }) => {
34 | x ( pageX );
35 | y ( pageY );
36 | });
37 |
38 | window.addEventListener ( 'mousedown', () => {
39 | big ( true );
40 | });
41 |
42 | window.addEventListener ( 'mouseup', () => {
43 | big ( false );
44 | });
45 |
46 | useAnimationLoop ( () => counter ( counter () + 1 ) );
47 |
48 | const max = useMemo ( () => COUNT + Math.round ( Math.sin ( counter () / 90 * 2 * Math.PI ) * COUNT * 0.5 ) );
49 |
50 | const makeCursor = ( i: number ) => ({
51 | x: (): number => {
52 | const f = i / max () * LOOPS;
53 | const θ = f * 2 * Math.PI;
54 | const m = 20 + i;
55 | return (untrack ( x ) + Math.sin ( θ ) * m) | 0;
56 | },
57 | y: (): number => {
58 | const f = i / max () * LOOPS;
59 | const θ = f * 2 * Math.PI;
60 | const m = 20 + i;
61 | return (untrack ( y ) + Math.cos ( θ ) * m) | 0;
62 | },
63 | color: (): string => {
64 | const f = i / max () * LOOPS;
65 | const hue = (f * 255 + untrack ( counter ) * 10) % 255;
66 | return `hsl(${hue},100%,50%)`;
67 | }
68 | });
69 |
70 | const cache = [];
71 | const cursors = useMemo ( () => Array ( max () ).fill ( 0 ).map ( ( _, i ) => cache[i] || ( cache[i] = makeCursor ( i ) ) ) );
72 |
73 | return (
74 |
75 |
76 |
77 | {({ x, y, color }) => {
78 | return
79 | }}
80 |
81 |
82 | );
83 |
84 | };
85 |
86 | /* RENDER */
87 |
88 | render ( , document.getElementById ( 'app' ) );
89 |
--------------------------------------------------------------------------------
/src/components/keep_alive.ts:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import useCleanup from '~/hooks/use_cleanup';
5 | import useMemo from '~/hooks/use_memo';
6 | import useResolved from '~/hooks/use_resolved';
7 | import useRoot from '~/hooks/use_root';
8 | import useSuspense from '~/hooks/use_suspense';
9 | import resolve from '~/methods/resolve';
10 | import $ from '~/methods/S';
11 | import {with as _with} from '~/oby';
12 | import type {Child, Disposer, FunctionMaybe, Observable, ObservableReadonly} from '~/types';
13 |
14 | /* TYPES */
15 |
16 | type Item = {
17 | id: string,
18 | lock: number,
19 | result?: Child,
20 | suspended?: Observable,
21 | dispose?: Disposer,
22 | reset?: Disposer
23 | };
24 |
25 | /* HELPERS */
26 |
27 | const cache: Record = {};
28 | const runWithSuperRoot = _with ();
29 |
30 | let lockId = 1;
31 |
32 | /* MAIN */
33 |
34 | //TODO: Support hot-swapping owner and context, to make the context JustWork™
35 |
36 | const KeepAlive = ({ id, ttl, children }: { id: FunctionMaybe, ttl?: FunctionMaybe, children: Child }): ObservableReadonly => {
37 |
38 | return useMemo ( () => {
39 |
40 | return useResolved ( [id, ttl], ( id, ttl ) => {
41 |
42 | const lock = lockId++;
43 | const item = cache[id] ||= { id, lock };
44 |
45 | item.lock = lock;
46 | item.reset?.();
47 | item.suspended ||= $(false);
48 | item.suspended ( false );
49 |
50 | if ( !item.dispose || !item.result ) {
51 |
52 | runWithSuperRoot ( () => {
53 |
54 | useRoot ( dispose => {
55 |
56 | item.dispose = () => {
57 |
58 | delete cache[id];
59 |
60 | dispose ();
61 |
62 | };
63 |
64 | useSuspense ( item.suspended, () => {
65 |
66 | item.result = resolve ( children );
67 |
68 | });
69 |
70 | });
71 |
72 | });
73 |
74 | }
75 |
76 | useCleanup ( () => {
77 |
78 | const hasLock = () => lock === item.lock;
79 |
80 | if ( !hasLock () ) return;
81 |
82 | item.suspended?.( true );
83 |
84 | if ( !ttl || ttl <= 0 || ttl >= Infinity ) return;
85 |
86 | const dispose = () => hasLock () && item.dispose?.();
87 | const timeoutId = setTimeout ( dispose, ttl );
88 | const reset = () => clearTimeout ( timeoutId );
89 |
90 | item.reset = reset;
91 |
92 | });
93 |
94 | return item.result;
95 |
96 | });
97 |
98 | });
99 |
100 | };
101 |
102 | /* EXPORT */
103 |
104 | export default KeepAlive;
105 |
--------------------------------------------------------------------------------
/demo/emoji_counter/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | import {$, render} from 'voby';
5 | import type {Observable} from 'voby';
6 |
7 | /* HELPERS */
8 |
9 | const EMOJIS = ['👌', '☝️', '✌️', '🤘', '🖖', '🖐️'];
10 | const PLUS = '➕';
11 | const MINUS = '➖';
12 |
13 | /* MAIN */
14 |
15 | const Button = ({ onClick, children }: { onClick: () => void, children: JSX.Children }): JSX.Element => {
16 |
17 | return (
18 |
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 |
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 |
12 |
--------------------------------------------------------------------------------
/resources/logo/svg/logo-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/resources/logo/svg/logo-dark-rounded.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/resources/logo/svg/logo-light-rounded.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | |
102 | {content}
103 | |
104 | item ().props} unkeyed>
105 | {text => (
106 |
107 | {text}
108 | |
109 | )}
110 |
111 |
112 | );
113 | }}
114 |
115 |
116 |
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 | |
115 | {id}
116 | |
117 |
118 |
119 | {label}
120 |
121 | |
122 |
123 |
124 |
125 |
126 | |
127 | |
128 |
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 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
181 |
182 |
183 | );
184 |
185 | /* RENDER */
186 |
187 | render ( , document.getElementById ( 'app' ) );
188 |
--------------------------------------------------------------------------------
/resources/banner/svg/banner.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/resources/banner/svg/banner-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/resources/banner/svg/banner-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/resources/banner/svg/banner-light-rounded.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------