Skip to content

Commit

Permalink
feat(debounceAsync): add async debounce function
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Feb 26, 2024
1 parent 632574b commit 6a987af
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ const debounced = debounce(
wait = 250, // milliseconds
{
immediate = false, // execute the debounceFunc on the leading end
instance = null // function or class instance for "this"
} = {},
)

Expand All @@ -255,6 +254,35 @@ debounced({ foo: 'bar'}) // delay the execution – again
debounced.cancel() // optional, cancel the execution
```

Async example:

```js
import { debounceAsync } from '@dnb/eufemia/shared/helpers'

async function debounceFunc({ foo }) {
// optionally, add a cancel event (wasCanceled is a "function" to check later if it was canceled)
const wasCanceled = this.addCancelEvent(myCancelMethod)

await wait(1000) // do something async
}

const myCancelMethod = () => {
console.log('canceled')
}

const debounced = debounceAsync(
debounceFunc,
(wait = 250), // milliseconds
)

debounceAsync({ foo: 'bar' }) // delay the execution – again

debounced.cancel() // optional, cancel the execution
debounced.addCancelEvent(myCancelMethod) // alternatively, you can add the cancel event like so
```

In order to use `this.addCancelEvent` you need to use a `function()`, and not an arrow function.

### copyToClipboard

Copies the given string to the device's clipboard.
Expand Down
102 changes: 1 addition & 101 deletions packages/dnb-eufemia/src/shared/__tests__/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
hasSelectedText,
getSelectedText,
insertElementBeforeSelection,
debounce,
isEdge,
isiOS,
isSafari,
Expand All @@ -25,7 +24,7 @@ import {
warn,
} from '../helpers'

import { mockGetSelection, wait } from '../../core/jest/jestSetup'
import { mockGetSelection } from '../../core/jest/jestSetup'

// make it possible to change the navigator lang
// because "navigator.language" defaults to en-GB
Expand Down Expand Up @@ -274,105 +273,6 @@ describe('selection related methods', () => {
})
})

describe('"debounce" should', () => {
it('delay execution', async () => {
let outside = 'one'

const debounced = debounce(({ inside }) => {
outside = inside
expect(outside).toBe('two')

return 'not accessible'
}, 1)

await wait(2)

const result = debounced({ inside: 'two' })

expect(typeof debounced).toBe('function')
expect(typeof debounced.cancel).toBe('function')

expect(outside).toBe('one')
expect(result).toBe(undefined)
})

it('use given instance', async () => {
const instance = () => {}
instance.property = 'hello'

const debounced = debounce(
// Needs to be a function (so we can use "this")
function () {
expect(this).toBe(instance)
expect(this.property).toBe(instance.property)
},
1,
{ instance }
)

debounced()
})

it('execution immediate', async () => {
let outside = 'one'

const debounced = debounce(
({ inside }) => {
expect(outside).toBe('one')
outside = inside
expect(outside).toBe('two')
},
1,
{ immediate: true }
)

debounced({ inside: 'two' })

await wait(2)

expect(outside).toBe('two')
})

it('execution immediate and return result', async () => {
let outside = 'one'

const debounced = debounce(
({ inside }) => {
expect(outside).toBe('one')
outside = inside
expect(outside).toBe('two')

return inside
},
1,
{ immediate: true }
)

const immediateResult = debounced({ inside: 'two' })

await wait(2)

expect(outside).toBe('two')
expect(immediateResult).toBe('two')
})

it('should not run debounced function when cancelled', async () => {
let outside = 'one'

const debounced = debounce(({ inside }) => {
expect(outside).toBe('one')
outside = inside
expect(outside).toBe('two')
}, 1)
debounced({ inside: 'two' })
debounced.cancel()

await wait(2)

expect(outside).toBe('one')
})
})

describe('"warn" should', () => {
const log = global.console.log

Expand Down
83 changes: 3 additions & 80 deletions packages/dnb-eufemia/src/shared/helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* Global helpers
*
* NB: Do not import other deps in this file.
* Just to have things clean and one directional.
*/

// For backward compatibility
export { debounce, debounceAsync } from './helpers/debounce'

export const PLATFORM_MAC = 'Mac|iPad|iPhone|iPod'
export const PLATFORM_WIN = 'Win'
export const PLATFORM_ANDROID = 'Android'
Expand Down Expand Up @@ -235,84 +236,6 @@ export function scrollToLocationHashId({
}
}

/**
* More or less classical debounce function
*
* Calling this function will return a new function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* @param {function} func The function to execute
* @param {number} wait The time (milliseconds) before the first given function executes, if the returned one, not got called
* @param {object} options The options object
* @property {boolean} immediate If true, the function will execute immediately. Defaults to false
* @property {instance} instance Defines the instance "this" to use on the executed function
* @returns The function to invalidate the execution
*/
export function debounce(
func,
wait = 250,
{ immediate = false, instance = null } = {}
) {
let timeout
let recall

const cancel = () => clearTimeout(timeout)

// This is the function that is actually executed when
// the DOM event is triggered.
function executedFunction(...args) {
// Store the instance of this and any
// parameters passed to executedFunction
let inst = this

// console.log('instance', instance)

if (typeof recall === 'function') {
recall()
}

// The function to be called after
// the debounce time has elapsed
const later = () => {
// null timeout to indicate the debounce ended
timeout = null

// Call function now if you did not on the leading end
if (!immediate) {
recall = func.apply(instance || inst, args)
}
}

// Determine if you should call the function
// on the leading or trail end
const callNow = immediate && !timeout

// This will reset the waiting every function execution.
// This is the step that prevents the function from
// being executed because it will never reach the
// inside of the previous setTimeout
clearTimeout(timeout)

// Restart the debounce waiting period.
// setTimeout returns a truthy value (it differs in web vs node)
timeout = setTimeout(later, wait)

// Call immediately if you're dong a leading
// end execution
if (callNow) {
recall = func.apply(instance || inst, args)
}

return recall
}

executedFunction.cancel = cancel

return executedFunction
}

export function insertElementBeforeSelection(elem) {
try {
const selection = window.getSelection()
Expand Down
Loading

0 comments on commit 6a987af

Please sign in to comment.