signal-chain
Advanced tools
Comparing version 0.7.6 to 0.8.0
@@ -11,6 +11,6 @@ export * from './signal/types'; | ||
batch?: boolean | undefined; | ||
update?: ("immediate" | "microtask" | "timeout") | undefined; | ||
update?: ("sync" | "microtask" | "timeout") | undefined; | ||
} | undefined) => { | ||
readonly batch: boolean; | ||
readonly update: "immediate" | "microtask" | "timeout"; | ||
readonly update: "sync" | "microtask" | "timeout"; | ||
}; | ||
@@ -20,3 +20,3 @@ primitive: { | ||
batch?: boolean | undefined; | ||
update?: ("immediate" | "microtask" | "timeout") | undefined; | ||
update?: ("sync" | "microtask" | "timeout") | undefined; | ||
}) => import("./signal/types").PrimitiveSignal<V>; | ||
@@ -179,4 +179,4 @@ connect: import("./signal/primitive").ConnectCall; | ||
merge: typeof merge; | ||
if: <Range_2, Fallback = never>(condition: import("./signal/types").Function1<Range_2, boolean>, args_0?: Fallback | undefined) => <V1_6 extends Range_2, V2_5, V3_5 = V2_5, V4_5 = V3_5, V5_5 = V4_5, V6_5 = V5_5, V7_5 = V6_5, V8_5 = V7_5, V9_5 = V8_5, V10_5 = V9_5, V11_5 = V10_5, V12_5 = V11_5, V13_5 = V12_5, V14_5 = V13_5, V15_5 = V14_5, V16_5 = V15_5, V17_5 = V16_5, V18_5 = V17_5, V19_5 = V18_5, V20_5 = V19_5>(element1?: import("./signal/types").Chain<V1_6, V2_5> | undefined, element2?: import("./signal/types").Chain<V2_5, V3_5> | undefined, element3?: import("./signal/types").Chain<V3_5, V4_5> | undefined, element4?: import("./signal/types").Chain<V4_5, V5_5> | undefined, element5?: import("./signal/types").Chain<V5_5, V6_5> | undefined, element6?: import("./signal/types").Chain<V6_5, V7_5> | undefined, element7?: import("./signal/types").Chain<V7_5, V8_5> | undefined, element8?: import("./signal/types").Chain<V8_5, V9_5> | undefined, element9?: import("./signal/types").Chain<V9_5, V10_5> | undefined, element10?: import("./signal/types").Chain<V10_5, V11_5> | undefined, element11?: import("./signal/types").Chain<V11_5, V12_5> | undefined, element12?: import("./signal/types").Chain<V12_5, V13_5> | undefined, element13?: import("./signal/types").Chain<V13_5, V14_5> | undefined, element14?: import("./signal/types").Chain<V14_5, V15_5> | undefined, element15?: import("./signal/types").Chain<V15_5, V16_5> | undefined, element16?: import("./signal/types").Chain<V16_5, V17_5> | undefined, element17?: import("./signal/types").Chain<V17_5, V18_5> | undefined, element18?: import("./signal/types").Chain<V18_5, V19_5> | undefined, element19?: import("./signal/types").Chain<V19_5, V20_5> | undefined) => import("./signal/types").Chain<V1_6, Fallback extends never ? V1_6 : Fallback | V20_5>; | ||
ifNot: <V1_7>(condition: import("./signal/types").Function1<V1_7, boolean>) => <V1_8 extends V1_7, V2_6, V3_6 = V2_6, V4_6 = V3_6, V5_6 = V4_6, V6_6 = V5_6, V7_6 = V6_6, V8_6 = V7_6, V9_6 = V8_6, V10_6 = V9_6, V11_6 = V10_6, V12_6 = V11_6, V13_6 = V12_6, V14_6 = V13_6, V15_6 = V14_6, V16_6 = V15_6, V17_6 = V16_6, V18_6 = V17_6, V19_6 = V18_6, V20_6 = V19_6>(element1?: import("./signal/types").Chain<V1_8, V2_6> | undefined, element2?: import("./signal/types").Chain<V2_6, V3_6> | undefined, element3?: import("./signal/types").Chain<V3_6, V4_6> | undefined, element4?: import("./signal/types").Chain<V4_6, V5_6> | undefined, element5?: import("./signal/types").Chain<V5_6, V6_6> | undefined, element6?: import("./signal/types").Chain<V6_6, V7_6> | undefined, element7?: import("./signal/types").Chain<V7_6, V8_6> | undefined, element8?: import("./signal/types").Chain<V8_6, V9_6> | undefined, element9?: import("./signal/types").Chain<V9_6, V10_6> | undefined, element10?: import("./signal/types").Chain<V10_6, V11_6> | undefined, element11?: import("./signal/types").Chain<V11_6, V12_6> | undefined, element12?: import("./signal/types").Chain<V12_6, V13_6> | undefined, element13?: import("./signal/types").Chain<V13_6, V14_6> | undefined, element14?: import("./signal/types").Chain<V14_6, V15_6> | undefined, element15?: import("./signal/types").Chain<V15_6, V16_6> | undefined, element16?: import("./signal/types").Chain<V16_6, V17_6> | undefined, element17?: import("./signal/types").Chain<V17_6, V18_6> | undefined, element18?: import("./signal/types").Chain<V18_6, V19_6> | undefined, element19?: import("./signal/types").Chain<V19_6, V20_6> | undefined) => import("./signal/types").Chain<V1_8, never>; | ||
if: import("./signal/if").IfFn; | ||
ifNot: <V1_6>(condition: import("./signal/types").Function1<V1_6, boolean>) => <V1_7 extends V1_6, V2_5, V3_5 = V2_5, V4_5 = V3_5, V5_5 = V4_5, V6_5 = V5_5, V7_5 = V6_5, V8_5 = V7_5, V9_5 = V8_5, V10_5 = V9_5, V11_5 = V10_5, V12_5 = V11_5, V13_5 = V12_5, V14_5 = V13_5, V15_5 = V14_5, V16_5 = V15_5, V17_5 = V16_5, V18_5 = V17_5, V19_5 = V18_5, V20_5 = V19_5>(element1?: import("./signal/types").Chain<V1_7, V2_5> | undefined, element2?: import("./signal/types").Chain<V2_5, V3_5> | undefined, element3?: import("./signal/types").Chain<V3_5, V4_5> | undefined, element4?: import("./signal/types").Chain<V4_5, V5_5> | undefined, element5?: import("./signal/types").Chain<V5_5, V6_5> | undefined, element6?: import("./signal/types").Chain<V6_5, V7_5> | undefined, element7?: import("./signal/types").Chain<V7_5, V8_5> | undefined, element8?: import("./signal/types").Chain<V8_5, V9_5> | undefined, element9?: import("./signal/types").Chain<V9_5, V10_5> | undefined, element10?: import("./signal/types").Chain<V10_5, V11_5> | undefined, element11?: import("./signal/types").Chain<V11_5, V12_5> | undefined, element12?: import("./signal/types").Chain<V12_5, V13_5> | undefined, element13?: import("./signal/types").Chain<V13_5, V14_5> | undefined, element14?: import("./signal/types").Chain<V14_5, V15_5> | undefined, element15?: import("./signal/types").Chain<V15_5, V16_5> | undefined, element16?: import("./signal/types").Chain<V16_5, V17_5> | undefined, element17?: import("./signal/types").Chain<V17_5, V18_5> | undefined, element18?: import("./signal/types").Chain<V18_5, V19_5> | undefined, element19?: import("./signal/types").Chain<V19_5, V20_5> | undefined) => import("./signal/types").Chain<V1_7, V1_7 | V20_5>; | ||
log: <V_1>(message?: string | undefined) => import("./signal/types").Chain<V_1>; | ||
@@ -183,0 +183,0 @@ buffer: <V_2>(size: number) => import("./signal/types").Chain<V_2, V_2[]>; |
import { Function1, Chain } from "./types"; | ||
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>; | ||
type IfCallFallback<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 | V20>; | ||
type IfCall<Range> = <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, V1 | V20>; | ||
export interface IfFn { | ||
<Range>(condition: Function1<Range, boolean>): IfCall<Range>; | ||
<Range, Fallback>(condition: Function1<Range, boolean>, fallback: Fallback): IfCallFallback<Range, Fallback>; | ||
} | ||
export declare const ifFn: IfFn; | ||
export declare const ifNot: <V1>(condition: Function1<V1, boolean>) => IfCall<V1>; | ||
export {}; | ||
//# sourceMappingURL=if.d.ts.map |
import { chain } from "./chain"; | ||
// @ts-expect-error | ||
export const ifFn = (condition, ...args) => { | ||
const hasFallback = args.length === 1; | ||
// @ts-expect-error | ||
return (first, ...elements) => { | ||
@@ -6,0 +6,0 @@ const chained = chain(first, ...elements); |
@@ -25,3 +25,3 @@ import type { PrimitiveSignal, PrimitiveReadonly, Chain } from './types'; | ||
export declare const connect: ConnectCall; | ||
type UpdateMethods = 'immediate' | 'microtask' | 'timeout'; | ||
type UpdateMethods = 'sync' | 'microtask' | 'timeout'; | ||
type Config = { | ||
@@ -28,0 +28,0 @@ batch?: boolean; |
@@ -53,3 +53,3 @@ import { chain } from './chain'; | ||
const batchMethod = (config) => { | ||
return (updateMethod(config) !== 'immediate') && (config.batch ?? defaultConfig.batch); | ||
return (updateMethod(config) !== 'sync') && (config.batch ?? defaultConfig.batch); | ||
}; | ||
@@ -97,3 +97,3 @@ export const setConfig = (config) => { | ||
switch (method) { | ||
case 'immediate': | ||
case 'sync': | ||
applyListeners(); | ||
@@ -100,0 +100,0 @@ break; |
{ | ||
"name": "signal-chain", | ||
"version": "0.7.6", | ||
"version": "0.8.0", | ||
"author": "Christoph Franke", | ||
"description": "Declarative Reactive Programming Library", | ||
"license": "MIT", | ||
"homepage": "https://christophfranke.github.io/signal-chain/", | ||
"repository": { | ||
@@ -9,0 +8,0 @@ "type": "git", |
215
README.md
@@ -26,3 +26,3 @@  | ||
**Getting Started** | ||
**Primitives** | ||
@@ -36,6 +36,64 @@ Let's define a primitive. | ||
Now we can define a chain that listens to the counter and logs the value using the `effect` operator. | ||
A primitive is a container holding a single reactive value. We can access and update the value directly. | ||
```typescript | ||
const log = $.chain( | ||
counter.value = 1 | ||
console.log(counter.value) // logs: 1 | ||
``` | ||
We can also listen to changes of the counter. | ||
```typescript | ||
const disconnect = counter.listen(newValue => { | ||
console.log('new value:', newValue) // logs: new value: 1 | ||
}) | ||
counter.value = 10 // logs: new value: 10 | ||
disconnect() | ||
counter.value = 0 // silence | ||
``` | ||
Note, that the listen fires immediately with the current value. | ||
We can also return a cleanup function, that is being executed before the next value is passed in and on disconnect. | ||
```typescript | ||
const el = document.createNode('p') | ||
const disconnect = counter.listen(value => { | ||
el.innerHTML = `Value is ${value}.` | ||
body.appendChild(el) | ||
return () => { | ||
body.removeChild(el) | ||
} | ||
}) | ||
``` | ||
Whenever the value changes, the paragraph will be updated. When the listener is disconnected, the paragraph will be removed from the body. | ||
This example has a serious downside though: We remove and append the element each time a value is updated, which feels somewhat wasteful. We can improve on this by using the parameter passed to the cleanup: A `boolean` that is true on disconnect. | ||
```typescript | ||
const el = document.createNode('p') | ||
body.appendChild(el) | ||
const disconnect = counter.listen(value => { | ||
el.innerHTML = `Value is ${value}.` | ||
return final => { | ||
if (final) { | ||
body.removeChild(el) | ||
} | ||
} | ||
}) | ||
``` | ||
**Chains** | ||
Now that we have seen how *Primitives* work, we will use *Chains* to operate on *Primitives*. A *Chain* gives us the ability to define a series of operations that can be combined and reused. As opposed to the `listen` function of a *Primitive*, a *Chain* will not execute before it is connected. | ||
We can use the `listen` function of a *Primitive* as an element in a *Chain* and combine it with a few operators: | ||
- `$.select`: Maps the incoming value to a new value. | ||
- `$.effect`: Executes a side effect. Like in the `listen` function we can return a cleanup. | ||
```typescript | ||
const counter = $.primitive.create(1) | ||
const logSquare = $.chain( | ||
counter.listen, | ||
$.select(value => value * value), | ||
$.effect(value => console.log(value)) | ||
@@ -45,23 +103,21 @@ ) | ||
When we connect the chain, it will start listening to the counter. | ||
Once we connect the chain, it will start listening to the counter. Note that it executes immediately and synchronously on connection. | ||
```typescript | ||
const disconnect = $.connect(log) // logs: 0 | ||
const disconnect = $.connect(logSquare) // logs: 1 | ||
counter.value = 1 // logs: 1 | ||
counter.value = 2 // logs: 4 | ||
disconnect() | ||
counter.value = 3 // silence | ||
``` | ||
We can also rewrite the value into something different with the `select` operation. | ||
We can also store the result of the chain in a primitive. | ||
```typescript | ||
const formatted = $.chain( | ||
const squared = $.chain( | ||
counter.listen, | ||
$.select(x => `The number is ${x}`) | ||
$.select(value => value * value) | ||
) | ||
``` | ||
const squaredValue = $.primitive.connect(squared) | ||
And then we create a primitive that is connected to the formatted chain. | ||
```typescript | ||
const formattedValue = $.primitive.connect(formatted) | ||
counter.value = 10 | ||
console.log(formattedValue.value) // logs: The number is 10 | ||
console.log(squaredValue.value) // logs: 100 | ||
``` | ||
@@ -71,7 +127,7 @@ | ||
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. | ||
In the above example, we squared a counter value. Sometimes, we want to specify behaviour, but want to be able to apply it to different sources later. 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)( | ||
$.if((x: number) => x > 1)( | ||
$.select(x => `We have ${x} apples`) | ||
@@ -86,3 +142,4 @@ ), | ||
$.assert.isNumber( | ||
$.select(() => 'I cannot handle negative apples. Or NaN apples.') | ||
$.effect(x => console.log("I don't like", x, 'apples')), | ||
$.stop() | ||
) | ||
@@ -93,6 +150,13 @@ ) | ||
Here we have created a chain, that will format a number into a string. There are a few things going on: | ||
- 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. This is the recommended approach of defining input types. If the first operator is not a `$.select`, you can always add an empty select operation `$.select<ExpectedType>()`. | ||
- The `$.if` operator is a higher order operator. That means, it expects a chain (or multiple elements) as a parameter. They define the *inner chain*. The *inner chain* will only execute, if the condition is true. | ||
- The `$.assert` operator, also a higher order 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. Here, all `number` input is rewritten into a `string` by the `$.select` operator, causing typescript to infer that the signal thereafter is always a `string`. | ||
The first `$.select` has a type parameter `number`, which 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. This is the recommended approach of defining input types for chains. It is possible to use an empty select operation `$.select<ExpectedType>()`. | ||
The `$.if` operator is a higher order operator. It expects a condition function, and can then define a new chain, the *inner chain*, which will only execute if the condition is true. If the condition is not met, it will pass on the value. The select operator in the *inner chain* rewrites the `number` into a `string`. | ||
The `$.assert.isNumber` is similar to the `$.if` operator, in that it allows you to define a *inner chain*, that only gets executed when the incoming value is a `number`. Every *Chain* is strongly typed, and after the `$.select` operations inside the `$.if` statement, Typescript will infer `number | string`, after the `$.if` operations. | ||
The `$.effect` writes to the console, without changing the passing value. The `$.stop` operator stops the chain, therefore the output of the *inner chain* is being inferred as `never`. The `$.assert` operator then concludes, that the remaining type can only be a `string`. | ||
This results in `format` being of type `Chain<number, string>`, a chain that requires a `number` input producing a `string` output. | ||
We can now use this chain to format a number. | ||
@@ -109,2 +173,5 @@ ```typescript | ||
console.log(formatted.value) // logs: We have 10 apples | ||
counter.value = -1 // logs: I don't like -1 apples | ||
console.log(formatted.value) // logs: We have 10 apples | ||
``` | ||
@@ -114,3 +181,3 @@ | ||
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. | ||
Admittedly, this type of formatting could have been done with a traditional function. Let us take this approach and combine it with some asynchronous logic. This is a main strength of **Signal-Chain**: It allows to define asynchronous reactive behaviour in a structured way. | ||
@@ -148,2 +215,3 @@ Here, we will implement an auto suggest feature, that fetches some data from an API and logs the result. | ||
$.await.latest( | ||
// TODO: Break next line into something more readable | ||
$.select(url => fetch(url).then(response => response.json()) as Promise<string[]>), | ||
@@ -153,3 +221,3 @@ ), | ||
$.effect(err => console.error('Error fetching suggestions:', err)), | ||
$.select(() => []) | ||
$.select(() => []) // fallback to empty array | ||
), | ||
@@ -161,3 +229,3 @@ ), | ||
``` | ||
In this example we first store the user input in a reactive primitive. We use that to primitive as a starting point to define the chain to fetch the suggestions. | ||
In this example we first store the user input in a reactive primitive. We use that primitive as a starting point to define the chain to fetch the suggestions. | ||
@@ -168,3 +236,3 @@ Let's have a look at the debounce part: | ||
When given no argument, `$.assert.not.isError()` will 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 `TypeOfPromiseResolve | Error`. Because we know that our wait function cannot reject, we can safely assert that there is no error. The assertion operator then removes the `Error` type from the chain. | ||
The `$.await.latest` operator will resolve the promise or pass on an `Error` if rejected. Its output type is `TypeOfPromiseResolve | Error`. In this case we know, that `wait` cannot reject, so we can safely assert that there is no error. The assertion operator then removes the `Error` type from the chain. | ||
@@ -175,5 +243,5 @@ *Why* is it designed like this? It follows the principle of **errors as values**. This reminds the developer that at this place something can go wrong and we need to handle it somehow. If we were not to handle the error at all, the suggestion pimitive would have an inferred type of `string[] | Error`. | ||
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. This is similar to the RxJS behviour of `switchMap`. For other scenarios there are 4 more await operators with different strategies: | ||
- `$.await.parallel`: Passes on all resolved values in the order they resolve. | ||
- `$.await.order`: Passes on all resolved values in the order they were requested. | ||
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 discarded. This is similar to the RxJS behviour of `switchMap`. For other scenarios there are 4 more await operators with different strategies: | ||
- `$.await.parallel`: Passes on each resolved value as soon as it resolves. | ||
- `$.await.order`: Passes on 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. | ||
@@ -213,2 +281,5 @@ - `$.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. | ||
In order for the `$.listen.key` operator to work on the object, it inserts a proxy in place of the object key resp. array. The proxy is designed to not interfere with the object itself, but it will make the object appear slightly different, for example on `console.log` operations. | ||
This is how we could implement a reactive filter: | ||
@@ -232,7 +303,7 @@ ```typescript | ||
```typescript | ||
state.filter = 'Alice' // this works as expected | ||
state.filter = 'Alice' // this will trigger the filter chain to update | ||
state.elements.push({ | ||
age: 42, | ||
name: 'Eve' | ||
}) // this is also fine | ||
}) // so will this | ||
@@ -242,7 +313,7 @@ state.elements[0] = { | ||
age: 53 | ||
} // also fine | ||
} // and this | ||
state.elements[0].name = 'Bob' // this will not trigger the chain | ||
``` | ||
The reason for that is, that although the `$.listen.key('elements')` listens to all array changes, the change to the name property of the array is not considered a change to the array itself. Also, `$.select` is not a reactive context. Only the `$.listen` operator is reactive. If we want to listen to changes to the keys of the objects, that are the elements of the array, we need to add another listener to the key: | ||
The reason for that is, that although the `$.listen.key('elements')` listens to all array changes, the change to the name property of the array is not considered a change to the array itself. Also, `$.select` is not a reactive context. If we want to listen to changes to the keys of the objects, that are the elements of the array, we need to add another listener to the key: | ||
```typescript | ||
@@ -275,5 +346,16 @@ const names = $.chain( | ||
) | ||
const squareRoot = $.chain( | ||
$.select<number>(x => Math.sqrt(x)) | ||
) | ||
const toFixed = $.chain( | ||
$.select((x: number) => x.toFixed(2)) | ||
) | ||
$.chain(toFixed, squareRoot) // <-- typescript error: | ||
// toFixed returns a string, but squareRoot expects a number | ||
``` | ||
Sometimes a little context is necessary | ||
Sometimes we need to explicitly define the type of a chain, because it is impossible to infer. | ||
```typescript | ||
@@ -289,3 +371,3 @@ const multiplicator = $.chain( | ||
In case you do want to be more general, you need to use a function with a generic | ||
In some cases, we would like the type to be inferred by usage. We can use Typescript *Generics* to achieve that. In order for this to work, we need to define a function, that returns a chain, because values cannot be generic in Typescript. | ||
```typescript | ||
@@ -301,3 +383,3 @@ // creates a chain from T -> boolean | ||
truthyness() // T gets inferred to number | ||
) | ||
) // somechain is inferred as Chain<void, boolean> | ||
``` | ||
@@ -319,7 +401,60 @@ | ||
- `$.assert.not.create`: Creates a custom negated assertion from a type predicate function. | ||
- `$.maybe.select`: Selects if the value is not undefined or null. | ||
- `$.maybe.listen.key`: Listens to a key if the value is not undefined or null. | ||
- `$.listen.event`: Listen to DOM events. | ||
**Signal-Chain** also includes a few utility functions: | ||
- `$.evaluate.sync`: Evaluates a chain synchronously. | ||
- `$.evaluate.async`: Evaluates a chain asynchronously. | ||
- `$.config`: Configures the update behaviour of the library. | ||
### Controlling Update Behaviour | ||
The default behaviour for updating *Primitives* is to batch updates and execute them asynchronously as microtasks. For example: | ||
```typescript | ||
const counter = $.primitive.create(0) | ||
$.connect( | ||
counter.listen, | ||
$.log('value') // logs: value 0 | ||
) | ||
counter.value = 1 | ||
counter.value = 2 | ||
counter.value = 3 | ||
// stop execution and give a chance to run queued microtasks | ||
await Promise.resolve() | ||
// logs: value 3 | ||
``` | ||
This is desirable on most cases. However, there are cases where we want to execute updates synchronously. You can either change the behaviour globally using `$.config` | ||
```typescript | ||
$.config({ update: 'sync' }) // synchronous updates, batching turned off | ||
$.config({ update: 'timeout' }) // use macrotasks for updates | ||
$.config({ batch: false }) // turn off batching | ||
$.config({ update: 'microtask', batch: false }) // no batching, use microtasks for updates | ||
console.log($.config()) // log the current configuration | ||
``` | ||
Or you can pass a config when creating a primitive: | ||
```typescript | ||
const counter = $.primitive.create(0, { update: 'sync' }) | ||
$.connect( | ||
counter.listen, | ||
$.log('value') // logs: value 0 | ||
) | ||
counter.value = 1 // logs: value 1 | ||
counter.value = 2 // logs: value 2 | ||
counter.value = 3 // logs: value 3 | ||
``` | ||
This can be especially useful when you want to use the primitive as a queue to push in tasks. | ||
### Documentation | ||
For more detail, have a look at the official [documentation](https://christophfranke.github.io/signal-chain). | ||
Please note, the documentation is still in progress. | ||
This was a brief overview of the **Signal-Chain** library. There is an effort to create a comprehensive documentation to cover all operators and concepts. However, the current focus is on inlining the documentation, so it is available in the editor via Typescript LSP. | ||
@@ -331,4 +466,7 @@ ### Known Issues | ||
- When listening to an object key, and the key had an array type, but is now being assigned a non-array, the application throws an error unsupported. | ||
- The interface design of `$.if` makes it impossible to infer the type of the condition, making it necessary to specify the type of the condition explicitly. | ||
- Options and behaviour of update batching and async updates is not stable yet. There are several options and there will be a way to turn on/off batching and asnyc, the default configuration may change though. | ||
>TODO: The second part is no longer true. Instead describe the api | ||
### Roadmap | ||
@@ -340,1 +478,4 @@ | ||
- Refactor the `Chain` type into `SyncChain` and `AsyncChain` and merge `$.evaluate.sync` and `$.evaluate.async` | ||
- Add a `$.toFunction` that creates a function from a chain | ||
> TODO: SyncChain, AsyncChain, WeakChain, AsyncWeakChain |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
257135
2120
460