From 357e266ee290c64ab1ba454f1318a9da1bb21fd2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sun, 5 Jun 2022 12:00:42 -0400 Subject: [PATCH] feat(util): add debounce example util --- ember-resources/package.json | 4 ++ ember-resources/src/util/debounce.ts | 72 +++++++++++++++++++ .../ember-app/tests/utils/debounce/js-test.ts | 48 +++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 ember-resources/src/util/debounce.ts create mode 100644 testing/ember-app/tests/utils/debounce/js-test.ts diff --git a/ember-resources/package.json b/ember-resources/package.json index 5e9e62cae..ae77600c7 100644 --- a/ember-resources/package.json +++ b/ember-resources/package.json @@ -14,6 +14,7 @@ "./util/map": "./dist/util/map.js", "./util/helper": "./dist/util/helper.js", "./util/remote-data": "./dist/util/remote-data.js", + "./util/debounce": "./dist/util/debounce.js", "./util/function": "./dist/util/function.js", "./util/function-resource": "./dist/util/function-resource.js", "./util/ember-concurrency": "./dist/util/ember-concurrency.js", @@ -42,6 +43,9 @@ "util/helper": [ "dist/util/helper.d.ts" ], + "util/debounce": [ + "dist/util/debounce.d.ts" + ], "util/remote-data": [ "dist/util/remote-data.d.ts" ], diff --git a/ember-resources/src/util/debounce.ts b/ember-resources/src/util/debounce.ts new file mode 100644 index 000000000..0f5c6c6b6 --- /dev/null +++ b/ember-resources/src/util/debounce.ts @@ -0,0 +1,72 @@ +import { tracked } from '@glimmer/tracking'; + +import { resource } from './function-resource'; + +class TrackedValue { + @tracked value: T | undefined; +} + +/** + * A utility for debouncing high-frequency updates. + * The returned value will only be updated every `ms` and is + * initially undefined. + * + * This can be useful when a user's typing is updating a tracked + * property and you want to derive data less frequently than on + * each keystroke. + * + * Note that this utility requires the @use decorator + * (debounce could be implemented without the need for the @use decorator + * but the current implementation is 8 lines) + * + * @example + * ```js + * import Component from '@glimmer/component'; + * import { tracked } from '@glimmer/tracking'; + * import { debounce } from 'ember-resources/util/debounce'; + * + * const delay = 100; // ms + * + * class Demo extends Component { + * @tracked userInput = ''; + * + * @use debouncedInput = debounce(delay, () => this.userInput); + * } + * ``` + * + * @example + * This could be further composed with [[RemoteData]] + * ```js + * import Component from '@glimmer/component'; + * import { tracked } from '@glimmer/tracking'; + * import { debounce } from 'ember-resources/util/debounce'; + * import { RemoteData } from 'ember-resources/util/remote-data'; + * + * const delay = 100; // ms + * + * class Demo extends Component { + * @tracked userInput = ''; + * + * @use debouncedInput = debounce(delay, () => this.userInput); + * + * @use search = RemoteData(() => `https://my.domain/search?q=${this.debouncedInput}`); + * } + * ``` + * + * @param {number} ms delay in milliseconds to wait before updating the returned value + * @param {() => Value} thunk function that returns the value to debounce + */ +export function debounce(ms: number, thunk: () => Value) { + let lastValue: Value; + let timer: number; + let state = new TrackedValue(); + + return resource(({ on }) => { + lastValue = thunk(); + + on.cleanup(() => timer && clearTimeout(timer)); + timer = setTimeout(() => (state.value = lastValue), ms); + + return state.value; + }); +} diff --git a/testing/ember-app/tests/utils/debounce/js-test.ts b/testing/ember-app/tests/utils/debounce/js-test.ts new file mode 100644 index 000000000..3eda6c146 --- /dev/null +++ b/testing/ember-app/tests/utils/debounce/js-test.ts @@ -0,0 +1,48 @@ +import { tracked } from '@glimmer/tracking'; +import { setOwner } from '@ember/application'; +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +import { debounce } from 'ember-resources/util/debounce'; +import { use } from 'ember-resources/util/function-resource'; + +module('Utils | debounce | js', function (hooks) { + setupTest(hooks); + + let someTime = (ms = 25) => new Promise((resolve) => setTimeout(resolve, ms)); + + module('debounce', function () { + test('works with @use', async function (assert) { + class Test { + @tracked data = ''; + + @use text = debounce(100, () => this.data); + } + + let test = new Test(); + + setOwner(test, this.owner); + + assert.strictEqual(test.text, undefined); + + test.data = 'b'; + await someTime(); + assert.strictEqual(test.text, undefined); + test.data = 'bo'; + await someTime(); + assert.strictEqual(test.text, undefined); + test.data = 'boo'; + await someTime(); + assert.strictEqual(test.text, undefined); + + await someTime(110); + assert.strictEqual(test.text, 'boo'); + + test.data = 'boop'; + assert.strictEqual(test.text, 'boo'); + + await someTime(110); + assert.strictEqual(test.text, 'boop'); + }); + }); +});