ember-lifeline
Version:
Ember.js addon for lifecycle aware async tasks and DOM events.
140 lines (122 loc) • 4.03 kB
JavaScript
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 };