signal-chain
Advanced tools
Comparing version 0.5.0 to 0.6.0
@@ -159,4 +159,4 @@ export type { BasicComputed, BasicSignal, Chain, CleanupExec, ConnectedChain } from './signal/types'; | ||
const stop: typeof tools.stop; | ||
const $if: <Range>(condition: import("./signal-ts").Function1<Range, boolean>) => import("./signal/if").IfCall<Range>; | ||
const ifNot: <V1>(condition: import("./signal-ts").Function1<V1, boolean>) => import("./signal/if").IfCall<V1>; | ||
const $if: <Range, Fallback = never>(condition: import("./signal-ts").Function1<Range, boolean>, args_0?: Fallback | undefined) => <V1 extends Range, V2, V3 = V2, V4 = V3, V5 = V4, V6 = V5, V7 = V6, V8 = V7, V9 = V8, V10 = V9, V11 = V10, V12 = V11, V13 = V12, V14 = V13, V15 = V14, V16 = V15, V17 = V16, V18 = V17, V19 = V18, V20 = V19>(element1?: Chain<V1, V2> | undefined, element2?: Chain<V2, V3> | undefined, element3?: Chain<V3, V4> | undefined, element4?: Chain<V4, V5> | undefined, element5?: Chain<V5, V6> | undefined, element6?: Chain<V6, V7> | undefined, element7?: Chain<V7, V8> | undefined, element8?: Chain<V8, V9> | undefined, element9?: Chain<V9, V10> | undefined, element10?: Chain<V10, V11> | undefined, element11?: Chain<V11, V12> | undefined, element12?: Chain<V12, V13> | undefined, element13?: Chain<V13, V14> | undefined, element14?: Chain<V14, V15> | undefined, element15?: Chain<V15, V16> | undefined, element16?: Chain<V16, V17> | undefined, element17?: Chain<V17, V18> | undefined, element18?: Chain<V18, V19> | undefined, element19?: Chain<V19, V20> | undefined) => Chain<V1, Fallback extends never ? V1 : Fallback | V20>; | ||
const ifNot: <V1>(condition: import("./signal-ts").Function1<V1, boolean>) => <V1_1 extends V1, V2, V3 = V2, V4 = V3, V5 = V4, V6 = V5, V7 = V6, V8 = V7, V9 = V8, V10 = V9, V11 = V10, V12 = V11, V13 = V12, V14 = V13, V15 = V14, V16 = V15, V17 = V16, V18 = V17, V19 = V18, V20 = V19>(element1?: Chain<V1_1, V2> | undefined, element2?: Chain<V2, V3> | undefined, element3?: Chain<V3, V4> | undefined, element4?: Chain<V4, V5> | undefined, element5?: Chain<V5, V6> | undefined, element6?: Chain<V6, V7> | undefined, element7?: Chain<V7, V8> | undefined, element8?: Chain<V8, V9> | undefined, element9?: Chain<V9, V10> | undefined, element10?: Chain<V10, V11> | undefined, element11?: Chain<V11, V12> | undefined, element12?: Chain<V12, V13> | undefined, element13?: Chain<V13, V14> | undefined, element14?: Chain<V14, V15> | undefined, element15?: Chain<V15, V16> | undefined, element16?: Chain<V16, V17> | undefined, element17?: Chain<V17, V18> | undefined, element18?: Chain<V18, V19> | undefined, element19?: Chain<V19, V20> | undefined) => Chain<V1_1, never>; | ||
/** | ||
@@ -163,0 +163,0 @@ * Chain multiple elements together. Each element can be a {@link Chain} or a {@link ConnectedChain} |
@@ -164,4 +164,4 @@ export * from './signal/types'; | ||
merge: typeof merge; | ||
if: <Range_2>(condition: import("./signal/types").Function1<Range_2, boolean>) => import("./signal/if").IfCall<Range_2>; | ||
ifNot: <V1_2>(condition: import("./signal/types").Function1<V1_2, boolean>) => import("./signal/if").IfCall<V1_2>; | ||
if: <Range_2, Fallback = never>(condition: import("./signal/types").Function1<Range_2, boolean>, args_0?: Fallback | undefined) => <V1_2 extends Range_2, V2_1, V3_1 = V2_1, V4_1 = V3_1, V5_1 = V4_1, V6_1 = V5_1, V7_1 = V6_1, V8_1 = V7_1, V9_1 = V8_1, V10_1 = V9_1, V11_1 = V10_1, V12_1 = V11_1, V13_1 = V12_1, V14_1 = V13_1, V15_1 = V14_1, V16_1 = V15_1, V17_1 = V16_1, V18_1 = V17_1, V19_1 = V18_1, V20_1 = V19_1>(element1?: import("./signal/types").Chain<V1_2, V2_1> | undefined, element2?: import("./signal/types").Chain<V2_1, V3_1> | undefined, element3?: import("./signal/types").Chain<V3_1, V4_1> | undefined, element4?: import("./signal/types").Chain<V4_1, V5_1> | undefined, element5?: import("./signal/types").Chain<V5_1, V6_1> | undefined, element6?: import("./signal/types").Chain<V6_1, V7_1> | undefined, element7?: import("./signal/types").Chain<V7_1, V8_1> | undefined, element8?: import("./signal/types").Chain<V8_1, V9_1> | undefined, element9?: import("./signal/types").Chain<V9_1, V10_1> | undefined, element10?: import("./signal/types").Chain<V10_1, V11_1> | undefined, element11?: import("./signal/types").Chain<V11_1, V12_1> | undefined, element12?: import("./signal/types").Chain<V12_1, V13_1> | undefined, element13?: import("./signal/types").Chain<V13_1, V14_1> | undefined, element14?: import("./signal/types").Chain<V14_1, V15_1> | undefined, element15?: import("./signal/types").Chain<V15_1, V16_1> | undefined, element16?: import("./signal/types").Chain<V16_1, V17_1> | undefined, element17?: import("./signal/types").Chain<V17_1, V18_1> | undefined, element18?: import("./signal/types").Chain<V18_1, V19_1> | undefined, element19?: import("./signal/types").Chain<V19_1, V20_1> | undefined) => import("./signal/types").Chain<V1_2, Fallback extends never ? V1_2 : Fallback | V20_1>; | ||
ifNot: <V1_3>(condition: import("./signal/types").Function1<V1_3, boolean>) => <V1_4 extends V1_3, V2_2, V3_2 = V2_2, V4_2 = V3_2, V5_2 = V4_2, V6_2 = V5_2, V7_2 = V6_2, V8_2 = V7_2, V9_2 = V8_2, V10_2 = V9_2, V11_2 = V10_2, V12_2 = V11_2, V13_2 = V12_2, V14_2 = V13_2, V15_2 = V14_2, V16_2 = V15_2, V17_2 = V16_2, V18_2 = V17_2, V19_2 = V18_2, V20_2 = V19_2>(element1?: import("./signal/types").Chain<V1_4, V2_2> | undefined, element2?: import("./signal/types").Chain<V2_2, V3_2> | undefined, element3?: import("./signal/types").Chain<V3_2, V4_2> | undefined, element4?: import("./signal/types").Chain<V4_2, V5_2> | undefined, element5?: import("./signal/types").Chain<V5_2, V6_2> | undefined, element6?: import("./signal/types").Chain<V6_2, V7_2> | undefined, element7?: import("./signal/types").Chain<V7_2, V8_2> | undefined, element8?: import("./signal/types").Chain<V8_2, V9_2> | undefined, element9?: import("./signal/types").Chain<V9_2, V10_2> | undefined, element10?: import("./signal/types").Chain<V10_2, V11_2> | undefined, element11?: import("./signal/types").Chain<V11_2, V12_2> | undefined, element12?: import("./signal/types").Chain<V12_2, V13_2> | undefined, element13?: import("./signal/types").Chain<V13_2, V14_2> | undefined, element14?: import("./signal/types").Chain<V14_2, V15_2> | undefined, element15?: import("./signal/types").Chain<V15_2, V16_2> | undefined, element16?: import("./signal/types").Chain<V16_2, V17_2> | undefined, element17?: import("./signal/types").Chain<V17_2, V18_2> | undefined, element18?: import("./signal/types").Chain<V18_2, V19_2> | undefined, element19?: import("./signal/types").Chain<V19_2, V20_2> | undefined) => import("./signal/types").Chain<V1_4, never>; | ||
log: <V_1>(message?: string | undefined) => import("./signal/types").Chain<V_1>; | ||
@@ -168,0 +168,0 @@ buffer: <V_2>(size: number) => import("./signal/types").Chain<V_2, V_2[]>; |
import { Function1, Chain } from "./types"; | ||
export interface IfCall<Range> { | ||
<V1 extends Range, V2>(element1: Chain<V1, V2>): Chain<V1, V1 | V2>; | ||
<V1 extends Range, V2, V3>(element1: Chain<V1, V2>, element2: Chain<V2, V3>): Chain<V1, V1 | V3>; | ||
<V1 extends Range, V2, V3, V4>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>): Chain<V1, V1 | V4>; | ||
<V1 extends Range, V2, V3, V4, V5>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>): Chain<V1, V1 | V5>; | ||
<V1 extends Range, V2, V3, V4, V5, V6>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>): Chain<V1, V1 | V6>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>): Chain<V1, V1 | V7>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>): Chain<V1, V1 | V8>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>): Chain<V1, V1 | V9>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>): Chain<V1, V1 | V10>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>): Chain<V1, V1 | V11>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>): Chain<V1, V1 | V12>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>): Chain<V1, V1 | V13>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>): Chain<V1, V1 | V14>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>): Chain<V1, V1 | V15>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>, element15: Chain<V15, V16>): Chain<V1, V1 | V16>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16, V17>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>, element15: Chain<V15, V16>, element16: Chain<V16, V17>): Chain<V1, V1 | V17>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16, V17, V18>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>, element15: Chain<V15, V16>, element16: Chain<V16, V17>, element17: Chain<V17, V18>): Chain<V1, V1 | V18>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16, V17, V18, V19>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>, element15: Chain<V15, V16>, element16: Chain<V16, V17>, element17: Chain<V17, V18>, element18: Chain<V18, V19>): Chain<V1, V1 | V19>; | ||
<V1 extends Range, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16, V17, V18, V19, V20>(element1: Chain<V1, V2>, element2: Chain<V2, V3>, element3: Chain<V3, V4>, element4: Chain<V4, V5>, element5: Chain<V5, V6>, element6: Chain<V6, V7>, element7: Chain<V7, V8>, element8: Chain<V8, V9>, element9: Chain<V9, V10>, element10: Chain<V10, V11>, element11: Chain<V11, V12>, element12: Chain<V12, V13>, element13: Chain<V13, V14>, element14: Chain<V14, V15>, element15: Chain<V15, V16>, element16: Chain<V16, V17>, element17: Chain<V17, V18>, element18: Chain<V18, V19>, element19: Chain<V19, V20>): Chain<V1, V1 | V20>; | ||
(first: Chain<unknown, unknown>, ...elements: Chain<unknown, unknown>[]): Chain<unknown, unknown>; | ||
} | ||
export declare const ifFn: <Range>(condition: Function1<Range, boolean>) => IfCall<Range>; | ||
export declare const ifNot: <V1>(condition: Function1<V1, boolean>) => IfCall<V1>; | ||
type IfCall<Range, Fallback> = <V1 extends Range, V2, V3 = V2, V4 = V3, V5 = V4, V6 = V5, V7 = V6, V8 = V7, V9 = V8, V10 = V9, V11 = V10, V12 = V11, V13 = V12, V14 = V13, V15 = V14, V16 = V15, V17 = V16, V18 = V17, V19 = V18, V20 = V19>(element1?: Chain<V1, V2>, element2?: Chain<V2, V3>, element3?: Chain<V3, V4>, element4?: Chain<V4, V5>, element5?: Chain<V5, V6>, element6?: Chain<V6, V7>, element7?: Chain<V7, V8>, element8?: Chain<V8, V9>, element9?: Chain<V9, V10>, element10?: Chain<V10, V11>, element11?: Chain<V11, V12>, element12?: Chain<V12, V13>, element13?: Chain<V13, V14>, element14?: Chain<V14, V15>, element15?: Chain<V15, V16>, element16?: Chain<V16, V17>, element17?: Chain<V17, V18>, element18?: Chain<V18, V19>, element19?: Chain<V19, V20>) => Chain<V1, Fallback extends never ? V1 : Fallback | V20>; | ||
export declare const ifFn: <Range, Fallback = never>(condition: Function1<Range, boolean>, args_0?: Fallback | undefined) => IfCall<Range, Fallback>; | ||
export declare const ifNot: <V1>(condition: Function1<V1, boolean>) => IfCall<V1, never>; | ||
export {}; | ||
//# sourceMappingURL=if.d.ts.map |
import { chain } from "./chain"; | ||
export const ifFn = (condition) => (first, ...elements) => { | ||
const chained = chain(first, ...elements); | ||
return (next, parameter, context) => { | ||
if (condition(parameter)) { | ||
return chained(next, parameter, context); | ||
} | ||
return next(parameter); | ||
export const ifFn = (condition, ...args) => { | ||
const hasFallback = args.length === 1; | ||
// @ts-expect-error | ||
return (first, ...elements) => { | ||
const chained = chain(first, ...elements); | ||
return (next, parameter, context) => { | ||
if (condition(parameter)) { | ||
return chained(next, parameter, context); | ||
} | ||
return hasFallback ? args[0] : next(parameter); | ||
}; | ||
}; | ||
}; | ||
export const ifNot = (condition) => ifFn(value => !condition(value)); |
{ | ||
"name": "signal-chain", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"author": "Christoph Franke", | ||
@@ -5,0 +5,0 @@ "description": "Declarative Reactive Programming Library", |
341
README.md
# Signal-Chain: A Declarative Reactive Programming Library | ||
## Simplify State Management in Web Applications | ||
**signal-chain** is a declarative reactive programming library aimed at streamlining state management and data flow in web applications. By focusing on a minimal set of reactive primitives and constructs, it offers a powerful yet straightforward approach to building dynamic web interfaces. Unlike traditional reactive programming libraries that overwhelm with an extensive array of operators and concepts, **signal-chain** focuses on simplicity, readability and reusability, ensuring that your projects remain manageable and scalable. | ||
**Signal-Chain** is a library for composing observables and asynchronous operations. It provides a core type, the Chain, several operators (select, effect, await, listen, combine, ...), and a reactive Primitive to combine declarative state management with asynchronous operations. | ||
### Features | ||
Think of Signal-Chain as RxJS for the grug developer. | ||
#### State Management | ||
The essential concepts of **Signal-Chain** are: | ||
1. **Reactive Primitives**: Supports defining state as reactive signals. | ||
2. **Reactivity to Data**: Detects changes in plain objects and arrays using proxies. | ||
3. **Reactivity to Events**: Allows subscriptions to DOM events. | ||
- **Primitive**: Represents a single reactive value. | ||
- **Chain**: A series of operations. Can be connected to update a primitive. | ||
- **Element**: A single operation in a chain. Every chain can be an element of another chain. | ||
- **Listener**: A subscription to a reactive value. Can be an element in a chain. | ||
- **Operator**: An element in a chain, that modifies the behaviour of a sub chain. | ||
#### Declarative and Asynchronous Operations | ||
**Installation**: | ||
```sh | ||
npm install signal-chain | ||
``` | ||
4. **Declarative Syntax**: Defines data flows and reactive operations. | ||
5. **Async Operations**: Integrates asynchronous tasks with the reactive model. | ||
6. **Error Handling**: Adopts an errors-as-values philosophy, supported by TypeScript typing. | ||
### Examples | ||
#### Composition | ||
**Getting Started** | ||
7. **Chain Functionality**: Combines elements into chains for cohesive data flows. | ||
8. **Reusability**: Chains can be reused and incorporated into other chains. | ||
9. **Activation on Connection**: Chains are activated upon connection, allowing modular construction. | ||
Let's define a primitive. | ||
```typescript | ||
import $ from 'signal-chain' | ||
#### Additional Features | ||
const counter = $.primitive.create(0) | ||
``` | ||
10. **Performance**: Optimized for low runtime overhead with a small library footprint (minified <10k). | ||
11. **TypeScript Support**: Fully typed, focusing on type inference. | ||
Now we can define a chain that listens to the counter and logs the value using the `effect` operator. | ||
```typescript | ||
const log = $.chain( | ||
counter.listen, | ||
$.effect(value => console.log(value)) | ||
) | ||
``` | ||
When we connect the chain, it will start listening to the counter. | ||
```typescript | ||
const disconnect = $.connect(log) // logs: 0 | ||
### Installation and Usage Instructions | ||
counter.value = 1 // logs: 1 | ||
``` | ||
To start using **signal-chain** in your projects, follow these steps: | ||
We can also rewrite the value into something different with the `select` operation. | ||
```typescript | ||
const formatted = $.chain( | ||
counter.listen, | ||
$.select(x => `The number is ${x}`) | ||
) | ||
``` | ||
1. **Installation**: | ||
```sh | ||
npm install signal-chain | ||
``` | ||
And then we create a primitive that is connected to the formatted chain. | ||
```typescript | ||
const formattedValue = $.primitive.connect(formatted) | ||
2. **Using Primitives**: | ||
```typescript | ||
import $ from 'signal-chain' | ||
counter.value = 10 | ||
console.log(formattedValue.value) // logs: The number is 10 | ||
``` | ||
// creates a reactive primitive, like a ref or a signal | ||
const counter = $.primitive.create(0) | ||
**Reusability** | ||
// chains are the core of signal-chain, they define a series of operations | ||
const format = $.chain( | ||
$.select(x => Math.round(x)), | ||
// select is like map, but with a more distinctive name | ||
$.select(x => `The number is ${x}`), | ||
In the above example, we formatted a counter value. Sometimes, we want to specify behaviour, but want to apply it to different sources. We can do that, by creating a chain that requires an input value. | ||
```typescript | ||
const format = $.chain( | ||
$.select<number>(x => Math.round(x)), | ||
$.if(x => x > 1)( | ||
$.select(x => `We have ${x} apples`) | ||
), | ||
$.if(x => x === 1)( | ||
$.select(() => `We have an apple`) | ||
), | ||
$.if(x => x === 0)( | ||
$.select(() => `We have no apples`) | ||
), | ||
$.assert.isNumber( | ||
$.select(() => 'I cannot handle negative apples. Or NaN apples.') | ||
) | ||
) | ||
``` | ||
const invert = $.chain( | ||
$.select(x => -x), | ||
) | ||
Here we have created a chain, that will format a number into a string. There are a few things going on here: | ||
- The first `$.select` has a type parameter `number`, that specifies that we expect a number as input. If we do not specify this, typescript will infer `unknown` and complain about the `Math.round(x)` operation. | ||
- The `$.if` operator will only execute the inner chain, if the condition is true. | ||
- The `$.assert` operator is similar to the `$.if` operator, in that if the condition is met, the inner chain will execute. In contrast to the `$.if` operator, it performs static type inference. In this case, all `number` input is rewritten into a `string` be the `$.select` operator, so typescript can infer that the signal after the assertion block is always a `string`. | ||
// for a chain to become active, it needs to be connected | ||
const disconnect = $.connect( | ||
counter.listen, // listen to changes in counter | ||
invert, // apply invert chain | ||
format, // apply format chain | ||
$.effect(result => console.log(result)) // log: The number is 0 | ||
) | ||
We can now use the chain to format any number. | ||
```typescript | ||
const counter = $.primitive.create(0) | ||
const formatted = $.primitive.connect( | ||
counter.listen, | ||
format, | ||
$.effect(value => console.log(value)) // logs: We have no apples | ||
) | ||
counter.value = 10 // log: The number is -10 | ||
``` | ||
counter.value = 10 // logs: We have 10 apples | ||
console.log(formatted.value) // logs: We have 10 apples | ||
``` | ||
3. **Reactive Data Fetching**: | ||
**Asynchronous Operations** | ||
Let's say we want to fetch some user data from an API and whenever the user changes, we need to fetch new data | ||
Admittedly, this type of formatting could have been done with a simple function. Let us take this approach and combine it with some asynchronous logic, so we can see the real value of the chain. | ||
```typescript | ||
import $ from 'signal-chain' | ||
Here, we will implement a auto suggest feature, that fetches some data from an API and logs the result. | ||
```typescript | ||
import $ from 'signal-chain' | ||
type UserJSON = { ... } | ||
// store user input into a reactive primitive | ||
const input = $.primitive.create('') | ||
document.getElementById('my-input')?.addEventListener('input', (event) => { | ||
input.value = (event.target as HTMLInputElement).value | ||
}) | ||
// here we store the user name, initialized with undefined | ||
const user = $.primitive.create<string | undefined>(undefined) | ||
// utility function we will use for debounce | ||
// resolves the promise to the input after the given time | ||
const wait = <T>(input: T, ms: number) => new Promise<T>(resolve => setTimeout(() => resolve(input), ms)) | ||
const suggestions = $.primitive.connect( | ||
input.listen, | ||
// here we define and connect the data fetching chain | ||
const data = $.primitive.connect( // connect will run eager and execute synchronously | ||
user.listen, // listen to user changes | ||
// debounce | ||
$.await.latest( | ||
$.select(input => wait(input, 150)), | ||
), | ||
$.assert.not.isError(), | ||
// type inferred: string | undefined | ||
$.assert.isNothing( // assert.isNothing catches null | undefined | ||
// the inside block will only be executed when the assertion is true, | ||
$.emit('guest') // in that case we emit 'guest' as our default | ||
// ensure long enough input, if not, fallback to empty array | ||
$.if((input: string) => input.length > 2, [])( | ||
$.select(input => `/api/suggest/${input}`), | ||
$.await.latest( | ||
$.select(url => fetch(url).then(response => response.json()) as Promise<string[]>), | ||
), | ||
// type inferred: string | ||
$.select(user => `/api/user/${user.toLowerCase()}`), // construct the url | ||
$.await.latest( // await.latest will only pass on the latest resolve | ||
$.select(url => fetch(url).then(response => response.json()) as Promise<UserJSON>), | ||
$.assert.isError( | ||
$.effect(err => console.error('Error fetching suggestions:', err)), | ||
$.select(() => []) | ||
), | ||
), | ||
// type inferred: UserJSON | Error | ||
$.assert.isError( // when a promise is rejected, its result will be a value of type Error | ||
$.effect(err => console.error('Error fetching data:', err)), | ||
$.stop() // no data, stop processing | ||
), | ||
$.log('Suggestions:') // Suggestions: ['So', 'many', 'suggestions', ...] | ||
) | ||
``` | ||
In this example we first store the user input in a reactive primitive and use it as input for the suggestions chain. | ||
Let's have a look at the debounce part: | ||
- The `$.await.latest` operator will only pass on the latest resolved value. If a value is incoming while the previous promise is still pending, the previous promise will be cancelled. | ||
- Together with the wait function, this will effectively create a debounce, only passing when there is no user input for 150ms. | ||
// type inferred: UserJSON | ||
$.log('Data fetched:') | ||
) | ||
When given no argument, `$.assert.not.isError()` will only pass on the value if it is not an error, otherwise it throws. We use it here to ensure type consistency: `$.await.latest` cannot know, if a promise will resolve or reject. Therefore, it passes on `ValueType | Error`. because we know that our wait function cannot reject, we can safely assert that there is no error. The assertion then removes the `Error` type from the chain. | ||
// now we set the user name, which will trigger data fetching | ||
user.value = 'Detlev' // logs: Data fetched: { ... } | ||
data.value // everything we know about Detlev, type UserJSON is inferred | ||
``` | ||
The `$.if` operator has a second parameter, which is the fallback value. If the condition is not met, the fallback value will be used instead. This defaults to the input of the operator, so if no fallback is given and the condition is not met, the input is being passed through. | ||
4. **Reactive State Management**: | ||
The `$.await.latest` is also exactly what we want in fetching data. If a new input is given while the previous request is still pending, the previous request will be cancelled. There are 4 more await operators for different strategies: | ||
- `$.await.parallel`: Passes all resolved values in the order they resolve. | ||
- `$.await.order`: Passes all resolved values in the order they were requested. | ||
- `$.await.block`: Will only enter the inner block when no promise is pending. Incoming values will be discarded. | ||
- `$.await.queue`: Will only enter the inner block when no promise is pending. Incoming values will be queued and processed by the inner block once the pending promise is resolved. | ||
Sometimes there is existing logic, that we cannot easily change. Instead of trying to find every potential place in the code, that potentially updates an object or sets a key, we can set up a listener from inside a chain, that will wrap the targeted part of the object and automatically listen for any changes. That way, we can gradually extend the observer pattern into a code base, that has not been designed with reactivity in mind. | ||
**Reactivity with Plain Objects** | ||
Sometimes you may work with existing logic, or maybe you prefer to store our state in plain objects. *Signal-Chain* can listen to plain objects and arrays using proxies. | ||
```typescript | ||
import $ from 'signal-chain' | ||
Let's assumet we have a state object like this: | ||
```typescript | ||
const state = { | ||
filter: '', | ||
elements: [ | ||
{ age: 25, name: 'Alice' }, | ||
{ age: 73, name: 'Bob' }, | ||
{ age: 42, name: 'Charlie' }, | ||
{ age: 18, name: 'David' }, | ||
] | ||
} | ||
``` | ||
// this is just a plan javascript object | ||
const user = { | ||
meta: { | ||
profil: '/default.png', | ||
loggedIn: false, | ||
comments: [] | ||
}, | ||
name: 'guest' | ||
} | ||
And let's further assume there is pre-existing logic spread out over the source code, that sets the filter and the elements. We can still listen to changes of the filter and the elements: | ||
```typescript | ||
const filter = $.primitive.connect( | ||
$.emit(state), // emit the state object | ||
$.listen.key('filter'), // listen to changes in the filter key | ||
) | ||
const elements = $.primitive.connect( | ||
$.emit(state), // emit the state object | ||
$.listen.key('elements'), // listen to changes in the elements key | ||
) | ||
``` | ||
The `$.emit` operator has no input and emits the passed argument. | ||
The `$.listen.key` operator will listen to changes in the given key of the incoming object. If the value of the key is an array type, the listener will be attached to the array itself via proxy, so that any changes to the array will also be detected. | ||
// clicking the logout button will logout the user | ||
document.getElementById('logout')?.addEventListener('click', () => { | ||
user.name = 'guest' | ||
user.meta = { | ||
profil: '/default.png', | ||
loggedIn: false, | ||
comments: [] | ||
} | ||
}) | ||
This is how we could implement a reactive filter: | ||
```typescript | ||
const filteredElements = $.primitive.connect( | ||
$.combine( | ||
elements.listen, | ||
filter.listen, | ||
), | ||
$.select(([elements, filter]) => elements.filter(element => element.name.includes(filter))) | ||
) | ||
``` | ||
Here we use the `$.combine` operator, which takes a list of elements, and combines them into one element that emits an array. Whenever one of the elements fires with a new value, the combined element will fire with the latest values of all elements. | ||
// here we can select a user from a dropdown | ||
document.getElementById('my-user-select')?.addEventListener('change', (event) => { | ||
user.name = (event.target as HTMLSelectElement).value | ||
user.meta = { | ||
profil: `/profil/${user.name}.png`, | ||
loggedIn: true, | ||
comments: [] | ||
} | ||
}) | ||
**Types and Inferrence** | ||
// here we can add a comment to the user | ||
document.getElementById('add-comment')?.addEventListener('click', () => { | ||
user.meta.comments.push({ ... }) // push the new comment | ||
}) | ||
We have so far used code with minimal type information. *Signal-Chain* is fully typed and built with type inferrence in mind. In all the above examples we have complete inferrence. Typescript will also protect us from making any mistakes, like chaining the wrong chains together: | ||
```typescript | ||
const counter = $.primitive.create('hallo') // <-- wait, this is a string! | ||
const formatted = $.primitive.connect( | ||
counter.listen, | ||
$.select(x => 2 * x) // <-- typescript error: The rhs of an arithmetic operation must be a number... | ||
) | ||
``` | ||
// up until here, we have plain javascript | ||
// we can now react to state updates without changing the existing code | ||
const numberOfComments = $.primitive.connect( | ||
$.emit(user), // emit the user object | ||
$.listen.key('meta'), // listen to the meta key | ||
// when the meta object changes, the comments listener gets reattached | ||
$.listen.key('comments'), | ||
// a proxy is used on the array to make sure we catch all changes | ||
$.select(comments => comments.length) | ||
) | ||
Sometimes a little context is necessary | ||
```typescript | ||
const multiplicator = $.chain( | ||
$.select(x => x * 5) // <-- typescript error: x is unknown | ||
) | ||
// or we could fetch some private data only for loggedIn users | ||
type PrivateData = { ... } | ||
const privateData = $.primitive.connect( | ||
$.emit(user), // emit the user object | ||
$.listen.key('meta'), // listen to changes in the meta object | ||
$.listen.key('loggedIn'), // when the meta object changes, this listener gets reattached | ||
$.if(loggedIn => !!loggedIn)( | ||
$.emit(user), | ||
$.listen.key('name'), // name changes when user account is switched without logout | ||
$.await.latest( | ||
$.select(name => `/api/private/${name.toLowerCase()}`), | ||
$.select(url => fetch(url).then(response => response.json()) as Promise<PrivateData>), | ||
), | ||
$.assert.isError($.emit(undefined)), // emit undefined on error | ||
), | ||
$.ifNot(loggedIn => !!loggedIn)( | ||
// do not leak any data if not logged in | ||
$.select(() => undefined) | ||
), | ||
// typescript cannot infer that the if/ifNot eliminated the possibility of a boolean type here | ||
// the assert statement will crash if not given a parameter | ||
// it will also remove the type boolean from the type inference | ||
$.assert.not.isBoolean() | ||
) | ||
const numberMultiplicator = $.chain( | ||
$.select<number>(x => x * 5) // no we are good | ||
) | ||
``` | ||
console.log( | ||
privateData.value // type inferred: PrivateData | undefined | ||
) | ||
``` | ||
In case you do want to be more general, you need to use a function with a generic | ||
```typescript | ||
// creates a chain from T -> boolean | ||
const truthyness = <T>() => $.chain( | ||
$.select((x: T) => !!x) | ||
) | ||
const counter = $.primitive.create(0) | ||
const somechain = $.chain( | ||
counter.listen, | ||
truthyness() // T gets inferred to number | ||
) | ||
``` | ||
### Documentation | ||
For more detail, have a look at the official [documentation](https://christophfranke.github.io/signal-chain). | ||
### Known Issues | ||
@@ -202,0 +245,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
252
297298
2130