mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 14:38:17 +08:00
DEV: Support @debounce
decorator in native class syntax (#20521)
The implementation previously generated a descriptor with an `initializer()`, and bound the function to the `this` context of the initializer. In native class syntax, the initializer of a descriptor is only called once, with a `this` context of the constructor, not the instance. This commit updates the implementation so that it generates the bound function on-demand using a getter. This is the same strategy employed by ember's built-in `@action` decorator. Unfortunately, this use of a getter means that the `@observes` decorator does not support being directly chained to `@debounce`. It throws the error "`observer must be provided a function or an observer definition`". The workaround is to put the observer on its own function, which then calls the debounced function. Given that we're aiming to reduce our usage of `@observes`, we've accepted the need for this workaround rather than spending the time to patch the implementation of `@observes`.
This commit is contained in:
parent
36ad653fa9
commit
e08a0b509d
|
@ -112,6 +112,10 @@ export default Controller.extend({
|
|||
},
|
||||
|
||||
@observes("filter", "onlyOverridden", "model")
|
||||
optsChanged() {
|
||||
this.filterContent();
|
||||
},
|
||||
|
||||
@debounce(INPUT_DELAY)
|
||||
filterContent() {
|
||||
if (this._skipBounce) {
|
||||
|
|
|
@ -100,10 +100,9 @@ export function debounce(delay, immediate = false) {
|
|||
return {
|
||||
enumerable: descriptor.enumerable,
|
||||
configurable: descriptor.configurable,
|
||||
writable: descriptor.writable,
|
||||
initializer() {
|
||||
get: function () {
|
||||
const originalFunction = descriptor.value;
|
||||
const debounced = function (...args) {
|
||||
const debounced = (...args) => {
|
||||
return discourseDebounce(
|
||||
this,
|
||||
originalFunction,
|
||||
|
@ -113,6 +112,13 @@ export function debounce(delay, immediate = false) {
|
|||
);
|
||||
};
|
||||
|
||||
// Memoize on instance for future access
|
||||
Object.defineProperty(this, name, {
|
||||
value: debounced,
|
||||
enumerable: descriptor.enumerable,
|
||||
configurable: descriptor.configurable,
|
||||
});
|
||||
|
||||
return debounced;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,6 +25,10 @@ export default Controller.extend({
|
|||
bulkSelection: null,
|
||||
|
||||
@observes("filterInput")
|
||||
filterInputChanged() {
|
||||
this._setFilter();
|
||||
},
|
||||
|
||||
@debounce(500)
|
||||
_setFilter() {
|
||||
this.set("filter", this.filterInput);
|
||||
|
|
|
@ -19,6 +19,10 @@ export default Controller.extend({
|
|||
loading: false,
|
||||
|
||||
@observes("filterInput")
|
||||
filterInputChanged() {
|
||||
this._setFilter();
|
||||
},
|
||||
|
||||
@debounce(500)
|
||||
_setFilter() {
|
||||
this.set("filter", this.filterInput);
|
||||
|
|
|
@ -30,6 +30,10 @@ export default Controller.extend({
|
|||
},
|
||||
|
||||
@observes("searchTerm")
|
||||
searchTermChanged() {
|
||||
this._searchTermChanged();
|
||||
},
|
||||
|
||||
@debounce(INPUT_DELAY)
|
||||
_searchTermChanged() {
|
||||
Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
|
||||
|
|
|
@ -8,6 +8,7 @@ import discourseComputed, {
|
|||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import { observes as nativeClassObserves } from "@ember-decorators/object";
|
||||
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import EmberObject from "@ember/object";
|
||||
|
@ -71,15 +72,43 @@ const TestStub = EmberObject.extend({
|
|||
this.state = state;
|
||||
},
|
||||
|
||||
// Note: it only works in this particular order:
|
||||
// `@observes()` first, then `@debounce()`
|
||||
@observes("prop")
|
||||
propChanged() {
|
||||
this.react();
|
||||
},
|
||||
|
||||
@debounce(50)
|
||||
react() {
|
||||
this.otherCounter++;
|
||||
},
|
||||
});
|
||||
|
||||
const ClassSyntaxTestStub = class extends EmberObject {
|
||||
counter = 0;
|
||||
otherCounter = 0;
|
||||
state = null;
|
||||
|
||||
@debounce(50)
|
||||
increment(value) {
|
||||
this.counter += value;
|
||||
}
|
||||
|
||||
@debounce(50, true)
|
||||
setState(state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@nativeClassObserves("prop")
|
||||
propChanged() {
|
||||
this.react();
|
||||
}
|
||||
|
||||
@debounce(50)
|
||||
react() {
|
||||
this.otherCounter++;
|
||||
}
|
||||
};
|
||||
|
||||
module("Unit | Utils | decorators", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
|
@ -167,15 +196,22 @@ module("Unit | Utils | decorators", function (hooks) {
|
|||
assert.strictEqual(stub.state, "foo");
|
||||
});
|
||||
|
||||
test("debounce works with @observe", async function (assert) {
|
||||
const stub = TestStub.create();
|
||||
test("debounce works with native class syntax", async function (assert) {
|
||||
const stub = ClassSyntaxTestStub.create();
|
||||
|
||||
stub.set("prop", 1);
|
||||
stub.set("prop", 2);
|
||||
stub.set("prop", 3);
|
||||
stub.increment(1);
|
||||
stub.increment(1);
|
||||
stub.increment(1);
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(stub.otherCounter, 1);
|
||||
assert.strictEqual(stub.counter, 1);
|
||||
|
||||
stub.increment(500);
|
||||
stub.increment(1000);
|
||||
stub.increment(5);
|
||||
await settled();
|
||||
|
||||
assert.strictEqual(stub.counter, 6);
|
||||
});
|
||||
|
||||
test("@observes works via .extend and native class syntax", async function (assert) {
|
||||
|
|
|
@ -61,6 +61,10 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
@observes("computedConfig.{from,to,options}", "options", "isValid", "isRange")
|
||||
configChanged() {
|
||||
this._renderPreview();
|
||||
},
|
||||
|
||||
@debounce(INPUT_DELAY)
|
||||
async _renderPreview() {
|
||||
if (this.markup) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user