UNPKG

ember-lifeline

Version:

Ember.js addon for lifecycle aware async tasks and DOM events.

140 lines (122 loc) 4.03 kB
import { assert } from '@ember/debug'; import { cancel, debounce } from '@ember/runloop'; import { registerDestructor } from '@ember/destroyable'; /** * A map of instances/debounce functions that allows us to * store pending debounces per instance. * * @private */ const registeredDebounces = new WeakMap(); /** * Runs the function with the provided name after the timeout has expired on the last * invocation. The timer is properly canceled if the object is destroyed before it is * invoked. * * Example: * * ```js * import Component from '@glimmer/component'; * import { debounceTask } from 'ember-lifeline'; * * export default LoggerComponent extends Component { * logMe() { * console.log('This will only run once every 300ms.'); * }, * * click() { * debounceTask(this, 'logMe', 300); * }, * } * ``` * * @function debounceTask * @param { Destroyable } destroyable the instance to register the task for * @param { String } name the name of the task to debounce * @param { ...* } debounceArgs arguments to pass to the debounced method * @param { Number } spacing the amount of time to wait before calling the method (in milliseconds) * @param { Boolean } [immediate] Trigger the function on the leading instead of the trailing edge of the wait interval. Defaults to false. * @public */ function debounceTask(destroyable, name, ...debounceArgs) { assert(`Called \`debounceTask\` without a string as the first argument on ${destroyable}.`, typeof name === 'string'); assert(`Called \`destroyable.debounceTask('${name}', ...)\` where 'destroyable.${name}' is not a function.`, typeof destroyable[name] === 'function'); if (destroyable.isDestroying) { return; } const lastArgument = debounceArgs[debounceArgs.length - 1]; const spacing = typeof lastArgument === 'boolean' ? debounceArgs[debounceArgs.length - 2] : lastArgument; assert(`Called \`debounceTask\` with incorrect \`spacing\` argument. Expected Number and received \`${spacing}\``, typeof spacing === 'number'); let pendingDebounces = registeredDebounces.get(destroyable); if (!pendingDebounces) { pendingDebounces = new Map(); registeredDebounces.set(destroyable, pendingDebounces); registerDestructor(destroyable, getDebouncesDisposable(pendingDebounces)); } let debouncedTask; if (!pendingDebounces.has(name)) { debouncedTask = (...args) => { pendingDebounces.delete(name); destroyable[name](...args); }; } else { debouncedTask = pendingDebounces.get(name).debouncedTask; } // cancelId is new, even if the debounced function was already present let cancelId = debounce(destroyable, debouncedTask, ...debounceArgs); pendingDebounces.set(name, { debouncedTask, cancelId }); } /** * Cancel a previously debounced task. * * Example: * * ```js * import Component from '@glimmer/component'; * import { debounceTask, cancelDebounce } from 'ember-lifeline'; * * export default LoggerComponent extends Component { * logMe() { * console.log('This will only run once every 300ms.'); * }, * * click() { * debounceTask(this, 'logMe', 300); * }, * * disable() { * cancelDebounce(this, 'logMe'); * }, * } * ``` * * @function cancelDebounce * @param { Destroyable } destroyable the instance to register the task for * @param { String } methodName the name of the debounced method to cancel * @public */ function cancelDebounce(destroyable, methodName) { if (!registeredDebounces.has(destroyable)) { return; } const pendingDebounces = registeredDebounces.get(destroyable); if (!pendingDebounces.has(methodName)) { return; } const { cancelId } = pendingDebounces.get(methodName); pendingDebounces.delete(methodName); cancel(cancelId); } function getDebouncesDisposable(debounces) { return function () { if (debounces.size === 0) { return; } debounces.forEach(p => cancel(p.cancelId)); }; } export { cancelDebounce, debounceTask };