diff --git a/datex_short.ts b/datex_short.ts index defefa06..b1685d16 100644 --- a/datex_short.ts +++ b/datex_short.ts @@ -5,7 +5,7 @@ import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty, /** make decorators global */ import { assert as _assert, property as _property, sync as _sync, endpoint as _endpoint, template as _template, jsdoc as _jsdoc} from "./datex_all.ts"; -import { effect as _effect, always as _always, asyncAlways as _asyncAlways, toggle as _toggle, map as _map, equals as _equals, selectProperty as _selectProperty, not as _not } from "./functions.ts"; +import { effect as _effect, always as _always, reactiveFn as _reactiveFn, asyncAlways as _asyncAlways, toggle as _toggle, map as _map, equals as _equals, selectProperty as _selectProperty, not as _not } from "./functions.ts"; export * from "./functions.ts"; import { NOT_EXISTING, DX_SLOTS, SLOT_GET, SLOT_SET } from "./runtime/constants.ts"; import { AssertionError } from "./types/errors.ts"; @@ -25,6 +25,7 @@ declare global { const endpoint: typeof _endpoint; const always: typeof _always; const asyncAlways: typeof _asyncAlways; + const reactiveFn: typeof _reactiveFn; const toggle: typeof _toggle; const map: typeof _map; const equals: typeof _equals; @@ -614,6 +615,7 @@ export function translocate|Set|Array (async (val) => val * 10)(input.val) ) // > any dependency value after the first `await` is not captured. > To avoid confusion, async transform functions are always disallowed for `asyncAlways`. +## Reactive functions + +The `always` transform function is useful for inline transforms, but sometimes you might want to define +reusable reactive functions. + +One way to achieve this is by wrapping the body of a normal function with `always`: + +```ts +// normal function +const getGreetingMessage = (country: string) => { + switch (country) { + case "de": return "Hallo"; + case "fr": return "Bonjour"; + case "es": return "Hola"; + default: return "Hello"; + } +} + +// reactive version +const getGreetingMessageReactive = (country: RefOrValue) => { + // returns a Ref that gets updated reactively + return always(() => { + switch (country) { + case "de": return "Hallo"; + case "fr": return "Bonjour"; + case "es": return "Hola"; + default: return "Hello"; + } + }) +} +``` + +You can simplify this by just wrapping a normal function with `reactiveFn`. +Although the created reactive function is called with `Ref` values, you don't need to set the input argument types to `Ref` or `RefOrValue`: + +```ts +// reactive function, returns a Ref that gets updated reactively when 'country' changes +const getGreetingMessageReactive = reactiveFn((country: string) => { + switch (country) { + case "de": return "Hallo"; + case "fr": return "Bonjour"; + case "es": return "Hola"; + default: return "Hello"; + } +}) + +const country: Ref = $$("de"); +const greetingMessage: Ref = getGreetingMessageReactive(country); + +console.log(greetingMessage.val) // "Hallo" + +country.val = "fr"; +console.log(greetingMessage.val) // "Bonjour" +``` ## Dedicated transform functions diff --git a/functions.ts b/functions.ts index baab5293..cd31a2b4 100644 --- a/functions.ts +++ b/functions.ts @@ -8,7 +8,7 @@ import { AsyncTransformFunction, CollapsedValue, CollapsedValueAdvanced, Decorat import { Datex } from "./mod.ts"; import { PointerError } from "./types/errors.ts"; import { IterableHandler } from "./utils/iterable-handler.ts"; - +import { RestrictSameType } from "./runtime/pointers.ts"; /** @@ -79,6 +79,32 @@ export async function asyncAlways(transform:SmartTransformFunction, option return Ref.collapseValue(ptr) as MinimalJSRef } +/** + * Decorator for creating a reactive function. + * Functions decorated with `reactiveFn` always return a pointer that is automatically updated when input references are updated. + * This has the same effect as wrapping the function body with `always`. + * A reactive functions accepts references or values as arguments, but is always called with collapsed values. + * This means that you don't have to specifiy `Ref` values as arguments, but can use regular types. + * + * Example: + * ```ts + * // create reactive function 'getSquared' + * const getSquared = reactiveFn((x: number) => x * x); + * + * const x = $$(2); + * const y = getSquared(x); // Ref<4> + * x.val = 3; + * y // Ref<9> + * ``` + */ +export function reactiveFn(fn: (...args: Args) => Awaited>>) { + return (...args: MapToRefOrVal) => always(() => { + const collapsedArgs = args.map(arg => Ref.collapseValue(arg, true, true)) as Args; + return fn(...collapsedArgs) + }); +} + +type MapToRefOrVal = {[K in keyof T]: T[K] extends Ref ? T[K] : RefOrValue} /**