Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 69cedd2

Browse files
rtugeekhuang-julienantfu
authored
feat(useCountdown): new function (#4125)
Co-authored-by: Julien Huang <[email protected]> Co-authored-by: Anthony Fu <[email protected]> Co-authored-by: Anthony Fu <[email protected]>
1 parent 48e0a2e commit 69cedd2

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

packages/core/useCountdown/demo.vue

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script setup lang="ts">
2+
import { useEventListener } from '@vueuse/core'
3+
import { ref } from 'vue'
4+
import { useCountdown } from './index'
5+
6+
const countdownSeconds = ref(5)
7+
const rocketRef = ref<HTMLDivElement>()
8+
const { remaining, start, stop, pause, resume } = useCountdown(countdownSeconds, {
9+
onComplete() {
10+
rocketRef.value!.classList.add('launching')
11+
},
12+
onTick() {
13+
14+
},
15+
})
16+
17+
function startCountdown() {
18+
rocketRef.value!.classList.remove('launching')
19+
start(countdownSeconds)
20+
}
21+
22+
useEventListener(rocketRef, 'animationend', () => {
23+
rocketRef.value!.classList.remove('launching')
24+
})
25+
</script>
26+
27+
<template>
28+
<div class="flex flex-col items-center">
29+
<div ref="rocketRef" class="rocket">
30+
🚀
31+
</div>
32+
Rocket launch in {{ remaining }} seconds
33+
34+
<div class="flex items-center gap-2 mt-4">
35+
Countdown: <input v-model="countdownSeconds" type="number">
36+
</div>
37+
<div class="flex items-center gap-2 justify-center">
38+
<button @click="startCountdown">
39+
Start
40+
</button>
41+
<button @click="stop">
42+
Stop
43+
</button>
44+
<button @click="pause">
45+
Pause
46+
</button>
47+
<button @click="resume">
48+
Resume
49+
</button>
50+
</div>
51+
</div>
52+
</template>
53+
54+
<style>
55+
input {
56+
width: 40px;
57+
}
58+
59+
:root {
60+
--rocket-rotate: rotate(-45deg);
61+
}
62+
@keyframes rocket {
63+
0% {
64+
transform: translateY(0) var(--rocket-rotate);
65+
}
66+
50% {
67+
transform: translateY(-200px) var(--rocket-rotate);
68+
}
69+
100% {
70+
transform: translateY(0) var(--rocket-rotate);
71+
}
72+
}
73+
74+
.rocket {
75+
transform: var(--rocket-rotate);
76+
}
77+
78+
.rocket.launching {
79+
animation-fill-mode: forwards;
80+
animation-play-state: running;
81+
animation-duration: 4s;
82+
animation-timing-function: ease-in-out;
83+
animation-name: rocket;
84+
}
85+
</style>

packages/core/useCountdown/index.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
category: Time
3+
---
4+
5+
# useCountdown
6+
7+
Wrapper for `useIntervalFn` that provides a countdown timer.
8+
9+
## Usage
10+
11+
```js
12+
import { useCountdown } from '@vueuse/core'
13+
14+
const countdownSeconds = 5
15+
const { remaining, start, stop, pause, resume } = useCountdown(countdownSeconds, {
16+
onComplete() {
17+
18+
},
19+
onTick() {
20+
21+
}
22+
})
23+
```
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { Pausable } from '@vueuse/shared'
2+
import type { UseCountdownOptions } from '.'
3+
import { promiseTimeout } from '@vueuse/shared'
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { effectScope } from 'vue'
6+
import { useCountdown } from '.'
7+
8+
describe('useCountdown', () => {
9+
let tickCallback = vi.fn()
10+
let completeCallback = vi.fn()
11+
let countdown = 3
12+
let interval = 100
13+
const immediate = true
14+
let options: UseCountdownOptions = {
15+
interval,
16+
onComplete: completeCallback,
17+
onTick: tickCallback,
18+
immediate,
19+
}
20+
beforeEach(() => {
21+
tickCallback = vi.fn()
22+
completeCallback = vi.fn()
23+
countdown = 3
24+
interval = 100
25+
options = {
26+
interval,
27+
onComplete: completeCallback,
28+
onTick: tickCallback,
29+
immediate,
30+
}
31+
})
32+
33+
async function exec({ isActive, pause, resume }: Pausable) {
34+
expect(isActive.value).toBeTruthy()
35+
expect(completeCallback).toHaveBeenCalledTimes(0)
36+
await promiseTimeout(110)
37+
expect(tickCallback).toHaveBeenCalledTimes(1)
38+
39+
pause()
40+
expect(isActive.value).toBeFalsy()
41+
42+
await promiseTimeout(110)
43+
expect(tickCallback).toHaveBeenCalledTimes(1)
44+
45+
resume()
46+
expect(isActive.value).toBeTruthy()
47+
48+
await promiseTimeout(110)
49+
expect(tickCallback).toHaveBeenCalledTimes(2)
50+
51+
await promiseTimeout(110)
52+
expect(tickCallback).toHaveBeenCalledTimes(3)
53+
expect(completeCallback).toHaveBeenCalledTimes(1)
54+
}
55+
56+
it('basic start/stop', async () => {
57+
const { isActive, stop, start, remaining } = useCountdown(countdown, options)
58+
expect(isActive.value).toBeTruthy()
59+
expect(completeCallback).toHaveBeenCalledTimes(0)
60+
61+
await promiseTimeout(110)
62+
63+
expect(tickCallback).toHaveBeenCalledTimes(1)
64+
expect(completeCallback).toHaveBeenCalledTimes(0)
65+
66+
stop()
67+
expect(isActive.value).toBeFalsy()
68+
await promiseTimeout(110)
69+
70+
expect(tickCallback).toHaveBeenCalledTimes(1)
71+
expect(remaining.value).toBe(countdown)
72+
73+
tickCallback.mockClear()
74+
completeCallback.mockClear()
75+
76+
start()
77+
78+
expect(isActive.value).toBeTruthy()
79+
await promiseTimeout(210)
80+
81+
expect(tickCallback).toHaveBeenCalledTimes(2)
82+
expect(completeCallback).toHaveBeenCalledTimes(0)
83+
84+
expect(remaining.value).toBe(1)
85+
86+
await promiseTimeout(110)
87+
expect(remaining.value).toBe(0)
88+
expect(completeCallback).toHaveBeenCalledTimes(1)
89+
})
90+
91+
it('basic pause/resume', async () => {
92+
await exec(useCountdown(countdown, options))
93+
})
94+
95+
it('pause/resume in scope', async () => {
96+
const scope = effectScope()
97+
await scope.run(async () => {
98+
await exec(useCountdown(countdown, options))
99+
})
100+
tickCallback.mockClear()
101+
await scope.stop()
102+
await promiseTimeout(300)
103+
expect(tickCallback).toHaveBeenCalledTimes(0)
104+
})
105+
106+
it('cant work when interval is negative', async () => {
107+
const { isActive } = useCountdown(5, { interval: -1 })
108+
109+
expect(isActive.value).toBeFalsy()
110+
await promiseTimeout(60)
111+
expect(tickCallback).toHaveBeenCalledTimes(0)
112+
})
113+
})

packages/core/useCountdown/index.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { MaybeRefOrGetter, Pausable } from '@vueuse/shared'
2+
import type { Ref } from 'vue'
3+
import { useIntervalFn } from '@vueuse/shared'
4+
import { ref, toValue } from 'vue'
5+
6+
export interface UseCountdownOptions {
7+
/**
8+
* Interval for the countdown in milliseconds. Default is 1000ms.
9+
*/
10+
interval?: MaybeRefOrGetter<number>
11+
/**
12+
* Callback function called when the countdown reaches 0.
13+
*/
14+
onComplete?: () => void
15+
/**
16+
* Callback function called on each tick of the countdown.
17+
*/
18+
onTick?: () => void
19+
/**
20+
* Start the countdown immediately
21+
*
22+
* @default false
23+
*/
24+
immediate?: boolean
25+
}
26+
27+
export interface UseCountdownReturn extends Pausable {
28+
/**
29+
* Current countdown value.
30+
*/
31+
remaining: Ref<number>
32+
/**
33+
* Resets the countdown and repeatsLeft to their initial values.
34+
*/
35+
reset: () => void
36+
/**
37+
* Stops the countdown and resets its state.
38+
*/
39+
stop: () => void
40+
/**
41+
* Reset the countdown and start it again.
42+
*/
43+
start: (initialCountdown?: MaybeRefOrGetter<number>) => void
44+
}
45+
46+
/**
47+
* Wrapper for `useIntervalFn` that provides a countdown timer in seconds.
48+
*
49+
* @param initialCountdown
50+
* @param options
51+
*
52+
* @see https://vueuse.org/useCountdown
53+
*/
54+
export function useCountdown(initialCountdown: MaybeRefOrGetter<number>, options?: UseCountdownOptions): UseCountdownReturn {
55+
const remaining = ref(toValue(initialCountdown))
56+
57+
const intervalController = useIntervalFn(() => {
58+
const value = remaining.value - 1
59+
remaining.value = value < 0 ? 0 : value
60+
options?.onTick?.()
61+
if (remaining.value <= 0) {
62+
intervalController.pause()
63+
options?.onComplete?.()
64+
}
65+
}, options?.interval ?? 1000, { immediate: options?.immediate ?? false })
66+
67+
const reset = () => {
68+
remaining.value = toValue(initialCountdown)
69+
}
70+
71+
const stop = () => {
72+
intervalController.pause()
73+
reset()
74+
}
75+
76+
const resume = () => {
77+
if (!intervalController.isActive.value) {
78+
if (remaining.value > 0) {
79+
intervalController.resume()
80+
}
81+
}
82+
}
83+
84+
const start = () => {
85+
reset()
86+
intervalController.resume()
87+
}
88+
89+
return {
90+
remaining,
91+
reset,
92+
stop,
93+
start,
94+
pause: intervalController.pause,
95+
resume,
96+
isActive: intervalController.isActive,
97+
}
98+
}

0 commit comments

Comments
 (0)