Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/abhishiv/alfama
- Owner: abhishiv
- Created: 2023-03-05T16:41:50.000Z (almost 2 years ago)
- Default Branch: master
- Last Pushed: 2024-10-18T00:19:51.000Z (2 months ago)
- Last Synced: 2024-10-19T13:42:31.866Z (2 months ago)
- Topics: fine-grained, framework-ui, javascript, jsx, observable, reactive, reactiveui, ui-library
- Language: TypeScript
- Homepage: https://alfama.vercel.app
- Size: 187 KB
- Stars: 8
- Watchers: 1
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
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 (
- {cursor().task} ;
{
return
}}
>
);
});
```
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
},
false: () => {
return
},
}}
>
```
Each: reactive map
```tsx
{
return
}}
>
```
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;
});
```