diff --git a/addons/web/static/src/core/code_editor/code_editor.js b/addons/web/static/src/core/code_editor/code_editor.js index 99440acbd20..aecaab91ec4 100644 --- a/addons/web/static/src/core/code_editor/code_editor.js +++ b/addons/web/static/src/core/code_editor/code_editor.js @@ -62,6 +62,12 @@ export class CodeEditor extends Component { onWillStart(async () => await loadBundle("web.ace_lib")); const sessions = {}; + // The ace library triggers the "change" event even if the change is + // programmatic. Even worse, it triggers 2 "change" events in that case, + // one with the empty string, and one with the new value. We only want + // to notify the parent of changes done by the user, in the UI, so we + // use this flag to filter out noisy "change" events. + let ignoredAceChange = false; useEffect( (el) => { if (!el) { @@ -85,7 +91,7 @@ export class CodeEditor extends Component { } aceEditor.setValue(this.props.value); session.on("change", () => { - if (this.props.onChange) { + if (this.props.onChange && !ignoredAceChange) { this.props.onChange(this.aceEditor.getValue()); } }); @@ -132,7 +138,9 @@ export class CodeEditor extends Component { let session = sessions[sessionId]; if (session) { if (session.getValue() !== value) { + ignoredAceChange = true; session.setValue(value); + ignoredAceChange = false; } } else { session = new window.ace.EditSession(value); @@ -143,7 +151,7 @@ export class CodeEditor extends Component { useSoftTabs: true, }); session.on("change", () => { - if (this.props.onChange) { + if (this.props.onChange && !ignoredAceChange) { this.props.onChange(this.aceEditor.getValue()); } }); diff --git a/addons/web/static/tests/core/code_editor_tests.js b/addons/web/static/tests/core/code_editor_tests.js index f11aa035b19..cd37b6e104d 100644 --- a/addons/web/static/tests/core/code_editor_tests.js +++ b/addons/web/static/tests/core/code_editor_tests.js @@ -128,6 +128,27 @@ QUnit.module("Web Components", (hooks) => { assert.verifySteps(["Some Text"], "Value properly given to onChange"); }); + QUnit.test("onChange props not called when value props is updated", async (assert) => { + class Parent extends Component { + static components = { CodeEditor }; + static template = xml``; + state = useState({ value: "initial value" }); + onChange(value) { + assert.step(value || "__emptystring__"); + } + } + + const parent = await mount(Parent, target, { env }); + await nextTick(); + assert.strictEqual(target.querySelector(".ace_line").textContent, "initial value"); + + parent.state.value = "new value"; + await nextTick(); + await nextTick(); + assert.strictEqual(target.querySelector(".ace_line").textContent, "new value"); + assert.verifySteps([]); + }); + QUnit.test("Default value correctly set and updates", async (assert) => { const textA = "
\n

A Paragraph

\n
"; const textB = "
\n

An Other Paragraph

\n
"; @@ -180,7 +201,7 @@ QUnit.module("Web Components", (hooks) => { "When the props is updated the value is correctly changed in the dom" ); - assert.verifySteps([textB, textC], "Changes properly given to onChange"); + assert.verifySteps([textB], "Changes properly given to onChange"); }); QUnit.test("Mode props update imports the mode", async (assert) => {