Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 69cedd2

Browse filesBrowse files
rtugeekhuang-julienantfu
authored
feat(useCountdown): new function (#4125)
Co-authored-by: Julien Huang <julien.h.dev@gmail.com> Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com> Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 48e0a2e commit 69cedd2
Copy full SHA for 69cedd2

File tree

Expand file treeCollapse file tree

4 files changed

+319
-0
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+319
-0
lines changed

‎packages/core/useCountdown/demo.vue

Copy file name to clipboard
+85Lines 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

Copy file name to clipboard
+23Lines 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+
```
+113Lines 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

Copy file name to clipboard
+98Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.