Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/abhishiv/alfama

⚡ Fine grained reactive UI library with no-compiler and no-magic, explicit subscriptions for signals, and first class support for HMR at ~9kb
https://github.com/abhishiv/alfama

fine-grained framework-ui javascript jsx observable reactive reactiveui ui-library

Last synced: about 2 months ago
JSON representation

⚡ Fine grained reactive UI library with no-compiler and no-magic, explicit subscriptions for signals, and first class support for HMR at ~9kb

Awesome Lists containing this project

README

        

# alfama

⚡ Fine grained reactive UI Library.

[![Version](https://img.shields.io/npm/v/alfama.svg?color=success&style=flat-square)](https://www.npmjs.com/package/alfama)
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
[![Build Status](https://github.com/abhishiv/alfama/actions/workflows/ci.yml/badge.svg)](https://github.com/abhishiv/alfama/actions/workflows/ci.yml)
![Badge size](https://deno.bundlejs.com/?q=alfama&config={%22analysis%22:undefined}&badge=)

**npm**: `npm i alfama`

**cdn**: https://cdn.jsdelivr.net/npm/alfama/+esm

---

#### Features

- **Rich and Complete:** From support for `SVG` to popular patterns like `dangerouslySetInnerHTML`, `ref` to `` and `` alfama has you covered.
- **Small:** Fully featured at `~9kB` gzip.
- **Truly reactive and fine grained:** Unlike VDOM libraries which use diffing to compute changes, it uses fine grained updates to target only the DOM which needs to update.
- **No Magic:** Explicit subscriptions obviate the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) methods found in other fine grained reactive libraries like solid/sinuous. _Importantly, many feel that this also makes your code easy to reason about._
- **Signals and Stores:** Signals for primitives and Stores for deeply nested objects/arrays.
- **First class HMR:** Preserves Signals/Stores across HMR loads for a truly stable HMR experience.
- **DevEx:** no compile step needed if you want: choose your view syntax: `h` for plain javascript or `` for babel/typescript.

#### Backers & Sponsors






grati.co


Next-Generation IDE

#### Ecosystem



alfama-router


Router with a familiar react-router like API

#### Example

[Counter - Codesandbox](https://codesandbox.io/s/counter-demo-alfama-t7ift3?file=/src/index.tsx)

```tsx
/** @jsx h **/
import { component, h, render } from "alfama";

// 1) The signal/wire/store functions are passed as a param to
// component definition
const Page = component("HomePage", (props, { signal, wire }) => {
// 2) Named signals for stable HMR
const [count, setCount] = signal("count", 0);
// or $count = signal("count", 0) and then $count.get/$count.set

// 3) Most importantly: wire reactivity to signals
// with explicit subscription using the $ token param
// NB: also makes code easy to reason about and prevents those pesky untrack/sample related errors
const $doubleCount = wire(($) => count($) * 2);

return (


Hey, {props.name}


setCount(count() + 1)}>
Increment / {wire(count)})

Double count = {$doubleCount}



);
});

render(, document.body);
```

## Motivation

This library is at its core inspired by [haptic](https://github.com/heyheyhello/haptic) that in particular it also favours manual subscription model instead of automatic subscriptions model. This oblivates the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) found in almost all other reactive libraries.

Also it borrows the nomenclature of aptly named Signal and Wire from haptic.

It's also influenced by Sinuous, Solid, & S.js

## API

### Core

signal: create a signal

```tsx
export const HomePage = component<{ name: string }>(
"HomePage",
(props, { signal, wire }) => {
const [count, setCount] = signal("count", 0);
//.. rest of component
}
);
```

wire: create a wire

```tsx


{
setCount(count() + 1);
}}
>
Increment to {wire(($) => $(count))}


```

store: create a store to hold object/arrays

```tsx
export const Todos = component("Todos", (props, { signal, wire, store }) => {
const $todos = store("todos", {
items: [{ task: "Do Something" }, { task: "Do Something else" }],
});
return (


    {
    return
  • {cursor().task}
  • ;
    }}
    >

);
});
```

defineContext: define context value

```tsx
export const RouterContext = defineContext("RouterObject");
```

setContext: set context value

```tsx
const BrowserRouter = component("Router", (props, { setContext, signal }) => {
setContext(
RouterContext,
signal("router", createRouter(window.history, window.location))
);
return props.children;
});
```

getContext: get context value

```tsx
const Link = component("Link", (props: any, { signal, wire, getContext }) => {
const router = getContext(RouterContext);
//... rest of component
});
```

onMount: triggered on mount

```tsx
export const Prosemirror = component("Prosemirror", (props, { onMount }) => {
onMount(() => {
console.log("component mounted");
});
// ...
});
```

onUnmount: triggered on unmount

```tsx
export const Prosemirror = component("Prosemirror", (props, { onUnmount }) => {
onUnmount(() => {
console.log("component unmounted");
});
// ...
});
```

### Helper Components

When: reactive if

```tsx
count($) > 5}
views={{
true: () => {
return

"TRUE"
;
},
false: () => {
return
"FALSE"
;
},
}}
>
```

Each: reactive map

```tsx
{
return

  • {wire(cursor().task)}
  • ;
    }}
    >
    ```

    Portal: mount outside of render tree

    ```tsx
    export const PortalExample = component("PortalExample", (props, utils) => {
    const [active, setActive] = utils.signal("active", false);
    return (


    {
    setActive(!active());
    }}
    >
    toggle modal

    active($)}
    views={{
    true: () => {
    return (


    Portal




    );
    },
    false: () => {
    return "";
    },
    }}
    >

    );
    });
    ```

    ## Reciepes

    HMR

    ```tsx
    /** @jsx h **/
    import { h, render } from "alfama";
    import { Layout } from "./index";

    const renderApp = ({ Layout }: { Layout: typeof Layout }) =>
    render(, document.getElementById("app")!);

    window.addEventListener("load", () => renderApp({ Layout }));

    if (import.meta.hot) {
    import.meta.hot.accept("./index", (newModule) => {
    if (newModule) renderApp(newModule as unknown as { Layout: typeof Layout });
    });
    }
    ```

    Refs

    ```tsx
    /** @jsx h **/

    export const Prosemirror = component("Prosemirror", (props, { onUnmount }) => {
    let container: Element | undefined = undefined;
    let prosemirror: EditorView | undefined = undefined;
    onUnmount(() => {
    if (prosemirror) {
    prosemirror.destroy();
    }
    });
    return (

    {
    container = el;
    if (container) {
    prosemirror = setupProsemirror(container);
    }
    }}
    >

    );
    });
    ```

    dangerouslySetInnerHTML

    ```tsx
    /** @jsx h **/

    ` }} />
    ```

    ## Concepts

    ### Signals

    These are reactive read/write variables who notify subscribers when they've been written to. They act as dispatchers in the reactive system.

    ```tsx
    const [count, setCount] = signal("count", 0);

    count(); // Passive read (read-pass)
    setCount(1); // Write

    // also possible to use get/set on signal instead of tuples
    const $count = signal("count", 0);
    $count.get();
    $count.set(5);
    ```

    The subscribers to signals are wires, which will be introduced later. They subscribe by read-subscribing the signal.

    ### Stores

    Stores are for storing nested arrays/objects and also act as dispatchers in the reactive system. And like signals, stores can also be read subsribed by wires. Outside of wires, they can be read via `reify` function. Writes can be done via `produce` function immer style.

    ```tsx
    const val = { name: "Jane", friends: [{ id: "1", name: "John" }] };
    const $profile = store("profile", val);

    // Passive read (read-pass)
    const friends = reify($profile.friends);
    console.log(friends.length);
    // Write
    produce($profile.friends, (friends) => {
    friends.push({ id: "2", name: "John Doe 2" });
    });
    ```

    ### Wires

    These are task runners who subscribe to signals/stores and react to writes. They hold a function (the task) and manage its subscriptions, nested wires, run count, and other metadata. The wire provides a `$` token to the function call that, at your discretion as the developer, can use to read-subscribe to signals.

    ```tsx
    wire(($) => {
    // Explicitly subscribe to count signal getter using the subtoken "$"
    const countValue = $(count);

    // also possible to subscribe to a stores using "$" subtoken
    const friendsCount = $($profile.friends);
    return countValue + friendsCount;
    });
    ```