Skip to content
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

Feature/10 add usetimeout and useinterval hooks #16

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
32a9035
Add the useTimeout hook
larsvanbraam Apr 22, 2022
8487c27
Update the useTimeout hook to return more explicit methods
larsvanbraam Apr 22, 2022
90ff012
Add the useInterval hook
larsvanbraam Apr 26, 2022
d6b2e6a
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam Apr 26, 2022
2659a29
Implement the storybook log on the useTimeout hook
larsvanbraam Apr 26, 2022
a424f75
Remove the excessive space
larsvanbraam Apr 26, 2022
5f65389
Update the timeout stories
larsvanbraam Apr 26, 2022
c9adccd
Update the useInterval stories and tests
larsvanbraam Apr 26, 2022
65f03be
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam Apr 26, 2022
baadfb6
Choose set in favour of create because it matches the native name
larsvanbraam Apr 29, 2022
dcaabc6
Merge branch 'main' into feature/10-add-usetimeout-and-useinterval-hooks
larsvanbraam Apr 29, 2022
ec95546
Update the muban peer dependency version
larsvanbraam Apr 29, 2022
a0fc164
Expose the state of the interval
larsvanbraam Apr 29, 2022
6189557
Remove the unused ref import
larsvanbraam Apr 29, 2022
1a4e173
Move the ref import to the main one
larsvanbraam Apr 29, 2022
d9c3bed
Implement the userEvent on the userIntervalStories.test
larsvanbraam Apr 29, 2022
55bd78e
Implement the userEvent on the useTimeoutStories.test
larsvanbraam Apr 29, 2022
28aa255
Implement jest.useFakeTimers on the main tests
larsvanbraam Apr 29, 2022
a48a9a6
Rename the handle variable to intervalId
larsvanbraam Apr 29, 2022
d4a7889
Rename the handle variable to timeoutId
larsvanbraam Apr 29, 2022
c56236b
Increase the time to ensure stop actually works
larsvanbraam Apr 29, 2022
96276ce
Remove the story that tests the stop button
larsvanbraam Apr 29, 2022
6778653
Move up the callback methods
larsvanbraam Apr 29, 2022
6a55551
Update the test description
larsvanbraam Apr 29, 2022
078f18b
Fix prettier formatting
larsvanbraam Apr 29, 2022
0174fe2
Switch to a Readonly Ref instead of a ComputedRef
larsvanbraam Apr 29, 2022
8d8de00
Use the intervalId to keep track if the interval is running.
larsvanbraam Apr 29, 2022
4f85184
Implement the isTimeoutRunning state in the useTimeout hook to stay c…
larsvanbraam Apr 29, 2022
483fc83
Update the docs to include the new isTimeoutRunning
larsvanbraam Apr 29, 2022
70a6a7c
Add a test to check if the isTimeoutRunning is correctly set
larsvanbraam Apr 29, 2022
13e6fb5
Include the isIntervalRunning type to the useInterval docs
larsvanbraam Apr 29, 2022
e8df249
Remove the usage of the actual timeout in the tests
larsvanbraam Apr 29, 2022
979e56e
rename cancelTimeout to clearTimeout to be more consistent with the n…
larsvanbraam Apr 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"plop": "plop"
},
"peerDependencies": {
"@muban/muban": "^1.0.0-alpha.28"
"@muban/muban": "^1.0.0-alpha.34"
},
"devDependencies": {
"@babel/core": "^7.12.10",
Expand Down
73 changes: 73 additions & 0 deletions src/useInterval/useInterval.stories.mdx
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.
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved

## Reference

```ts
function useInterval(
callback: () => void,
interval?: number = 100,
startImmediate?: boolean = true,
): { startInterval: () => void, stopInterval: () => void }
```

### 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.
}
})
]
}
})
```
86 changes: 86 additions & 0 deletions src/useInterval/useInterval.stories.ts
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, ref } 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);

const { startInterval, stopInterval, isIntervalRunning } = useInterval(
onInterval,
props.interval,
props.startImmediate,
);

function onInterval() {
log('interval called');
}

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';
98 changes: 98 additions & 0 deletions src/useInterval/useInterval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { runComponentSetup } from '@muban/test-utils';
import { timeout } from '../useTimeout/useTimeout.test.utils';
import { useInterval } from './useInterval';

jest.mock('@muban/muban', () => jest.requireActual('@muban/test-utils').getMubanLifecycleMock());

describe('useInterval', () => {
it('should not crash', async () => {
await runComponentSetup(() => {
useInterval(() => undefined);
});
});

it('should start immediate and not be completed', async () => {
const mockHandler = jest.fn();

await runComponentSetup(() => {
useInterval(mockHandler, 100);
});

expect(mockHandler).toBeCalledTimes(0);
});

it('should start immediate and be called once', async () => {
const mockHandler = jest.fn();

await runComponentSetup(
() => useInterval(mockHandler, 100),
async ({ stopInterval }) => {
await timeout(100);
stopInterval();
},
);

expect(mockHandler).toBeCalledTimes(1);
});

it('should trigger start and be stopped after three calls', async () => {
const mockHandler = jest.fn();

await runComponentSetup(
() => useInterval(mockHandler, 100, false),
async ({ startInterval, stopInterval }) => {
startInterval();
await timeout(400);
stopInterval();
},
);

expect(mockHandler).toBeCalledTimes(3);
});

it('should trigger stop once the interval is started', async () => {
const mockHandler = jest.fn();

await runComponentSetup(
() => useInterval(mockHandler, 200, false),
async ({ startInterval, stopInterval }) => {
startInterval();
await timeout(100);
stopInterval();
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved
},
);

expect(mockHandler).toBeCalledTimes(0);
});

it('should know that the interval is running', async () => {
const mockHandler = jest.fn();

await runComponentSetup(
() => useInterval(mockHandler, 200, false),
async ({ startInterval, stopInterval, isIntervalRunning }) => {
startInterval();
await timeout(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', async () => {
const mockHandler = jest.fn();

await runComponentSetup(
() => useInterval(mockHandler, 100, false),
async ({ startInterval }) => {
startInterval();
await timeout(50);
startInterval();
await timeout(100);
},
);

expect(mockHandler).toBeCalledTimes(1);
});
});
49 changes: 49 additions & 0 deletions src/useInterval/useInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ComputedRef } from '@muban/muban';
import { computed, onMounted, onUnmounted } from '@muban/muban';
import { ref } from '@muban/muban/dist/esm';

/**
* 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 isIntervalRunning = ref(false);
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved
let handle = -1;
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved

function start() {
stop();
isIntervalRunning.value = true;
handle = setInterval(callback, interval) as unknown as number;
}

function stop() {
isIntervalRunning.value = false;
clearInterval(handle);
}

onUnmounted(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here
This could be

onUnmounted(stop)

stop();
});

onMounted(() => {
if (startImmediate) start();
});

return {
startInterval: start,
stopInterval: stop,
isIntervalRunning: computed(() => isIntervalRunning.value),
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved
};
};
39 changes: 39 additions & 0 deletions src/useInterval/useIntervalStories.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import '@testing-library/jest-dom';
import { waitFor, render } from '@muban/testing-library';
import { Demo } from './useInterval.stories';
import { timeout } from '../useTimeout/useTimeout.test.utils';

describe('useInterval stories', () => {
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');

startButton.click();

await waitFor(() => expect(getByText('interval called')).toBeInTheDocument());
});

it('should stop the interval after starting', async () => {
const { getByText, getByRef } = render(Demo, { interval: 100 });
const startButton = getByRef('start-button');
const stopButton = getByRef('stop-button');

startButton.click();
await timeout(100);
stopButton.click();

await waitFor(() => expect(getByText('interval stopped')).toBeInTheDocument());
larsvanbraam marked this conversation as resolved.
Show resolved Hide resolved
});
});
Loading