From 09e4eb41374d2125b3d8f3aaff6bb9335c90c5eb Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Wed, 19 Oct 2022 20:43:58 +0200 Subject: [PATCH] DEV: Introduce a `@debounce(delay)` decorator (#18667) An example from tests: ```js class TestStub { counter = 0; @debounce(50) increment() { this.counter++; } } const stub = new TestStub(); stub.increment(); stub.increment(); stub.increment(); await settled(); assert.strictEqual(stub.counter, 1); ``` --- .../addon/utils/decorators.js | 19 +++++++ .../tests/unit/utils/decorators-test.js | 54 ++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse-common/addon/utils/decorators.js b/app/assets/javascripts/discourse-common/addon/utils/decorators.js index b847ad830e3..f9544d9176a 100644 --- a/app/assets/javascripts/discourse-common/addon/utils/decorators.js +++ b/app/assets/javascripts/discourse-common/addon/utils/decorators.js @@ -36,6 +36,7 @@ import extractValue from "discourse-common/utils/extract-value"; import handleDescriptor from "discourse-common/utils/handle-descriptor"; import isDescriptor from "discourse-common/utils/is-descriptor"; import macroAlias from "discourse-common/utils/macro-alias"; +import discourseDebounce from "discourse-common/lib/debounce"; export default function discourseComputedDecorator(...params) { // determine if user called as @discourseComputed('blah', 'blah') or @discourseComputed @@ -87,6 +88,24 @@ export function readOnly(target, name, desc) { }; } +export function debounce(delay) { + return function (target, name, descriptor) { + return { + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + initializer() { + const originalFunction = descriptor.value; + const debounced = function (...args) { + return discourseDebounce(this, originalFunction, ...args, delay); + }; + + return debounced; + }, + }; + }; +} + export const on = decoratorAlias(emberOn, "Can not `on` without event names"); export const observes = decoratorAlias( observer, diff --git a/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js b/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js index 0563fc56d02..dc7598055eb 100644 --- a/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js +++ b/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js @@ -1,12 +1,15 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import Component from "@ember/component"; -import { clearRender, render } from "@ember/test-helpers"; +import { clearRender, render, settled } from "@ember/test-helpers"; import discourseComputed, { afterRender, + debounce, + observes, } from "discourse-common/utils/decorators"; import { exists } from "discourse/tests/helpers/qunit-helpers"; import { hbs } from "ember-cli-htmlbars"; +import EmberObject from "@ember/object"; const fooComponent = Component.extend({ classNames: ["foo-component"], @@ -51,6 +54,24 @@ class NativeComponent extends Component { } } +const TestStub = EmberObject.extend({ + counter: 0, + otherCounter: 0, + + @debounce(50) + increment(value) { + this.counter += value; + }, + + // Note: it only works in this particular order: + // `@observes()` first, then `@debounce()` + @observes("prop") + @debounce(50) + react() { + this.otherCounter++; + }, +}); + module("Unit | Utils | decorators", function (hooks) { setupRenderingTest(hooks); @@ -58,7 +79,7 @@ module("Unit | Utils | decorators", function (hooks) { this.registry.register("component:foo-component", fooComponent); this.set("baz", 0); - await render(hbs`{{foo-component baz=baz}}`); + await render(hbs``); assert.ok(exists(document.querySelector(".foo-component"))); assert.strictEqual(this.baz, 1); @@ -109,4 +130,33 @@ module("Unit | Utils | decorators", function (hooks) { "rerenders the component when arguments change" ); }); + + test("debounce", async function (assert) { + const stub = TestStub.create(); + + stub.increment(1); + stub.increment(1); + stub.increment(1); + await settled(); + + assert.strictEqual(stub.counter, 1); + + stub.increment(500); + stub.increment(1000); + stub.increment(5); + await settled(); + + assert.strictEqual(stub.counter, 6); + }); + + test("debounce works with @observe", async function (assert) { + const stub = TestStub.create(); + + stub.set("prop", 1); + stub.set("prop", 2); + stub.set("prop", 3); + await settled(); + + assert.strictEqual(stub.otherCounter, 1); + }); });