vuse-rx
Version:
First-class rxjs support for Vue 3
206 lines (181 loc) • 5.5 kB
text/typescript
import { Ref, UnwrapRef, ref, watch, WatchOptions, WatchStopHandle } from 'vue';
/**
* Creates a binding between two refs.\
* The binding can be:
* - One-way if only the one mapper is defined.
* - Two-way if both mappers (`to` and `from`) are defined.
*
* The resulting ref serves as an origin point for the binding,\
* values **from** the resulting ref and **to** the resulting ref are mapped onto the first.
*/
export function syncRef<R1, R2>(
ref1: Ref<R1>,
{ to }: { to: Mapper<R1, UnwrapRef<R2>> },
): SyncedRef<R1, 'to', UnwrapRef<R2>>;
export function syncRef<R1, R2>(
ref1: Ref<R1>,
{ from }: { from: Mapper<UnwrapRef<R2>, R1> },
defaultValue?: R2,
): SyncedRef<R1, 'from', UnwrapRef<R2>>;
export function syncRef<R1, R2>(
ref1: Ref<R1>,
{ to, from }: { to: Mapper<R1, UnwrapRef<R2>>, from: Mapper<UnwrapRef<R2>, R1> },
): SyncedRef<R1, 'to' | 'from', UnwrapRef<R2>>;
/**
* Creates a binding between two refs.\
* The binding can be:
* - One-way if only the one mapper is defined.
* - Two-way if both mappers (`to` and `from`) are defined.
*
* The resulting ref serves as an origin point for the binding,\
* values **from** the resulting ref and **to** the resulting ref are mapped onto the first.
*/
export function syncRef<R1, R2, M extends Readonly<Mappers<R1, R2>> = Mappers<R1, R2>>(
ref1: Ref<R1>,
{ to, from }: M,
defaultValue?: R2,
): SyncedRef<R1, keyof M, UnwrapRef<R2>>;
/**
* Creates a binding between two refs.\
* The binding can be:
* - One-way if only the one mapper is defined.
* - Two-way if both mappers (`to` and `from`) are defined.
*
* The second ref serves as an origin point for the binding,\
* values **from** the second ref and **to** the second ref are mapped onto the first.
*/
export function syncRef<R1, R2, M extends Readonly<Mappers<R1, R2>> = Mappers<R1, R2>>(
ref1: Ref<R1>,
{ to, from }: M,
ref2: Ref<R2>,
): SyncedRef<R1, keyof M, R2>;
export function syncRef<R1, R2>(
this: WatchOptions,
ref1: Ref<R1>,
maps: Mappers<R1, R2>,
_ref2?: Ref<R2> | R2,
): _SyncedRef<R1, keyof Mappers<R1, R2>, R2> {
const ref2 = ref(
_ref2 == null
? maps.to
? maps.to(ref1.value)
: ref1.value
: _ref2
) as _SyncedRef<R1, keyof Mappers<R1, R2>, R2>;
for (const key in maps) (
ref2[key as keyof typeof maps] = bind(ref1, ref2, maps, key as any, this)
).bind();
return ref2;
}
syncRef.with = <T extends Readonly<boolean> = false>(
...options: WatchOptions<T>[]
): typeof syncRef => {
const opts = Object.assign({}, ...options);
const f = syncRef.bind(opts);
f.with = syncRef.with.bind(opts);
return f;
}
const bind = <R1, R2>(
refBase: Ref<R1>,
refDest: _SyncedRef<R1, keyof Mappers<R1, R2>, R2>,
maps: Mappers<R1, R2>,
dir: keyof Mappers<R1, R2>,
options: WatchOptions,
) => ({
bind: (bindOptions?: Partial<BindingOptions<R1, R2, keyof Mappers<R1, R2>>>) => {
const {
ref, map, watch: opts
}: BindingOptions<any, R2, any> = {
ref: refBase,
map: maps[dir]!,
watch: options,
...bindOptions,
};
refDest[dir]!.stop();
refDest[dir]!.stop = dir === 'to'
? watch(ref, v => refDest.value = map(v), Object.assign({}, options, opts))
: watch(refDest, v => ref.value = map(v), Object.assign({}, options, opts));
},
stop: () => {}
});
type Mapper<F, T> = (value: F) => T;
type Mappers<R1, R2> = {
/**
* A map from the first ref to the second
*/
to: Mapper<R1, R2>,
/**
* A map from the second ref to the first
*/
from?: never,
} | {
/**
* A map from the first ref to the second
*/
to?: never,
/**
* A map from the second ref to the first
*/
from: Mapper<R2, R1>,
} | {
/**
* A map from the first ref to the second
*/
to: Mapper<R1, R2>,
/**
* A map from the second ref to the first
*/
from: Mapper<R2, R1>,
};
type Binders<R1, R2, Keys extends PropertyKey> = {
[key in Keys]: {
/**
* Cut the binding in this direction
*/
stop: WatchStopHandle;
bind: {
/**
* Reapply the binding in this direction
*/
(): void;
/**
* Bind this direction to a new ref
*/
(options?: BindingOptions<R1, R2, key>): void;
/**
* Bind this direction to a new ref with a new map
*/
<T>(options: CustomBindingOptions<T, R2, key>): void;
};
};
};
/**
* Allows to correctly declare a reusable options object for the [`.bind`](https://vuse-rx.raiondesu.dev/api/refs#change-ref-bindings) method.
*/
export type BindingOptions<R1, R2, Key extends PropertyKey> = {
ref?: Ref<R1>;
map?: Key extends 'to' ? Mapper<R1, R2> : Mapper<R2, R1>;
watch?: WatchOptions;
}
/**
* Allows to correctly declare a reusable options object for the [`.bind`](https://vuse-rx.raiondesu.dev/api/refs#change-ref-bindings) method.
* Has a semantic difference from `BindingOptions` in that it's supposed to be used with a new ref type instead of the original first ref
*/
export type CustomBindingOptions<T, R2, Key extends PropertyKey> = {
ref: Ref<T>;
map: Key extends 'to' ? Mapper<T, R2> : Mapper<R2, T>;
watch?: WatchOptions;
}
/**
* Return type of the `syncRef` function, combined Vue Ref with `syncRef`'s bindings
*/
export type SyncedRef<
R1,
Keys extends PropertyKey,
R2,
> = Ref<R2> & Binders<R1, R2, Keys>;
type _SyncedRef<
R1,
_Keys extends PropertyKey,
R2,
> = Ref<R2> & Partial<Binders<R1, R2, keyof Mappers<R1, R2>>>;