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

Use event listener composition #293

Merged
merged 7 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# vue-compositions

A collection of reusable vue compositions

## Installation
```

```bash
npm i --save @prefecthq/vue-compositions
```

## Compositions

- [useBoolean](https://github.com/prefecthq/vue-compositions/tree/main/src/useBoolean)
- [useChildrenAreWrapped](https://github.com/prefecthq/vue-compositions/tree/main/src/useChildrenAreWrapped)
- [useComputedStyle](https://github.com/prefecthq/vue-compositions/tree/main/src/useComputedStyle)
- [useDebouncedRef](https://github.com/prefecthq/vue-compositions/tree/main/src/useDebouncedRef)
- [useElementRect](https://github.com/prefecthq/vue-compositions/tree/main/src/useElementRect)
- [useElementWidth](https://github.com/prefecthq/vue-compositions/tree/main/src/useElementWidth)
- [useEventListener](https://github.com/prefecthq/vue-compositions/tree/main/src/useEventListener)
- [useGlobalEventListener](https://github.com/prefecthq/vue-compositions/tree/main/src/useGlobalEventListener)
- [useIntersectionObserver](https://github.com/prefecthq/vue-compositions/tree/main/src/useIntersectionObserver)
- [useIsSame](https://github.com/prefecthq/vue-compositions/tree/main/src/useIsSame)
Expand Down
1 change: 1 addition & 0 deletions src/types/maybe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { Ref, UnwrapRef } from 'vue'
export type MaybePromise<T = unknown> = T | Promise<T>
export type MaybeRef<T = unknown> = T | Ref<T>
export type MaybeUnwrapRef<T = unknown> = T | UnwrapRef<T>
export type MaybeRefOrGetter<T = unknown> = MaybeRef<T> | (() => T)
export type MaybeArray<T = unknown> = T | T[]
30 changes: 30 additions & 0 deletions src/useEventListener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# useEventListener

The `useEventListener` composition can be used to automatically handle setup and teardown of event listeners on the document or HTMElement scope. The options argument extends browser AddEventListenerOptions with `immediate` boolean, which defaults to `true` but when passed in as `false`, will prevent the composition from adding the listener automatically.

## Example

```typescript
import { useEventListener } from '@prefecthq/vue-compositions'

function handleEvent(event: MouseEvent) {
// Respond to event
}
const element = ref<HTMLDivElement | undefined>()
useEventListener(element, 'keyup', handleEvent)
```

## Arguments

| Name | Type | Default |
|-----------|-----------------------------------------------------------|-----------|
| type | `K (K extends keyof DocumentEventMap)` | None |
| callback | `(this: Document, event: DocumentEventMap[K]) => unknown` | None |
| options | `AddEventListenerOptions & { immediate: boolean }` | None |
stackoverfloweth marked this conversation as resolved.
Show resolved Hide resolved

## Returns

| Name | Type | Description |
|--------|-------------|---------------------------------------------------|
| add | () => void | Manually attach the event listener (has no effect if the event listener already exists) |
| remove | () => void | Manually detach the event listener |
1 change: 1 addition & 0 deletions src/useEventListener/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useEventListener'
49 changes: 49 additions & 0 deletions src/useEventListener/useEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { getCurrentScope, onScopeDispose, watch } from 'vue'
import { MaybeRefOrGetter } from '@/types/maybe'
import { toValue } from '@/utilities/vue'

export type UseEventListener = {
add: () => void,
remove: () => void,
}

export type UseEventListenerOptions = AddEventListenerOptions & {
immediate?: boolean,
}

const defaultOptions: UseEventListenerOptions = {
immediate: true,
}

export function useEventListener<K extends keyof DocumentEventMap>(target: MaybeRefOrGetter<Document | undefined | null>, key: K, callback: (this: Document, event: DocumentEventMap[K]) => unknown, options?: UseEventListenerOptions): UseEventListener
export function useEventListener<K extends keyof HTMLElementEventMap>(target: MaybeRefOrGetter<HTMLElement | undefined | null>, key: K, callback: (this: HTMLElement, event: HTMLElementEventMap[K]) => unknown, options?: UseEventListenerOptions): UseEventListener
// eslint-disable-next-line max-params
export function useEventListener<K extends string>(target: MaybeRefOrGetter<Node | undefined | null>, key: K, callback: (this: Node, event: Event) => unknown, options: UseEventListenerOptions = {}): UseEventListener {
const { immediate, ...listenerOptions } = { ...defaultOptions, ...options }

function add(): void {
toValue(target)?.addEventListener(key, callback, listenerOptions)
}

function remove(): void {
toValue(target)?.removeEventListener(key, callback, listenerOptions)
}

if (getCurrentScope()) {
onScopeDispose(() => remove())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you use tryOnScopeDispose for this?


if (immediate) {
add()
}
stackoverfloweth marked this conversation as resolved.
Show resolved Hide resolved

watch(() => toValue(target), () => {
remove()
add()
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this needs to be a little more sophisticated. If immediate is false I think this might add the listener anyway. Similar if remove is called but then the target changes this will automatically add the listener back.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I have a better, more elegant solution but it now seems to warrant some unit tests. Let me get some good tests written and I'll update this PR


return {
add,
remove,
}
}
stackoverfloweth marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 6 additions & 2 deletions src/useGlobalEventListener/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
# useGlobalEventListener

The `useGlobalEventListener` composition can be used to automatically handle setup and teardown of event listeners on the document scope. It takes the same arguments as the global `addEventListener` method

## Example

```typescript
import { useGlobalEventListener } from '@prefecthq/vue-compositions'

function handleEvent(event: Event) {
function handleEvent(event: MouseEvent) {
// Respond to event
}
useGlobalEventListener('keyup', handleEvent)
```

## Arguments

| Name | Type | Default |
|-----------|-----------------------------------------------------------|-----------|
| type | `K (K extends keyof DocumentEventMap)` | None |
| callback | `(this: Document, event: DocumentEventMap[K]) => unknown` | None |
| options | `AddEventListenerOptions` | None |

## Returns

| Name | Type | Description |
|--------|-------------|---------------------------------------------------|
| add | () => void | Manually attach the event listener (has no effect if the event listener already exists) |
| remove | () => void | Manually detach the event listener |
| remove | () => void | Manually detach the event listener |
18 changes: 3 additions & 15 deletions src/useGlobalEventListener/useGlobalEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { tryOnScopeDispose } from '@/utilities/tryOnScopeDispose'
import { useEventListener } from '@/useEventListener'

type UseGlobalEventListener = {
add: () => void,
Expand Down Expand Up @@ -31,20 +31,8 @@ type UseGlobalEventListener = {
export function useGlobalEventListener<K extends keyof DocumentEventMap>(
type: K,
callback: (this: Document, event: DocumentEventMap[K]) => unknown,
options?: boolean | AddEventListenerOptions,
options?: AddEventListenerOptions,
): UseGlobalEventListener {

const add = (): void => {
document.addEventListener(type, callback, options)
}

const remove = (): void => {
document.removeEventListener(type, callback, options)
}

add()
tryOnScopeDispose(remove)

return { add, remove }
return useEventListener(document, type, callback, options)
}

8 changes: 8 additions & 0 deletions src/utilities/vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { unref } from 'vue'
import { MaybeRefOrGetter } from '@/types/maybe'
import { isFunction } from '@/utilities/functions'

// temp shim for Vue 3.3^ function
export function toValue<T>(source: MaybeRefOrGetter<T>): T {
return isFunction(source) ? source() : unref(source)
}