Skip to content
This repository was archived by the owner on Sep 20, 2024. It is now read-only.

Commit 5b1a860

Browse files
feat(use-counter): create useCounter composable with example
1 parent 1cb3773 commit 5b1a860

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div>
3+
<pre>
4+
{{
5+
JSON.stringify({
6+
value,
7+
valueAsNumber,
8+
isOutOfRange,
9+
})
10+
}}
11+
</pre>
12+
<br />
13+
<button @click="increment()">Click Me to Increase!</button>
14+
<br />
15+
<button @click="decrement()">Click Me to Decrease!</button>
16+
<br />
17+
<button @click="update(4)">Update to 4!</button>
18+
<br />
19+
<input v-model="value" :style="{ background: 'transparent' }" />
20+
</div>
21+
</template>
22+
<script setup lang="ts">
23+
import { useCounter } from "../index"
24+
25+
const { increment, decrement, valueAsNumber, update, value, isOutOfRange } =
26+
useCounter({
27+
max: 10,
28+
min: 0,
29+
keepWithinRange: false,
30+
precision: 4,
31+
step: 0.5,
32+
})
33+
</script>

packages/vue-composables/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from "./use-window-event"
44
export * from "./use-element-stack"
55
export * from "./use-clipboard"
66
export * from "./use-disclosure"
7+
export * from "./use-counter"
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { clampValue, countDecimalPlaces, toPrecision } from "@chakra-ui/utils"
2+
import { computed, ref, watchEffect } from "vue"
3+
4+
export interface UseCounterProps {
5+
/**
6+
* The number of decimal points used to round the value
7+
*/
8+
precision?: number
9+
/**
10+
* The initial value of the counter. Should be less than `max` and greater than `min`
11+
*/
12+
defaultValue?: string | number
13+
/**
14+
* The step used to increment or decrement the value
15+
* @default 1
16+
*/
17+
step?: number
18+
/**
19+
* The minimum value of the counter
20+
* @default Number.MIN_SAFE_INTEGER
21+
*/
22+
min?: number
23+
/**
24+
* The maximum value of the counter
25+
* @default Number.MAX_SAFE_INTEGER
26+
*/
27+
max?: number
28+
/**
29+
* This controls the value update behavior in general.
30+
*
31+
* - If `true` and you use the stepper or up/down arrow keys,
32+
* the value will not exceed the `max` or go lower than `min`
33+
*
34+
* - If `false`, the value will be allowed to go out of range.
35+
*
36+
* @default true
37+
*/
38+
keepWithinRange?: boolean
39+
}
40+
41+
/**
42+
* Composable providing step functionality
43+
*/
44+
export function useCounter(props: UseCounterProps = {}) {
45+
const {
46+
precision: precisionProp,
47+
defaultValue = 0,
48+
step: stepProp = 1,
49+
min = Number.MIN_SAFE_INTEGER,
50+
max = Number.MAX_SAFE_INTEGER,
51+
keepWithinRange = true,
52+
} = props
53+
54+
const valueState = ref<string | number>(
55+
(() => {
56+
if (defaultValue == null) return ""
57+
return cast(defaultValue, stepProp, precisionProp) ?? ""
58+
})()
59+
)
60+
61+
const decimalPlaces = ref(0)
62+
63+
let precision: number
64+
65+
const update = (next: string | number) => {
66+
if (next === valueState.value) return
67+
valueState.value = toPrecision(parse(next), precision).toString()
68+
}
69+
70+
const clamp = (value: number) => {
71+
let nextValue = value
72+
73+
if (keepWithinRange) {
74+
nextValue = clampValue(nextValue, min, max)
75+
}
76+
77+
return toPrecision(nextValue, precision)
78+
}
79+
80+
const increment = (step: number = stepProp) => {
81+
let next: string | number
82+
83+
/**
84+
* Let's follow the native browser behavior for
85+
* scenarios where the input starts empty ("")
86+
*/
87+
if (valueState.value === "") {
88+
/**
89+
* If `min` is set, native input, starts at the `min`.
90+
* Else, it starts at `step`
91+
*/
92+
next = parse(step)
93+
} else {
94+
next = parse(valueState.value) + step
95+
}
96+
97+
next = clamp(next)
98+
update(next)
99+
}
100+
101+
const decrement = (step = stepProp) => {
102+
let next: string | number
103+
104+
// Follow native implementation
105+
if (valueState.value === "") {
106+
next = parse(-step)
107+
} else {
108+
next = parse(valueState.value) - step
109+
}
110+
111+
next = clamp(next)
112+
update(next)
113+
}
114+
115+
const isOutOfRange = ref(false)
116+
117+
const valueAsNumber = ref(0)
118+
119+
watchEffect(() => {
120+
decimalPlaces.value = getDecimalPlaces(parse(valueState.value), stepProp)
121+
122+
precision = precisionProp ?? decimalPlaces.value
123+
124+
valueAsNumber.value = parse(valueState.value)
125+
126+
isOutOfRange.value = valueAsNumber.value > max || valueAsNumber.value < min
127+
})
128+
129+
return {
130+
decrement,
131+
increment,
132+
update,
133+
valueAsNumber,
134+
isOutOfRange,
135+
value: valueState,
136+
}
137+
}
138+
139+
function parse(value: string | number) {
140+
return parseFloat(value.toString().replace(/[^\w.-]+/g, ""))
141+
}
142+
143+
function getDecimalPlaces(value: number, step: number) {
144+
return Math.max(countDecimalPlaces(step), countDecimalPlaces(value))
145+
}
146+
147+
function cast(value: string | number, step: number, precision?: number) {
148+
const parsedValue = parse(value)
149+
if (Number.isNaN(parsedValue)) return undefined
150+
151+
const decimalPlaces = getDecimalPlaces(parsedValue, step)
152+
return toPrecision(parsedValue, precision ?? decimalPlaces)
153+
}

0 commit comments

Comments
 (0)