-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/10 add usetimeout and useinterval hooks #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
larsvanbraam
wants to merge
33
commits into
main
Choose a base branch
from
feature/10-add-usetimeout-and-useinterval-hooks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
32a9035
Add the useTimeout hook
larsvanbraam 8487c27
Update the useTimeout hook to return more explicit methods
larsvanbraam 90ff012
Add the useInterval hook
larsvanbraam d6b2e6a
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam 2659a29
Implement the storybook log on the useTimeout hook
larsvanbraam a424f75
Remove the excessive space
larsvanbraam 5f65389
Update the timeout stories
larsvanbraam c9adccd
Update the useInterval stories and tests
larsvanbraam 65f03be
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam baadfb6
Choose set in favour of create because it matches the native name
larsvanbraam dcaabc6
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam ec95546
Update the muban peer dependency version
larsvanbraam a0fc164
Expose the state of the interval
larsvanbraam 6189557
Remove the unused ref import
larsvanbraam 1a4e173
Move the ref import to the main one
larsvanbraam d9c3bed
Implement the userEvent on the userIntervalStories.test
larsvanbraam 55bd78e
Implement the userEvent on the useTimeoutStories.test
larsvanbraam 28aa255
Implement jest.useFakeTimers on the main tests
larsvanbraam a48a9a6
Rename the handle variable to intervalId
larsvanbraam d4a7889
Rename the handle variable to timeoutId
larsvanbraam c56236b
Increase the time to ensure stop actually works
larsvanbraam 96276ce
Remove the story that tests the stop button
larsvanbraam 6778653
Move up the callback methods
larsvanbraam 6a55551
Update the test description
larsvanbraam 078f18b
Fix prettier formatting
larsvanbraam 0174fe2
Switch to a Readonly Ref instead of a ComputedRef
larsvanbraam 8d8de00
Use the intervalId to keep track if the interval is running.
larsvanbraam 4f85184
Implement the isTimeoutRunning state in the useTimeout hook to stay c…
larsvanbraam 483fc83
Update the docs to include the new isTimeoutRunning
larsvanbraam 70a6a7c
Add a test to check if the isTimeoutRunning is correctly set
larsvanbraam 13e6fb5
Include the isIntervalRunning type to the useInterval docs
larsvanbraam e8df249
Remove the usage of the actual timeout in the tests
larsvanbraam 979e56e
rename cancelTimeout to clearTimeout to be more consistent with the n…
larsvanbraam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { Meta } from '@storybook/addon-docs'; | ||
|
|
||
| <Meta | ||
| title="useInterval/docs" | ||
| /> | ||
|
|
||
| # useInterval | ||
|
|
||
| The `useInterval` hook is a wrapper around the native `setInterval`, it allows you to easily set | ||
| an interval within your component that will be auto cancelled when the component unmounts. | ||
|
|
||
| ## Reference | ||
|
|
||
| ```ts | ||
| function useInterval( | ||
| callback: () => void, | ||
| interval?: number = 100, | ||
| startImmediate?: boolean = true, | ||
| ): { startInterval: () => void, stopInterval: () => void; isIntervalRunning: ComputedRef<boolean> } | ||
| ``` | ||
|
|
||
| ### Parameters | ||
| * `callback` – The callback you want to trigger once the interval runs. | ||
| * `interval` - The duration of the interval you want to create. | ||
| * `startImmediate` - Whether or not you want to immediately start the interval. | ||
|
|
||
| ### Returns | ||
| * `{ startInterval, stopInterval, isIntervalRunning }` | ||
| * `startInterval` – A function that starts the interval, any running intervals will automatically be stopped. | ||
| * `stopInterval` – A function that will stop the current active interval. | ||
| * `isIntervalRunning` – A computed ref that keeps track whether or not the interval is running. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```ts | ||
| const { startInterval, stopInterval, isIntervalRunning } = useInterval(() => { | ||
| console.log('The interval has run') | ||
| }, 1000, false); | ||
| ```` | ||
|
|
||
| ```ts | ||
| const Demo = defineComponent({ | ||
| name: 'demo', | ||
| refs: { | ||
| startBtn: 'start-button' | ||
| stopButton: 'stop-button' | ||
| }, | ||
| setup({ refs }) { | ||
| // The interval starts as soon as the component is mounted. | ||
| useInterval(() => { | ||
| console.log('The immediate interval callback is triggered.') | ||
| }, 1000); | ||
|
|
||
| // The interval doesn't start automatically, but requires a user action to start. | ||
| const { startInterval, stopInterval, isIntervalRunning } = useInterval(() => { | ||
| console.log('The user-action interval callback is triggered.') | ||
| }, 1000, false); | ||
|
|
||
| return [ | ||
| bind(refs.startBtn, { | ||
| click() { | ||
| startInterval(); // This actually starts the interval. | ||
| } | ||
| }), | ||
| bind(refs.stopButton, { | ||
| click() { | ||
| stopInterval(); // This stops the interval if it's active. | ||
| } | ||
| }) | ||
| ] | ||
| } | ||
| }) | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import { bind, computed, defineComponent, propType } from '@muban/muban'; | ||
| import type { Story } from '@muban/storybook/types-6-0'; | ||
| import { html } from '@muban/template'; | ||
| import { useInterval } from './useInterval'; | ||
| import { useStorybookLog } from '../hooks/useStorybookLog'; | ||
|
|
||
| export default { | ||
| title: 'useInterval', | ||
| }; | ||
|
|
||
| type DemoStoryProps = { startImmediate?: boolean; interval?: number }; | ||
|
|
||
| export const Demo: Story<DemoStoryProps> = () => ({ | ||
| component: defineComponent({ | ||
| name: 'story', | ||
| props: { | ||
| startImmediate: propType.boolean.defaultValue(false), | ||
| interval: propType.number, | ||
| }, | ||
| refs: { | ||
| label: 'label', | ||
| startButton: 'start-button', | ||
| stopButton: 'stop-button', | ||
| }, | ||
| setup({ refs, props }) { | ||
| const [logBinding, log] = useStorybookLog(refs.label); | ||
|
|
||
| function onInterval() { | ||
| log('interval called'); | ||
| } | ||
|
|
||
| const { startInterval, stopInterval, isIntervalRunning } = useInterval( | ||
| onInterval, | ||
| props.interval, | ||
| props.startImmediate, | ||
| ); | ||
|
|
||
| return [ | ||
| logBinding, | ||
| bind(refs.startButton, { | ||
| attr: { | ||
| disabled: isIntervalRunning, | ||
| }, | ||
| click() { | ||
| startInterval(); | ||
| }, | ||
| }), | ||
| bind(refs.stopButton, { | ||
| attr: { | ||
| disabled: computed(() => !isIntervalRunning.value), | ||
| }, | ||
| click() { | ||
| log('interval stopped'); | ||
| stopInterval(); | ||
| }, | ||
| }), | ||
| ]; | ||
| }, | ||
| }), | ||
| template: ({ startImmediate = false, interval = 2500 }: DemoStoryProps = {}) => html`<div | ||
| data-component="story" | ||
| data-start-immediate=${startImmediate} | ||
| data-interval=${interval} | ||
| > | ||
| <div class="alert alert-primary"> | ||
| <h4 class="alert-heading">Instructions!</h4> | ||
| <p class="mb-0"> | ||
| The demo interval is set to 2.5 seconds, you can start it by clicking the start button. You | ||
| can stop the interval by clicking the stop button. | ||
| </p> | ||
| </div> | ||
| <div data-ref="label" /> | ||
| <div class="card border-dark"> | ||
| <div class="card-header">Test Area</div> | ||
| <div class="card-body"> | ||
| <button type="button" data-ref="start-button" class="btn btn-primary"> | ||
| Start interval | ||
| </button> | ||
| ${' '} | ||
| <button type="button" data-ref="stop-button" class="btn btn-danger">Stop interval</button> | ||
| </div> | ||
| </div> | ||
| </div>`, | ||
| }); | ||
| Demo.storyName = 'demo'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { runComponentSetup } from '@muban/test-utils'; | ||
| import { useInterval } from './useInterval'; | ||
|
|
||
| jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock()); | ||
|
|
||
| describe('useInterval', () => { | ||
| beforeEach(() => { | ||
| jest.useFakeTimers(); | ||
| }); | ||
|
|
||
| it('should not crash', () => { | ||
| runComponentSetup(() => { | ||
| useInterval(() => undefined); | ||
| }); | ||
| }); | ||
|
|
||
| it('should start immediate and not be completed', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup(() => { | ||
| useInterval(mockHandler, 100); | ||
| }); | ||
|
|
||
| expect(mockHandler).toBeCalledTimes(0); | ||
| }); | ||
|
|
||
| it('should start immediate and be called once', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup( | ||
| () => useInterval(mockHandler, 100), | ||
| ({ stopInterval }) => { | ||
| jest.advanceTimersByTime(100); | ||
| stopInterval(); | ||
| }, | ||
| ); | ||
|
|
||
| expect(mockHandler).toBeCalledTimes(1); | ||
| }); | ||
|
|
||
| it('should trigger start and be stopped after three calls', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup( | ||
| () => useInterval(mockHandler, 100, false), | ||
| ({ startInterval, stopInterval }) => { | ||
| startInterval(); | ||
| jest.advanceTimersByTime(300); | ||
| stopInterval(); | ||
| }, | ||
| ); | ||
|
|
||
| expect(mockHandler).toBeCalledTimes(3); | ||
| }); | ||
|
|
||
| it('should trigger stop once the interval is started', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup( | ||
| () => useInterval(mockHandler, 200, false), | ||
| ({ startInterval, stopInterval }) => { | ||
| startInterval(); | ||
| jest.advanceTimersByTime(100); | ||
| stopInterval(); | ||
larsvanbraam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| jest.advanceTimersByTime(200); | ||
| }, | ||
| ); | ||
|
|
||
| expect(mockHandler).toBeCalledTimes(0); | ||
| }); | ||
|
|
||
| it('should know that the interval is running', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup( | ||
| () => useInterval(mockHandler, 200, false), | ||
| ({ startInterval, stopInterval, isIntervalRunning }) => { | ||
| startInterval(); | ||
| jest.advanceTimersByTime(100); | ||
| expect(isIntervalRunning.value).toEqual(true); | ||
| stopInterval(); | ||
| expect(isIntervalRunning.value).toEqual(false); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should start a new interval before the old one was triggered and only complete once', () => { | ||
| const mockHandler = jest.fn(); | ||
|
|
||
| runComponentSetup( | ||
| () => useInterval(mockHandler, 100, false), | ||
| ({ startInterval }) => { | ||
| startInterval(); | ||
| jest.advanceTimersByTime(50); | ||
| startInterval(); | ||
| jest.advanceTimersByTime(100); | ||
| }, | ||
| ); | ||
|
|
||
| expect(mockHandler).toBeCalledTimes(1); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { ComputedRef } from '@muban/muban'; | ||
| import { ref, onMounted, onUnmounted, computed } from '@muban/muban'; | ||
|
|
||
| // We use `-1` as the value to indicate that an interval is not running. | ||
| const NOT_RUNNING = -1; | ||
|
|
||
| /** | ||
| * A hook that can be used to call a function on a provided interval, by default the interval | ||
| * will run immediate. You can also start and cancel the interval whenever needed. | ||
| * | ||
| * @param callback The callback you want to trigger once the interval runs. | ||
| * @param interval The duration of the interval you want to create. | ||
| * @param startImmediate Whether or not you want to immediately start the interval. | ||
| */ | ||
| export const useInterval = ( | ||
| callback: () => void, | ||
| interval: number = 100, | ||
| startImmediate: boolean = true, | ||
| ): { | ||
| startInterval: () => void; | ||
| stopInterval: () => void; | ||
| isIntervalRunning: ComputedRef<boolean>; | ||
| } => { | ||
| const intervalId = ref<number>(NOT_RUNNING); | ||
|
|
||
| function start() { | ||
| stop(); | ||
| intervalId.value = setInterval(callback, interval) as unknown as number; | ||
| } | ||
|
|
||
| function stop() { | ||
| clearInterval(intervalId.value); | ||
| intervalId.value = NOT_RUNNING; | ||
| } | ||
|
|
||
| onUnmounted(() => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as here onUnmounted(stop) |
||
| stop(); | ||
| }); | ||
|
|
||
| onMounted(() => { | ||
| if (startImmediate) start(); | ||
| }); | ||
|
|
||
| return { | ||
| startInterval: start, | ||
| stopInterval: stop, | ||
| isIntervalRunning: computed(() => intervalId.value !== NOT_RUNNING), | ||
| }; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import '@testing-library/jest-dom'; | ||
| import { waitFor, render } from '@muban/testing-library'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { Demo } from './useInterval.stories'; | ||
|
|
||
| describe('useInterval stories', () => { | ||
| const { click } = userEvent.setup(); | ||
|
|
||
| it('should render', () => { | ||
| const { getByText } = render(Demo); | ||
|
|
||
| expect(getByText('Test Area')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should start immediate and be called after 100ms', async () => { | ||
| const { getByText } = render(Demo, { startImmediate: true, interval: 100 }); | ||
|
|
||
| await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); | ||
| }); | ||
|
|
||
| it('should start after clicking start and be called after 100ms', async () => { | ||
| const { getByText, getByRef } = render(Demo, { interval: 100 }); | ||
| const startButton = getByRef('start-button'); | ||
|
|
||
| click(startButton); | ||
|
|
||
| await waitFor(() => expect(getByText('interval called')).toBeInTheDocument()); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.