diff --git a/packages/vue/src/composables/useClamp/index.test.ts b/packages/vue/src/composables/useClamp/index.test.ts new file mode 100644 index 0000000..e7daca8 --- /dev/null +++ b/packages/vue/src/composables/useClamp/index.test.ts @@ -0,0 +1,60 @@ +import { ref, readonly, computed } from 'vue'; +import { describe, it, expect } from 'vitest'; +import { useClamp } from '.'; + +describe('useClamp', () => { + it('non-reactive values should be clamped', () => { + const clampedValue = useClamp(10, 0, 5); + + expect(clampedValue.value).toBe(5); + }); + + it('clamp the value within the given range', () => { + const value = ref(10); + const clampedValue = useClamp(value, 0, 5); + + expect(clampedValue.value).toBe(5); + }); + + it('clamp the value within the given range using functions', () => { + const value = ref(10); + const clampedValue = useClamp(value, () => 0, () => 5); + + expect(clampedValue.value).toBe(5); + }); + + it('clamp readonly values', () => { + const computedValue = computed(() => 10); + const readonlyValue = readonly(ref(10)); + const clampedValue1 = useClamp(computedValue, 0, 5); + const clampedValue2 = useClamp(readonlyValue, 0, 5); + + expect(clampedValue1.value).toBe(5); + expect(clampedValue2.value).toBe(5); + }); + + it('update the clamped value when the original value changes', () => { + const value = ref(10); + const clampedValue = useClamp(value, 0, 5); + value.value = 3; + + expect(clampedValue.value).toBe(3); + }); + + it('update the clamped value when the min or max changes', () => { + const value = ref(10); + const min = ref(0); + const max = ref(5); + const clampedValue = useClamp(value, min, max); + + expect(clampedValue.value).toBe(5); + + max.value = 15; + + expect(clampedValue.value).toBe(10); + + min.value = 11; + + expect(clampedValue.value).toBe(11); + }); +}); \ No newline at end of file diff --git a/packages/vue/src/composables/useClamp/index.ts b/packages/vue/src/composables/useClamp/index.ts new file mode 100644 index 0000000..2ed2d47 --- /dev/null +++ b/packages/vue/src/composables/useClamp/index.ts @@ -0,0 +1,37 @@ +import { clamp, isFunction } from '@robonen/stdlib'; +import { computed, isReadonly, ref, toValue, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type WritableComputedRef } from 'vue'; + +/** + * @name useClamp + * @category Math + * @description Clamps a value between a minimum and maximum value + * + * @param {MaybeRefOrGetter} value The value to clamp + * @param {MaybeRefOrGetter} min The minimum value + * @param {MaybeRefOrGetter} max The maximum value + * @returns {ComputedRef} The clamped value + * + * @example + * const value = ref(10); + * const clampedValue = useClamp(value, 0, 5); + * + * @example + * const value = ref(10); + * const clampedValue = useClamp(value, () => 0, () => 5); + */ +export function useClamp(value: MaybeRef, min: MaybeRefOrGetter, max: MaybeRefOrGetter): WritableComputedRef; +export function useClamp(value: MaybeRefOrGetter, min: MaybeRefOrGetter, max: MaybeRefOrGetter): ComputedRef { + if (isFunction(value) || isReadonly(value)) + return computed(() => clamp(toValue(value), toValue(min), toValue(max))); + + const _value = ref(value); + + return computed({ + get() { + return clamp(_value.value, toValue(min), toValue(max)); + }, + set(newValue) { + _value.value = clamp(newValue, toValue(min), toValue(max)); + }, + }); +}