diff --git a/app/assets/javascripts/discourse/components/text-field.js b/app/assets/javascripts/discourse/components/text-field.js index 23440b291dd..bb57f25309c 100644 --- a/app/assets/javascripts/discourse/components/text-field.js +++ b/app/assets/javascripts/discourse/components/text-field.js @@ -1,8 +1,14 @@ import { TextField } from "@ember/component"; import discourseComputed from "discourse-common/utils/decorators"; import { siteDir, isRTL, isLTR } from "discourse/lib/text-direction"; +import { next, debounce, cancel } from "@ember/runloop"; + +const DEBOUNCE_MS = 500; export default TextField.extend({ + _prevValue: null, + _timer: null, + attributeBindings: [ "autocorrect", "autocapitalize", @@ -11,6 +17,28 @@ export default TextField.extend({ "dir" ], + didReceiveAttrs() { + this._super(...arguments); + this._prevValue = this.value; + }, + + didUpdateAttrs() { + this._super(...arguments); + if (this._prevValue !== this.value) { + if (this.onChangeImmediate) { + next(() => this.onChangeImmediate(this.value)); + } + if (this.onChange) { + cancel(this._timer); + this._timer = debounce(this, this._debouncedChange, DEBOUNCE_MS); + } + } + }, + + _debouncedChange() { + next(() => this.onChange(this.value)); + }, + @discourseComputed dir() { if (this.siteSettings.support_mixed_text_direction) { @@ -23,6 +51,11 @@ export default TextField.extend({ } }, + willDestroyElement() { + this._super(...arguments); + cancel(this._timer); + }, + keyUp(event) { this._super(event); diff --git a/test/javascripts/components/text-field-test.js b/test/javascripts/components/text-field-test.js index dae51393b16..7b26dddb101 100644 --- a/test/javascripts/components/text-field-test.js +++ b/test/javascripts/components/text-field-test.js @@ -44,3 +44,43 @@ componentTest("sets the dir attribute to ltr for English text", { assert.equal(find("input").attr("dir"), "ltr"); } }); + +componentTest("supports onChange", { + template: `{{text-field class="tf-test" value=value onChange=changed}}`, + beforeEach() { + this.called = false; + this.newValue = null; + this.set("value", "hello"); + this.set("changed", v => { + this.newValue = v; + this.called = true; + }); + }, + async test(assert) { + await fillIn(".tf-test", "hello"); + assert.ok(!this.called); + await fillIn(".tf-test", "new text"); + assert.ok(this.called); + assert.equal(this.newValue, "new text"); + } +}); + +componentTest("supports onChangeImmediate", { + template: `{{text-field class="tf-test" value=value onChangeImmediate=changed}}`, + beforeEach() { + this.called = false; + this.newValue = null; + this.set("value", "old"); + this.set("changed", v => { + this.newValue = v; + this.called = true; + }); + }, + async test(assert) { + await fillIn(".tf-test", "old"); + assert.ok(!this.called); + await fillIn(".tf-test", "no longer old"); + assert.ok(this.called); + assert.equal(this.newValue, "no longer old"); + } +});