diff --git a/addons/web/static/src/views/fields/float/float_field.js b/addons/web/static/src/views/fields/float/float_field.js
index b835343fcf5..66e333804ea 100644
--- a/addons/web/static/src/views/fields/float/float_field.js
+++ b/addons/web/static/src/views/fields/float/float_field.js
@@ -8,41 +8,27 @@ import { formatFloat } from "../formatters";
import { parseFloat } from "../parsers";
import { standardFieldProps } from "../standard_field_props";
-const { Component, onWillUpdateProps } = owl;
+const { Component } = owl;
export class FloatField extends Component {
setup() {
- this.defaultInputValue = this.getFormattedValue();
useInputField({
- getValue: () => this.defaultInputValue,
+ getValue: () => this.formattedValue,
refName: "numpadDecimal",
parse: (v) => this.parse(v),
});
useNumpadDecimal();
- onWillUpdateProps((nextProps) => {
- if (
- nextProps.readonly !== this.props.readonly &&
- !nextProps.readonly &&
- nextProps.inputType !== "number"
- ) {
- this.defaultInputValue = this.getFormattedValue(nextProps);
- }
- });
}
parse(value) {
return this.props.inputType === "number" ? Number(value) : parseFloat(value);
}
- onChange(ev) {
- this.defaultInputValue = ev.target.value;
- }
-
- getFormattedValue(props = this.props) {
- if (props.inputType === "number" && !props.readonly && props.value) {
- return props.value;
+ get formattedValue() {
+ if (this.props.inputType === "number" && !this.props.readonly && this.props.value) {
+ return this.props.value;
}
- return formatFloat(props.value, { digits: props.digits });
+ return formatFloat(this.props.value, { digits: this.props.digits });
}
}
diff --git a/addons/web/static/src/views/fields/float/float_field.xml b/addons/web/static/src/views/fields/float/float_field.xml
index 6d424f2d55e..af885b4514f 100644
--- a/addons/web/static/src/views/fields/float/float_field.xml
+++ b/addons/web/static/src/views/fields/float/float_field.xml
@@ -2,8 +2,8 @@
-
-
+
+
diff --git a/addons/web/static/src/views/fields/input_field_hook.js b/addons/web/static/src/views/fields/input_field_hook.js
index 0f383b354eb..44a5aa783b0 100644
--- a/addons/web/static/src/views/fields/input_field_hook.js
+++ b/addons/web/static/src/views/fields/input_field_hook.js
@@ -18,29 +18,62 @@ export function useInputField(params) {
const env = useEnv();
const inputRef = useRef(params.refName || "input");
const component = useComponent();
+
+ /*
+ * A field is dirty if it is no longer sync with the model
+ * More specifically, a field is no longer dirty after it has *tried* to update the value in the model.
+ * An invalid value will thefore not be dirty even if the model will not actually store the invalid value.
+ */
let isDirty = false;
+
+ /**
+ * A field is invalid if the parsing of its value failed.
+ */
+ let isInvalid = false;
+
+ /**
+ * The last value that has been commited to the model.
+ * Not changed in case of invalid field value.
+ */
let lastSetValue = null;
+
+ /**
+ * When a user types, we need to set the field as dirty.
+ */
function onInput(ev) {
isDirty = ev.target.value !== lastSetValue;
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
}
+
+ /**
+ * On blur, we consider the field no longer dirty, even if it were to be invalid.
+ * However, if the field is invalid, the new value will not be committed to the model.
+ */
function onChange(ev) {
- lastSetValue = ev.target.value;
isDirty = false;
- try {
- const val = params.parse ? params.parse(ev.target.value) : ev.target.value;
- component.props.update(val);
- } catch (_e) {
- component.props.record.setInvalidField(component.props.name);
+ isInvalid = false;
+ let val = ev.target.value;
+ if (params.parse) {
+ try {
+ val = params.parse(val);
+ } catch (_e) {
+ component.props.record.setInvalidField(component.props.name);
+ isInvalid = true;
+ }
}
+
+ if (!isInvalid) {
+ component.props.update(val);
+ lastSetValue = ev.target.value;
+ }
+
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
}
- useBus(env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () => commitChanges(true));
- useBus(env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", () => commitChanges(false));
+
useEffect(
(inputEl) => {
if (inputEl) {
@@ -54,30 +87,64 @@ export function useInputField(params) {
},
() => [inputRef.el]
);
+
+ /**
+ * Sometimes, a patch can happen with possible a new value for the field
+ * If the user was typing a new value (isDirty) or had enter an invalid value (isInvalid),
+ * we need to do nothing.
+ * If it is not such a case, we update the field with the new value.
+ */
useEffect(() => {
- if (inputRef.el && !isDirty) {
+ if (inputRef.el && !isDirty && !isInvalid) {
inputRef.el.value = params.getValue();
lastSetValue = inputRef.el.value;
}
});
- async function commitChanges(urgent) {
+ useBus(env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () => commitChanges(true));
+ useBus(env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", () => commitChanges(false));
+
+ /**
+ * Roughly the same as onChange, but called at more specific / critical times. (See bus events)
+ */
+ function commitChanges(urgent) {
+ if (!inputRef.el) {
+ return;
+ }
+
+ if (isInvalid && !isDirty) {
+ return;
+ }
+
+ isDirty = inputRef.el.value !== lastSetValue;
if (isDirty || urgent) {
+ isInvalid = false;
isDirty = false;
- try {
- const val = params.parse ? params.parse(inputRef.el.value) : inputRef.el.value;
- if (val !== component.props.value) {
- await component.props.update(val);
- }
- } catch (_e) {
- if (urgent) {
- return;
- } else {
- component.props.record.setInvalidField(component.props.name);
+ let val = inputRef.el.value;
+ if (params.parse) {
+ try {
+ val = params.parse(val);
+ } catch (_e) {
+ isInvalid = true;
+ if (urgent) {
+ return;
+ } else {
+ component.props.record.setInvalidField(component.props.name);
+ }
}
}
+
if (component.props.setDirty) {
- component.props.setDirty(false);
+ component.props.setDirty(isDirty);
+ }
+
+ if (isInvalid) {
+ return;
+ }
+
+ if (val !== component.props.value) {
+ component.props.update(val);
+ lastSetValue = inputRef.el.value;
}
}
}
diff --git a/addons/web/static/tests/views/fields/float_field_tests.js b/addons/web/static/tests/views/fields/float_field_tests.js
index fdd46a67990..6a84d68e00e 100644
--- a/addons/web/static/tests/views/fields/float_field_tests.js
+++ b/addons/web/static/tests/views/fields/float_field_tests.js
@@ -161,8 +161,8 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, 'div[name="float_field"] input', "108.2451938598598");
assert.strictEqual(
target.querySelector(".o_field_widget[name=float_field] input").value,
- "108.2451938598598",
- "The value should not be formatted yet."
+ "108.245",
+ "The value should have been formatted on blur."
);
await editInput(target, ".o_field_widget[name=float_field] input", "18.8958938598598");
@@ -278,8 +278,8 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, 'div[name="float_field"] input', "108.2458938598598");
assert.strictEqual(
target.querySelector('div[name="float_field"] input').value,
- "108.2458938598598",
- "The value should not be formatted yet."
+ "108.246",
+ "The value should have been formatted on blur."
);
await editInput(target, 'div[name="float_field"] input', "18.8958938598598");
@@ -352,8 +352,8 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelector(".o_form_button_edit"));
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
- "123456.7890",
- "Float value must be not formatted if input type is number."
+ "123456.789",
+ "Float value must be not formatted if input type is number. (but the trailing 0 is gone)"
);
await click(target.querySelector(".o_form_button_save"));
assert.strictEqual(
@@ -464,4 +464,52 @@ QUnit.module("Fields", (hooks) => {
"Placeholder"
);
});
+
+ QUnit.test("float field can be updated by another field/widget", async function (assert) {
+ class MyWidget extends owl.Component {
+ onClick() {
+ const val = this.props.record.data.float_field;
+ this.props.record.update({ float_field: val + 1 });
+ }
+ }
+ MyWidget.template = owl.xml``;
+ registry.category("view_widgets").add("wi", MyWidget);
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ serverData,
+ arch: `
+