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")
|
@observes("filter", "onlyOverridden", "model")
|
||||||
|
optsChanged() {
|
||||||
|
this.filterContent();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(INPUT_DELAY)
|
@debounce(INPUT_DELAY)
|
||||||
filterContent() {
|
filterContent() {
|
||||||
if (this._skipBounce) {
|
if (this._skipBounce) {
|
||||||
|
|
|
@ -100,10 +100,9 @@ export function debounce(delay, immediate = false) {
|
||||||
return {
|
return {
|
||||||
enumerable: descriptor.enumerable,
|
enumerable: descriptor.enumerable,
|
||||||
configurable: descriptor.configurable,
|
configurable: descriptor.configurable,
|
||||||
writable: descriptor.writable,
|
get: function () {
|
||||||
initializer() {
|
|
||||||
const originalFunction = descriptor.value;
|
const originalFunction = descriptor.value;
|
||||||
const debounced = function (...args) {
|
const debounced = (...args) => {
|
||||||
return discourseDebounce(
|
return discourseDebounce(
|
||||||
this,
|
this,
|
||||||
originalFunction,
|
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;
|
return debounced;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,10 @@ export default Controller.extend({
|
||||||
bulkSelection: null,
|
bulkSelection: null,
|
||||||
|
|
||||||
@observes("filterInput")
|
@observes("filterInput")
|
||||||
|
filterInputChanged() {
|
||||||
|
this._setFilter();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(500)
|
@debounce(500)
|
||||||
_setFilter() {
|
_setFilter() {
|
||||||
this.set("filter", this.filterInput);
|
this.set("filter", this.filterInput);
|
||||||
|
|
|
@ -19,6 +19,10 @@ export default Controller.extend({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
@observes("filterInput")
|
@observes("filterInput")
|
||||||
|
filterInputChanged() {
|
||||||
|
this._setFilter();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(500)
|
@debounce(500)
|
||||||
_setFilter() {
|
_setFilter() {
|
||||||
this.set("filter", this.filterInput);
|
this.set("filter", this.filterInput);
|
||||||
|
|
|
@ -30,6 +30,10 @@ export default Controller.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
@observes("searchTerm")
|
@observes("searchTerm")
|
||||||
|
searchTermChanged() {
|
||||||
|
this._searchTermChanged();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(INPUT_DELAY)
|
@debounce(INPUT_DELAY)
|
||||||
_searchTermChanged() {
|
_searchTermChanged() {
|
||||||
Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
|
Invite.findInvitedBy(this.user, this.filter, this.searchTerm).then(
|
||||||
|
|
|
@ -8,6 +8,7 @@ import discourseComputed, {
|
||||||
observes,
|
observes,
|
||||||
on,
|
on,
|
||||||
} from "discourse-common/utils/decorators";
|
} from "discourse-common/utils/decorators";
|
||||||
|
import { observes as nativeClassObserves } from "@ember-decorators/object";
|
||||||
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
|
@ -71,15 +72,43 @@ const TestStub = EmberObject.extend({
|
||||||
this.state = state;
|
this.state = state;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Note: it only works in this particular order:
|
|
||||||
// `@observes()` first, then `@debounce()`
|
|
||||||
@observes("prop")
|
@observes("prop")
|
||||||
|
propChanged() {
|
||||||
|
this.react();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(50)
|
@debounce(50)
|
||||||
react() {
|
react() {
|
||||||
this.otherCounter++;
|
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) {
|
module("Unit | Utils | decorators", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
@ -167,15 +196,22 @@ module("Unit | Utils | decorators", function (hooks) {
|
||||||
assert.strictEqual(stub.state, "foo");
|
assert.strictEqual(stub.state, "foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("debounce works with @observe", async function (assert) {
|
test("debounce works with native class syntax", async function (assert) {
|
||||||
const stub = TestStub.create();
|
const stub = ClassSyntaxTestStub.create();
|
||||||
|
|
||||||
stub.set("prop", 1);
|
stub.increment(1);
|
||||||
stub.set("prop", 2);
|
stub.increment(1);
|
||||||
stub.set("prop", 3);
|
stub.increment(1);
|
||||||
await settled();
|
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) {
|
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")
|
@observes("computedConfig.{from,to,options}", "options", "isValid", "isRange")
|
||||||
|
configChanged() {
|
||||||
|
this._renderPreview();
|
||||||
|
},
|
||||||
|
|
||||||
@debounce(INPUT_DELAY)
|
@debounce(INPUT_DELAY)
|
||||||
async _renderPreview() {
|
async _renderPreview() {
|
||||||
if (this.markup) {
|
if (this.markup) {
|
||||||
|
|
Loading…
Reference in New Issue