diff --git a/addons/account/static/src/js/account_resequence_field.js b/addons/account/static/src/js/account_resequence_field.js
index 0f918e9a297..57b57bfeac0 100644
--- a/addons/account/static/src/js/account_resequence_field.js
+++ b/addons/account/static/src/js/account_resequence_field.js
@@ -4,7 +4,7 @@ odoo.define('account.ShowResequenceRenderer', function (require) {
const { Component } = owl;
const { useState } = owl.hooks;
const AbstractFieldOwl = require('web.AbstractFieldOwl');
-const field_registry = require('web.field_registry');
+const field_registry = require('web.field_registry_owl');
class ChangeLine extends Component { }
ChangeLine.template = 'account.ResequenceChangeLine';
diff --git a/addons/account/static/src/js/grouped_view_widget.js b/addons/account/static/src/js/grouped_view_widget.js
index 3af2d0e1fe0..e1df30cf997 100644
--- a/addons/account/static/src/js/grouped_view_widget.js
+++ b/addons/account/static/src/js/grouped_view_widget.js
@@ -4,7 +4,7 @@ odoo.define('account.ShowGroupedList', function (require) {
const { Component } = owl;
const { useState } = owl.hooks;
const AbstractFieldOwl = require('web.AbstractFieldOwl');
-const field_registry = require('web.field_registry');
+const field_registry = require('web.field_registry_owl');
class ListItem extends Component { }
ListItem.template = 'account.GroupedItemTemplate';
diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py
index ccfbd9b8499..e72fabaff2a 100644
--- a/addons/web/__manifest__.py
+++ b/addons/web/__manifest__.py
@@ -23,6 +23,7 @@ This module provides the core of the Odoo Web Client.
'qweb': [
"static/src/xml/base.xml",
"static/src/xml/chart.xml",
+ "static/src/xml/fields.xml",
"static/src/xml/file_upload_progress_bar.xml",
"static/src/xml/file_upload_progress_card.xml",
"static/src/xml/kanban.xml",
diff --git a/addons/web/static/src/js/_deprecated/basic_fields.js b/addons/web/static/src/js/_deprecated/basic_fields.js
new file mode 100644
index 00000000000..2a70c3793c0
--- /dev/null
+++ b/addons/web/static/src/js/_deprecated/basic_fields.js
@@ -0,0 +1,154 @@
+////////////////////////////////////////////////////////////////////////////////
+// /!\ DEPRECATED
+//
+// Legacy Field Widgets are added in this file when they are converted into
+// Owl Component.
+////////////////////////////////////////////////////////////////////////////////
+
+odoo.define('web.basic_fields.deprecated', function (require) {
+"use strict";
+
+/**
+ * This module contains most of the basic (meaning: non relational) field
+ * widgets. Field widgets are supposed to be used in views inheriting from
+ * BasicView, so, they can work with the records obtained from a BasicModel.
+ */
+
+var AbstractField = require('web.AbstractField');
+var core = require('web.core');
+
+var _lt = core._lt;
+
+var FieldBoolean = AbstractField.extend({
+ className: 'o_field_boolean',
+ description: _lt("Checkbox"),
+ events: _.extend({}, AbstractField.prototype.events, {
+ change: '_onChange',
+ }),
+ supportedFieldTypes: ['boolean'],
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Toggle the checkbox if it is activated due to a click on itself.
+ *
+ * @override
+ */
+ activate: function (options) {
+ var activated = this._super.apply(this, arguments);
+ // The formatValue of boolean fields renders HTML elements similar to
+ // the one rendered by the widget itself. Even though the event might
+ // have been fired on the non-widget version of this field, we can still
+ // test the presence of its custom class.
+ if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) {
+ this._setValue(!this.value); // Toggle the checkbox
+ }
+ return activated;
+ },
+
+ /**
+ * @override
+ * @returns {jQuery} the focusable checkbox input
+ */
+ getFocusableElement: function () {
+ return this.mode === 'readonly' ? $() : this.$input;
+ },
+ /**
+ * A boolean field is always set since false is a valid value.
+ *
+ * @override
+ */
+ isSet: function () {
+ return true;
+ },
+ /**
+ * When the checkbox is rerendered, we need to check if it was the actual
+ * origin of the reset. If it is, we need to activate it back so it looks
+ * like it was not rerendered but is still the same input.
+ *
+ * @override
+ */
+ reset: function (record, event) {
+ var rendered = this._super.apply(this, arguments);
+ if (event && event.target.name === this.name) {
+ this.activate();
+ }
+ return rendered;
+ },
+ /**
+ * Associates the 'for' attribute of the internal label.
+ *
+ * @override
+ */
+ setIDForLabel: function (id) {
+ this._super.apply(this, arguments);
+ this.$('.custom-control-label').attr('for', id);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * The actual checkbox is designed in css to have full control over its
+ * appearance, as opposed to letting the browser and the os decide how
+ * a checkbox should look. The actual input is disabled and hidden. In
+ * readonly mode, the checkbox is disabled.
+ *
+ * @override
+ * @private
+ */
+ _render: function () {
+ var $checkbox = this._formatValue(this.value);
+ this.$input = $checkbox.find('input');
+ this.$input.prop('disabled', this.mode === 'readonly');
+ this.$el.addClass($checkbox.attr('class'));
+ this.$el.empty().append($checkbox.contents());
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Properly update the value when the checkbox is (un)ticked to trigger
+ * possible onchanges.
+ *
+ * @private
+ */
+ _onChange: function () {
+ this._setValue(this.$input[0].checked);
+ },
+ /**
+ * Implement keyboard movements. Mostly useful for its environment, such
+ * as a list view.
+ *
+ * @override
+ * @private
+ * @param {KeyEvent} ev
+ */
+ _onKeydown: function (ev) {
+ switch (ev.which) {
+ case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
+ this.$input.prop('checked', !this.value);
+ this._setValue(!this.value);
+ return;
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.RIGHT:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
+ }
+ this._super.apply(this, arguments);
+ },
+});
+
+return {
+ FieldBoolean: FieldBoolean,
+};
+
+});
diff --git a/addons/web/static/src/js/components/custom_checkbox.js b/addons/web/static/src/js/components/custom_checkbox.js
new file mode 100644
index 00000000000..bc98dd7bab2
--- /dev/null
+++ b/addons/web/static/src/js/components/custom_checkbox.js
@@ -0,0 +1,58 @@
+odoo.define('web.CustomCheckbox', function (require) {
+ "use strict";
+
+ const utils = require('web.utils');
+
+ const { Component } = owl;
+
+ /**
+ * Custom checkbox
+ *
+ * Component that can be used in templates to render the custom checkbox of Odoo.
+ *
+ *
+ *
+ * @extends Component
+ */
+ class CustomCheckbox extends Component {
+ /**
+ * @param {Object} [props]
+ * @param {string | number | null} [props.id]
+ * @param {boolean} [props.value=false]
+ * @param {boolean} [props.disabled=false]
+ * @param {string} [props.text]
+ */
+ constructor() {
+ super(...arguments);
+ this._id = `checkbox-comp-${utils.generateID()}`;
+ }
+ }
+
+ CustomCheckbox.props = {
+ id: {
+ type: [String, Number],
+ optional: 1,
+ },
+ disabled: {
+ type: Boolean,
+ optional: 1,
+ },
+ value: {
+ type: Boolean,
+ optional: 1,
+ },
+ text: {
+ type: String,
+ optional: 1,
+ },
+ };
+
+ CustomCheckbox.template = 'web.CustomCheckbox';
+
+ return CustomCheckbox;
+});
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 93e9bda5593..f1d4b00c6a9 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -445,6 +445,14 @@ var utils = {
is_bin_size: function (v) {
return (/^\d+(\.\d*)? [^0-9]+$/).test(v);
},
+ /**
+ * Checks if a class is an extension of owl.Component.
+ *
+ * @param {any} value A class reference
+ */
+ isComponent: function (value) {
+ return value.prototype instanceof owl.Component;
+ },
/**
* Returns whether the given anchor is valid.
*
diff --git a/addons/web/static/src/js/fields/abstract_field_owl.js b/addons/web/static/src/js/fields/abstract_field_owl.js
index c9beafc9203..937fc2f88ee 100644
--- a/addons/web/static/src/js/fields/abstract_field_owl.js
+++ b/addons/web/static/src/js/fields/abstract_field_owl.js
@@ -9,34 +9,39 @@ odoo.define('web.AbstractFieldOwl', function (require) {
/**
* This file defines the Owl version of the AbstractField. Specific fields
* written in Owl should override this component.
+ *
+ * =========================================================================
+ *
+ * /!\ This api works almost exactly like the legacy one but
+ * /!\ it still could change! There are already a few methods that will be
+ * /!\ removed like setIdForLabel, setInvalidClass, etc..
+ *
+ * =========================================================================
*
- * Note that the API is not complete yet. Some features may not work properly
- * yet (e.g. part of keyboard navigation, invalid fields notification).
- *
- * This is the basic field widget used by all the views to render a field in a view.
- * These field widgets are mostly common to all views, in particular form and list
+ * This is the basic field component used by all the views to render a field in a view.
+ * These field components are mostly common to all views, in particular form and list
* views.
*
- * The responsabilities of a field widget are mainly:
+ * The responsabilities of a field component are mainly:
* - render a visual representation of the current value of a field
* - that representation is either in 'readonly' or in 'edit' mode
* - notify the rest of the system when the field has been changed by
* the user (in edit mode)
*
* Notes
- * - the widget is not supposed to be able to switch between modes. If another
- * mode is required, the view will take care of instantiating another widget.
+ * - the component is not supposed to be able to switch between modes. If another
+ * mode is required, the view will take care of instantiating another component.
* - notify the system when its value has changed and its mode is changed to 'readonly'
* - notify the system when some action has to be taken, such as opening a record
- * - the Field widget should not, ever, under any circumstance, be aware of
- * its parent. The way it communicates changes with the rest of the system is by
- * triggering events (with trigger_up). These events bubble up and are interpreted
+ * - the Field component should not, ever, under any circumstance, be aware of
+ * its parent. The way it communicates changes with the rest of the system is by
+ * triggering events. These events bubble up and are interpreted
* by the most appropriate parent.
*
- * Also, in some cases, it may not be practical to have the same widget for all
- * views. In that situation, you can have a 'view specific widget'. Just register
- * the widget in the registry prefixed by the view type and a dot. So, for example,
- * a form specific many2one widget should be registered as 'form.many2one'.
+ * Also, in some cases, it may not be practical to have the same component for all
+ * views. In that situation, you can have a 'view specific component'. Just register
+ * the component in the registry prefixed by the view type and a dot. So, for example,
+ * a form specific many2one component should be registered as 'form.many2one'.
*
* @module web.AbstractFieldOwl
*/
@@ -45,118 +50,28 @@ odoo.define('web.AbstractFieldOwl', function (require) {
* Abstract field class
*
* @constructor
- * @param {Widget} parent
- * @param {string} name The field name defined in the model
- * @param {Object} record A record object (result of the get method of
- * a basic model)
- * @param {Object} [options]
- * @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
+ * @param {Component} parent
+ * @param {Object} props
+ * @param {string} props.fieldName The field name defined in the model
+ * @param {Object} props.record A record object (result of the get method
+ * of a basic model)
+ * @param {Object} [props.options]
+ * @param {string} [props.options.mode=readonly] should be 'readonly' or 'edit'
+ * @param {string} [props.options.viewType=default]
*/
- constructor(parent, props) {
- super(parent, props);
+ constructor() {
+ super(...arguments);
- const options = Object.assign({}, props.options);
- const record = props.record;
- // 'name' is the field name displayed by this widget
- this.name = props.fieldName;
-
- // the datapoint fetched from the model
- this.record = record;
-
- // the 'field' property is a description of all the various field properties,
- // such as the type, the comodel (relation), ...
- this.field = record.fields[this.name];
-
- // the 'viewType' is the type of the view in which the field widget is
- // instantiated. For standalone widgets, a 'default' viewType is set.
- this.viewType = options.viewType || 'default';
-
- // the 'attrs' property contains the attributes of the xml 'field' tag,
- // the inner views...
- const fieldsInfo = record.fieldsInfo[this.viewType];
- this.attrs = options.attrs || (fieldsInfo && fieldsInfo[this.name]) || {};
-
- // the 'additionalContext' property contains the attributes to pass through the context.
- this.additionalContext = options.additionalContext || {};
-
- // this property tracks the current (parsed if needed) value of the field.
- // Note that we don't use an event system anymore, using this.get('value')
- // is no longer valid.
- this.value = record.data[this.name];
-
- // recordData tracks the values for the other fields for the same record.
- // note that it is expected to be mostly a readonly property, you cannot
- // use this to try to change other fields value, this is not how it is
- // supposed to work. Also, do not use this.recordData[this.name] to get
- // the current value, this could be out of sync after a _setValue.
- this.recordData = record.data;
-
- // the 'string' property is a human readable (and translated) description
- // of the field. Mostly useful to be displayed in various places in the
- // UI, such as tooltips or create dialogs.
- this.string = this.attrs.string || this.field.string || this.name;
-
- // Widget can often be configured in the 'options' attribute in the
- // xml 'field' tag. These options are saved (and evaled) in nodeOptions
- this.nodeOptions = this.attrs.options || {};
-
- // dataPointID is the id corresponding to the current record in the model.
- // Its intended use is to be able to tag any messages going upstream,
- // so the view knows which records was changed for example.
- this.dataPointID = record.id;
-
- // this is the res_id for the record in database. Obviously, it is
- // readonly. Also, when the user is creating a new record, there is
- // no res_id. When the record will be created, the field widget will
- // be destroyed (when the form view switches to readonly mode) and a new
- // widget with a res_id in mode readonly will be created.
- this.res_id = record.res_id;
-
- // useful mostly to trigger rpcs on the correct model
- this.model = record.model;
-
- // a widget can be in two modes: 'edit' or 'readonly'. This mode should
- // never be changed, if a view changes its mode, it will destroy and
- // recreate a new field widget.
- this.mode = options.mode || "readonly";
-
- // this flag tracks if the widget is in a valid state, meaning that the
- // current value represented in the DOM is a value that can be parsed
- // and saved. For example, a float field can only use a number and not
- // a string.
this._isValid = true;
-
- // this is the last value that was set by the user, unparsed. This is
+ // this is the last value that was set by the user, unparsed. This is
// used to avoid setting the value twice in a row with the exact value.
- this.lastSetValue = undefined;
-
- // formatType is used to determine which format (and parse) functions
- // to call to format the field's value to insert into the DOM (typically
- // put into a span or an input), and to parse the value from the input
- // to send it to the server. These functions are chosen according to
- // the 'widget' attrs if is is given, and if it is a valid key, with a
- // fallback on the field type, ensuring that the value is formatted and
- // displayed according to the chosen widget, if any.
- this.formatType = this.attrs.widget in field_utils.format ?
- this.attrs.widget :
- this.field.type;
- // formatOptions (resp. parseOptions) is a dict of options passed to
- // calls to the format (resp. parse) function.
- this.formatOptions = {};
- this.parseOptions = {};
-
- // if we add decorations, we need to reevaluate the field whenever any
- // value from the record is changed
- if (this.attrs.decorations) {
- this.resetOnAnyFieldChange = true;
- }
+ this._lastSetValue = undefined;
useListener('keydown', this._onKeydown);
useListener('navigation-move', this._onNavigationMove);
onMounted(() => this._applyDecorations());
onPatched(() => this._applyDecorations());
}
-
/**
* Hack: studio tries to find the field with a selector base on its
* name, before it is mounted into the DOM. Ideally, this should be
@@ -174,38 +89,91 @@ odoo.define('web.AbstractFieldOwl', function (require) {
this.el.classList.add('o_field_widget');
return res;
}
-
/**
* @async
* @param {Object} [nextProps]
- * @param {Object} [nextProps.record]
- * @param {Object} [nextProps.event]
* @returns {Promise}
*/
async willUpdateProps(nextProps) {
- this.record = nextProps.record;
- this.recordData = this.record.data;
- this.value = this.recordData[this.name];
- this.lastSetValue = undefined;
+ this._lastSetValue = undefined;
return super.willUpdateProps(nextProps);
}
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
+ // Getters
+ //----------------------------------------------------------------------
/**
- * Returns the main field's DOM element (jQuery form) which can be focused
- * by the browser.
+ * This contains the attributes to pass through the context.
+ *
+ * @returns {Object}
+ */
+ get additionalContext() {
+ return this.options.additionalContext || {};
+ }
+ /**
+ * This contains the attributes of the xml 'field' tag, the inner views...
+ *
+ * @returns {Object}
+ */
+ get attrs() {
+ const fieldsInfo = this.record.fieldsInfo[this.viewType];
+ return this.options.attrs || (fieldsInfo && fieldsInfo[this.name]) || {};
+ }
+ /**
+ * Id corresponding to the current record in the model.
+ * Its intended use is to be able to tag any messages going upstream,
+ * so the view knows which records was changed for example.
+ *
+ * @returns {string}
+ */
+ get dataPointId() {
+ return this.record.id;
+ }
+ /**
+ * This is a description of all the various field properties,
+ * such as the type, the comodel (relation), ...
+ *
+ * @returns {string}
+ */
+ get field() {
+ return this.record.fields[this.name];
+ }
+ /**
+ * Returns the main field's DOM element which can be focused by the browser.
*
- * @returns {HTMLElement|null} main focusable element inside the widget
+ * @returns {HTMLElement|null} main focusable element inside the component
*/
get focusableElement() {
return null;
}
+ /**
+ * Returns the additional options pass to the format function.
+ * Override this getter to add options.
+ *
+ * @returns {Object}
+ */
+ get formatOptions() {
+ return {};
+ }
+ /**
+ * Used to determine which format (and parse) functions
+ * to call to format the field's value to insert into the DOM (typically
+ * put into a span or an input), and to parse the value from the input
+ * to send it to the server. These functions are chosen according to
+ * the 'widget' attrs if is is given, and if it is a valid key, with a
+ * fallback on the field type, ensuring that the value is formatted and
+ * displayed according to the chosen widget, if any.
+ *
+ * @returns {string}
+ */
+ get formatType() {
+ return this.attrs.widget in field_utils.format ?
+ this.attrs.widget : this.field.type;
+ }
/**
* Returns whether or not the field is empty and can thus be hidden. This
- * method is typically called when the widget is in readonly, to hide it
+ * method is typically called when the component is in readonly, to hide it
* (and its label) if it is empty.
*
* @returns {boolean}
@@ -214,39 +182,171 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return !this.isSet;
}
/**
- * Returns true if the widget has a visible element that can take the focus
+ * Returns true if the component has a visible element that can take the focus
*
* @returns {boolean}
*/
get isFocusable() {
const focusable = this.focusableElement;
// check if element is visible
- return focusable && !!(focusable.offsetWidth || focusable.offsetHeight
- || focusable.getClientRects().length);
+ return focusable && !!(focusable.offsetWidth ||
+ focusable.offsetHeight || focusable.getClientRects().length);
}
/**
+ * Determines if the field value is set to a meaningful
+ * value. This is useful to determine if a field should be displayed as empty
+ *
* @returns {boolean}
*/
get isSet() {
return !!this.value;
}
/**
+ * Tracks if the component is in a valid state, meaning that the current
+ * value represented in the DOM is a value that can be parsed and saved.
+ * For example, a float field can only use a number and not a string.
+ *
* @returns {boolean}
*/
get isValid() {
return this._isValid;
}
/**
- * Activates the field widget. By default, activation means focusing and
+ * Fields can be in two modes: 'edit' or 'readonly'.
+ *
+ * @returns {string}
+ */
+ get mode() {
+ return this.options.mode || "readonly";
+ }
+ /**
+ * Useful mostly to trigger rpcs on the correct model.
+ *
+ * @returns {string}
+ */
+ get model() {
+ return this.record.model;
+ }
+ /**
+ * The field name displayed by this component.
+ *
+ * @returns {string}
+ */
+ get name() {
+ return this.props.fieldName;
+ }
+ /**
+ * Component can often be configured in the 'options' attribute in the
+ * xml 'field' tag. These options are saved (and evaled) in nodeOptions.
+ *
+ * @returns {Object}
+ */
+ get nodeOptions() {
+ return this.attrs.options || {};
+ }
+ /**
+ * @returns {Object}
+ */
+ get options() {
+ return this.props.options || {};
+ }
+ /**
+ * Returns the additional options passed to the parse function.
+ * Override this getter to add options.
+ *
+ * @returns {Object}
+ */
+ get parseOptions() {
+ return {};
+ }
+ /**
+ * The datapoint fetched from the model.
+ *
+ * @returns {Object}
+ */
+ get record() {
+ return this.props.record;
+ }
+ /**
+ * Tracks the values for the other fields for the same record.
+ * note that it is expected to be mostly a readonly property, you cannot
+ * use this to try to change other fields value, this is not how it is
+ * supposed to work. Also, do not use this.recordData[this.name] to get
+ * the current value, this could be out of sync after a _setValue.
+ *
+ * @returns {Object}
+ */
+ get recordData() {
+ return this.record.data;
+ }
+ /**
+ * If this flag is set to true, the field component will be reset on
+ * every change which is made in the view (if the view supports it).
+ * This is currently a form view feature.
+ *
+ * /!\ This getter could be removed when basic views (form, list, kanban)
+ * are converted.
+ *
+ * @returns {boolean}
+ */
+ get resetOnAnyFieldChange() {
+ return !!this.attrs.decorations;
+ }
+ /**
+ * The res_id of the record in database.
+ * When the user is creating a new record, there is no res_id.
+ * When the record will be created, the field component will
+ * be destroyed (when the form view switches to readonly mode) and a
+ * new component with a res_id in mode readonly will be created.
+ *
+ * @returns {Number}
+ */
+ get resId() {
+ return this.record.res_id;
+ }
+ /**
+ * Human readable (and translated) description of the field.
+ * Mostly useful to be displayed in various places in the
+ * UI, such as tooltips or create dialogs.
+ *
+ * @returns {string}
+ */
+ get string() {
+ return this.attrs.string || this.field.string || this.name;
+ }
+ /**
+ * Tracks the current (parsed if needed) value of the field.
+ *
+ * @returns {any}
+ */
+ get value() {
+ return this.record.data[this.name];
+ }
+ /**
+ * The type of the view in which the field component is instantiated.
+ * For standalone components, a 'default' viewType is set.
+ *
+ * @returns {string}
+ */
+ get viewType() {
+ return this.options.viewType || 'default';
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * Activates the field component. By default, activation means focusing and
* selecting (if possible) the associated focusable element. The selecting
- * part can be disabled. In that case, note that the focused input/textarea
+ * part can be disabled. In that case, note that the focused input/textarea
* will have the cursor at the very end.
*
* @param {Object} [options]
* @param {boolean} [options.noselect=false] if false and the input
* is of type text or textarea, the content will also be selected
* @param {Event} [options.event] the event which fired this activation
- * @returns {boolean} true if the widget was activated, false if the
+ * @returns {boolean} true if the component was activated, false if the
* focusable element was not found or invisible
*/
activate(options) {
@@ -264,50 +364,60 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return false;
}
/**
- * This function should be implemented by widgets that are not able to
+ * This function should be implemented by components that are not able to
* notify their environment when their value changes (maybe because their
* are not aware of the changes) or that may have a value in a temporary
* state (maybe because some action should be performed to validate it
* before notifying it). This is typically called before trying to save the
- * widget's value, so it should call _setValue() to notify the environment
+ * component's value, so it should call _setValue() to notify the environment
* if the value changed but was not notified.
*
* @abstract
* @returns {Promise|undefined}
*/
commitChanges() {}
+ /**
+ * Remove the invalid class on a field
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
+ */
+ removeInvalidClass() {
+ this.el.classList.remove('o_field_invalid');
+ this.el.removeAttribute('aria-invalid');
+ }
/**
* Sets the given id on the focusable element of the field and as 'for'
* attribute of potential internal labels.
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
*
* @param {string} id
*/
- setIDForLabel(id) {
+ setIdForLabel(id) {
if (this.focusableElement) {
this.focusableElement.setAttribute('id', id);
}
}
/**
- * Update the modifiers with the newest value.
- * Now this.attrs.modifiersValue can be used consistantly even with
- * conditional modifiers inside field widgets, and without needing new
- * events or synchronization between the widgets, renderer and controller
- *
- * @param {Object | null} modifiers the updated modifiers
- * @override
+ * add the invalid class on a field
+ *
+ * This function should be removed when BasicRenderer will be rewritten in owl
*/
- updateModifiersValue(modifiers) {
- this.attrs.modifiersValue = modifiers || {};
+ setInvalidClass() {
+ this.el.classList.add('o_field_invalid');
+ this.el.setAttribute('aria-invalid', 'true');
}
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
// Private
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
/**
* Apply field decorations (only if field-specific decorations have been
* defined in an attribute).
*
+ * This function should be removed when BasicRenderer will be rewritten in owl
+ *
* @private
*/
_applyDecorations() {
@@ -326,13 +436,13 @@ odoo.define('web.AbstractFieldOwl', function (require) {
* @returns {string}
*/
_formatValue(value) {
- const options = _.extend({}, this.nodeOptions,
+ const options = Object.assign({}, this.nodeOptions,
{ data: this.recordData }, this.formatOptions);
return field_utils.format[this.formatType](value, this.field, options);
}
/**
* This method check if a value is the same as the current value of the
- * field. For example, a fieldDate widget might want to use the moment
+ * field. For example, a fieldDate component might want to use the moment
* specific value isSame instead of ===.
*
* This method is used by the _setValue method.
@@ -355,18 +465,18 @@ odoo.define('web.AbstractFieldOwl', function (require) {
return field_utils.parse[this.formatType](value, this.field, this.parseOptions);
}
/**
- * This method is called by the widget, to change its value and to notify
- * the outside world of its new state. This method also validates the new
- * value. Note that this method does not rerender the widget, it should be
- * handled by the widget itself, if necessary.
+ * This method is called by the component, to change its value and to notify
+ * the outside world of its new state. This method also validates the new
+ * value. Note that this method does not rerender the component, it should be
+ * handled by the component itself, if necessary.
*
* @private
* @param {any} value
* @param {Object} [options]
* @param {boolean} [options.doNotSetDirty=false] if true, the basic model
* will not consider that this field is dirty, even though it was changed.
- * Please do not use this flag unless you really need it. Our only use
- * case is currently the pad widget, which does a _setValue in the
+ * Please do not use this flag unless you really need it. Our only use
+ * case is currently the pad component, which does a _setValue in the
* renderEdit method.
* @param {boolean} [options.notifyChange=true] if false, the basic model
* will not notify and not trigger the onchange, even though it was changed.
@@ -377,16 +487,16 @@ odoo.define('web.AbstractFieldOwl', function (require) {
_setValue(value, options) {
// we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values.
- if (this.lastSetValue === value || (this.value === false && value === '')) {
+ if (this._lastSetValue === value || (this.value === false && value === '')) {
return Promise.resolve();
}
- this.lastSetValue = value;
+ this._lastSetValue = value;
try {
value = this._parseValue(value);
this._isValid = true;
} catch (e) {
this._isValid = false;
- this.trigger('set-dirty', {dataPointID: this.dataPointID});
+ this.trigger('set-dirty', {dataPointID: this.dataPointId});
return Promise.reject({message: "Value set is not valid"});
}
if (!(options && options.forceChange) && this._isSameValue(value)) {
@@ -396,7 +506,7 @@ odoo.define('web.AbstractFieldOwl', function (require) {
const changes = {};
changes[this.name] = value;
this.trigger('field-changed', {
- dataPointID: this.dataPointID,
+ dataPointID: this.dataPointId,
changes: changes,
viewType: this.viewType,
doNotSetDirty: options && options.doNotSetDirty,
@@ -408,20 +518,14 @@ odoo.define('web.AbstractFieldOwl', function (require) {
});
}
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
// Handlers
- //--------------------------------------------------------------------------
+ //----------------------------------------------------------------------
/**
* Intercepts navigation keyboard events to prevent their default behavior
* and notifies the view so that it can handle it its own way.
*
- * Note: the navigation keyboard events are stopped so that potential parent
- * abstract field does not trigger the navigation_move event a second time.
- * However, this might be controversial, we might wanna let the event
- * continue its propagation and flag it to say that navigation has already
- * been handled (TODO ?).
- *
* @private
* @param {KeyEvent} ev
*/
@@ -430,9 +534,8 @@ odoo.define('web.AbstractFieldOwl', function (require) {
case $.ui.keyCode.TAB:
this.trigger('navigation-move', {
direction: ev.shiftKey ? 'previous' : 'next',
+ originalEvent: ev,
});
- // TODO: stop/prevent original event if navigation-move event
- // has been handled
break;
case $.ui.keyCode.ENTER:
// We preventDefault the ENTER key because of two coexisting behaviours:
@@ -479,18 +582,13 @@ odoo.define('web.AbstractFieldOwl', function (require) {
}
/**
- * An object representing fields to be fetched by the model eventhough not present in the view
+ * An object representing fields to be fetched by the model even though not
+ * present in the view.
* This object contains "field name" as key and an object as value.
* That value object must contain the key "type"
- * see FieldBinaryImage for an example.
+ * @see FieldBinaryImage for an example.
*/
AbstractField.fieldDependencies = {};
- /**
- * If this flag is set to true, the field widget will be reset on every
- * change which is made in the view (if the view supports it). This is
- * currently a form view feature.
- */
- AbstractField.resetOnAnyFieldChange = false;
/**
* If this flag is given a string, the related BasicModel will be used to
* initialize specialData the field might need. This data will be available
@@ -500,15 +598,15 @@ odoo.define('web.AbstractFieldOwl', function (require) {
*/
AbstractField.specialData = false;
/**
- * to override to indicate which field types are supported by the widget
+ * to override to indicate which field types are supported by the component
*
- * @type Array
+ * @type Array
*/
AbstractField.supportedFieldTypes = [];
/**
- * To override to give a user friendly name to the widget.
+ * To override to give a user friendly name to the component.
*
- * @type
+ * @type string
*/
AbstractField.description = "";
/**
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 3a3a037a0f0..ce7a7fac07d 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -11,6 +11,7 @@ var AbstractField = require('web.AbstractField');
var config = require('web.config');
var core = require('web.core');
var datepicker = require('web.datepicker');
+var deprecatedFields = require('web.basic_fields.deprecated');
var dom = require('web.dom');
var Domain = require('web.Domain');
var DomainSelector = require('web.DomainSelector');
@@ -24,6 +25,8 @@ var field_utils = require('web.field_utils');
var time = require('web.time');
var ColorpickerDialog = require('web.ColorpickerDialog');
+let FieldBoolean = deprecatedFields.FieldBoolean;
+
require("web.zoomodoo");
var qweb = core.qweb;
@@ -986,134 +989,6 @@ var FieldMonetary = NumericField.extend({
},
});
-var FieldBoolean = AbstractField.extend({
- className: 'o_field_boolean',
- description: _lt("Checkbox"),
- events: _.extend({}, AbstractField.prototype.events, {
- change: '_onChange',
- }),
- supportedFieldTypes: ['boolean'],
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Toggle the checkbox if it is activated due to a click on itself.
- *
- * @override
- */
- activate: function (options) {
- var activated = this._super.apply(this, arguments);
- // The formatValue of boolean fields renders HTML elements similar to
- // the one rendered by the widget itself. Even though the event might
- // have been fired on the non-widget version of this field, we can still
- // test the presence of its custom class.
- if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) {
- this._setValue(!this.value); // Toggle the checkbox
- }
- return activated;
- },
-
- /**
- * @override
- * @returns {jQuery} the focusable checkbox input
- */
- getFocusableElement: function () {
- return this.mode === 'readonly' ? $() : this.$input;
- },
- /**
- * A boolean field is always set since false is a valid value.
- *
- * @override
- */
- isSet: function () {
- return true;
- },
- /**
- * When the checkbox is rerendered, we need to check if it was the actual
- * origin of the reset. If it is, we need to activate it back so it looks
- * like it was not rerendered but is still the same input.
- *
- * @override
- */
- reset: function (record, event) {
- var rendered = this._super.apply(this, arguments);
- if (event && event.target.name === this.name) {
- this.activate();
- }
- return rendered;
- },
- /**
- * Associates the 'for' attribute of the internal label.
- *
- * @override
- */
- setIDForLabel: function (id) {
- this._super.apply(this, arguments);
- this.$('.custom-control-label').attr('for', id);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * The actual checkbox is designed in css to have full control over its
- * appearance, as opposed to letting the browser and the os decide how
- * a checkbox should look. The actual input is disabled and hidden. In
- * readonly mode, the checkbox is disabled.
- *
- * @override
- * @private
- */
- _render: function () {
- var $checkbox = this._formatValue(this.value);
- this.$input = $checkbox.find('input');
- this.$input.prop('disabled', this.mode === 'readonly');
- this.$el.addClass($checkbox.attr('class'));
- this.$el.empty().append($checkbox.contents());
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Properly update the value when the checkbox is (un)ticked to trigger
- * possible onchanges.
- *
- * @private
- */
- _onChange: function () {
- this._setValue(this.$input[0].checked);
- },
- /**
- * Implement keyboard movements. Mostly useful for its environment, such
- * as a list view.
- *
- * @override
- * @private
- * @param {KeyEvent} ev
- */
- _onKeydown: function (ev) {
- switch (ev.which) {
- case $.ui.keyCode.ENTER:
- // prevent subsequent 'click' event (see _onKeydown of AbstractField)
- ev.preventDefault();
- this.$input.prop('checked', !this.value);
- this._setValue(!this.value);
- return;
- case $.ui.keyCode.UP:
- case $.ui.keyCode.RIGHT:
- case $.ui.keyCode.DOWN:
- case $.ui.keyCode.LEFT:
- ev.preventDefault();
- }
- this._super.apply(this, arguments);
- },
-});
-
var FieldInteger = NumericField.extend({
description: _lt("Integer"),
className: 'o_field_integer o_field_number',
diff --git a/addons/web/static/src/js/fields/basic_fields_owl.js b/addons/web/static/src/js/fields/basic_fields_owl.js
new file mode 100644
index 00000000000..a00b14fb9ee
--- /dev/null
+++ b/addons/web/static/src/js/fields/basic_fields_owl.js
@@ -0,0 +1,111 @@
+odoo.define('web.basic_fields_owl', function (require) {
+ "use strict";
+
+ const AbstractField = require('web.AbstractFieldOwl');
+ const CustomCheckbox = require('web.CustomCheckbox');
+ const core = require('web.core');
+
+ const _lt = core._lt;
+
+ class FieldBoolean extends AbstractField {
+ patched() {
+ super.patched();
+ if (this.props.event && this.props.event.target === this) {
+ this.activate();
+ }
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ * @returns {HTMLElement|null} the focusable checkbox input
+ */
+ get focusableElement() {
+ return this.mode === 'readonly' ? null : this.el.querySelector('input');
+ }
+ /**
+ * A boolean field is always set since false is a valid value.
+ *
+ * @override
+ */
+ get isSet() {
+ return true;
+ }
+ /**
+ * Toggle the checkbox if it is activated due to a click on itself.
+ *
+ * @override
+ * @param {Object} [options]
+ * @param {Event} [options.event] the event which fired this activation
+ * @returns {boolean} true if the component was activated, false if the
+ * focusable element was not found or invisible
+ */
+ activate(options) {
+ const activated = super.activate(options);
+ // The event might have been fired on the non field version of
+ // this field, we can still test the presence of its custom class.
+ if (activated && options && options.event && options.event.target
+ .closest('.custom-control.custom-checkbox')) {
+ this._setValue(!this.value); // Toggle the checkbox
+ }
+ return activated;
+ }
+ /**
+ * Associates the 'for' attribute of the internal label.
+ *
+ * @override
+ */
+ setIdForLabel(id) {
+ super.setIdForLabel(id);
+ this.el.querySelector('label').setAttribute('for', id);
+ }
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * Properly update the value when the checkbox is (un)ticked to trigger
+ * possible onchanges.
+ *
+ * @private
+ */
+ _onChange(ev) {
+ this._setValue(ev.target.checked);
+ }
+ /**
+ * Implement keyboard movements. Mostly useful for its environment, such
+ * as a list view.
+ *
+ * @override
+ * @private
+ * @param {KeyEvent} ev
+ */
+ _onKeydown(ev) {
+ switch (ev.which) {
+ case $.ui.keyCode.ENTER:
+ // prevent subsequent 'click' event (see _onKeydown of AbstractField)
+ ev.preventDefault();
+ this._setValue(!this.value);
+ return;
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.RIGHT:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
+ }
+ super._onKeydown(ev);
+ }
+ }
+ FieldBoolean.components = { CustomCheckbox };
+ FieldBoolean.description = _lt("Checkbox");
+ FieldBoolean.supportedFieldTypes = ['boolean'];
+ FieldBoolean.template = 'web.FieldBoolean';
+
+ return {
+ FieldBoolean,
+ };
+});
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 49f1f678304..b8b3fe61452 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -3,7 +3,16 @@ odoo.define('web.field_registry', function (require) {
var Registry = require('web.Registry');
-return new Registry();
+var FieldRegistry = Registry.extend({
+ add: function (key, value, score) {
+ if (value.prototype instanceof owl.Component) {
+ throw new Error("This registry should not contain any Component. Use 'web.field_registry_owl' instead.");
+ }
+ return this._super(...arguments);
+ },
+});
+
+return new FieldRegistry();
});
odoo.define('web._field_registry', function (require) {
diff --git a/addons/web/static/src/js/fields/field_registry_owl.js b/addons/web/static/src/js/fields/field_registry_owl.js
new file mode 100644
index 00000000000..f5f7d900ef8
--- /dev/null
+++ b/addons/web/static/src/js/fields/field_registry_owl.js
@@ -0,0 +1,31 @@
+odoo.define('web.field_registry_owl', function (require) {
+ "use strict";
+
+ const Registry = require('web.Registry');
+
+ const FieldRegistry = Registry.extend({
+ add: function (key, value, score) {
+ if (!(value.prototype instanceof owl.Component)) {
+ throw new Error("This registry should only contain subclasses of Component");
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ return new FieldRegistry();
+});
+
+odoo.define('web._field_registry_owl', function (require) {
+ "use strict";
+
+ /**
+ * This module registers field components (specifications of the AbstractField Component)
+ */
+
+ const basicFields = require('web.basic_fields_owl');
+ const registry = require('web.field_registry_owl');
+
+ // Basic fields
+ registry
+ .add('boolean', basicFields.FieldBoolean);
+});
diff --git a/addons/web/static/src/js/fields/field_wrapper.js b/addons/web/static/src/js/fields/field_wrapper.js
index 0b808d0c732..8cf747d84e9 100644
--- a/addons/web/static/src/js/fields/field_wrapper.js
+++ b/addons/web/static/src/js/fields/field_wrapper.js
@@ -33,8 +33,6 @@ odoo.define('web.FieldWrapper', function (require) {
this._data.res_id = record.res_id;
this._data.model = record.model;
this._data.mode = options.mode || "readonly";
- this._data._isValid = true;
- this._data.lastSetValue = undefined;
this._data.formatType = this._data.attrs.widget in field_utils.format ?
this._data.attrs.widget :
this._data.field.type;
@@ -43,9 +41,19 @@ odoo.define('web.FieldWrapper', function (require) {
if (this._data.attrs.decorations) {
this._data.resetOnAnyFieldChange = true;
}
- for (const key in this._data) {
+
+ for (let key in this._data) {
Object.defineProperty(this, key, {
- get: () => (this.el ? this.componentRef.comp : this._data)[key],
+ get: () => {
+ if (this.el) {
+ if (key === 'dataPointID') {
+ return this.componentRef.comp.dataPointId;
+ } else if (key === 'res_id') {
+ return this.componentRef.comp.resId;
+ }
+ }
+ return (this.el ? this.componentRef.comp : this._data)[key];
+ },
});
}
}
@@ -60,9 +68,6 @@ odoo.define('web.FieldWrapper', function (require) {
get fieldDependencies() {
return this.Component.fieldDependencies;
}
- get resetOnAnyFieldChange() {
- return this.Component.resetOnAnyFieldChange;
- }
get specialData() {
return this.Component.specialData;
}
@@ -80,11 +85,11 @@ odoo.define('web.FieldWrapper', function (require) {
// Public
//----------------------------------------------------------------------
- activate(options) {
- return this.componentRef.comp.activate(options);
+ activate() {
+ return this.componentRef.comp.activate(...arguments);
}
commitChanges() {
- return this.componentRef.comp.commitChanges();
+ return this.componentRef.comp.commitChanges(...arguments);
}
getFocusableElement() {
return $(this.componentRef.comp.focusableElement);
@@ -99,7 +104,7 @@ odoo.define('web.FieldWrapper', function (require) {
if (this.componentRef.comp) {
return this.componentRef.comp.isSet;
}
- // because of the willStart, the real field widget may not be
+ // because of the willStart, the real field component may not be
// instantiated yet when the renderer first asks if it is set
// (only the wrapper is instantiated), so we instantiate one
// with the same props, get its 'isSet' status, and destroy it.
@@ -111,14 +116,27 @@ odoo.define('web.FieldWrapper', function (require) {
isValid() {
return this.componentRef.comp.isValid;
}
+ removeInvalidClass() {
+ return this.componentRef.comp.removeInvalidClass(...arguments);
+ }
reset(record, event) {
return this.update({record, event});
}
- setIDForLabel(id) {
- return this.componentRef.comp.setIDForLabel(id);
+ setIDForLabel() {
+ return this.componentRef.comp.setIdForLabel(...arguments);
+ }
+ setInvalidClass() {
+ return this.componentRef.comp.setInvalidClass(...arguments);
}
updateModifiersValue(modifiers) {
- return this.componentRef.comp.updateModifiersValue(modifiers);
+ if (this.props.options.attrs) {
+ this.props.options.attrs.modifiersValue = modifiers || {};
+ } else {
+ const viewType = this.props.options.viewType || 'default';
+ const fieldsInfo = this.props.record.fieldsInfo[viewType];
+ fieldsInfo[this.props.fieldName].modifiersValue = modifiers || {};
+ }
+ this.componentRef.comp.props = this.props;
}
}
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 140c338d6f6..4d266657c71 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -15,6 +15,7 @@ var BasicController = require('web.BasicController');
var BasicModel = require('web.BasicModel');
var config = require('web.config');
var fieldRegistry = require('web.field_registry');
+var fieldRegistryOwl = require('web.field_registry_owl');
var pyUtils = require('web.py_utils');
var utils = require('web.utils');
@@ -78,7 +79,8 @@ var BasicView = AbstractView.extend({
_getFieldWidgetClass: function (viewType, field, attrs) {
var FieldWidget;
if (attrs.widget) {
- FieldWidget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
+ FieldWidget = fieldRegistryOwl.getAny([viewType + "." + attrs.widget, attrs.widget]) ||
+ fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
if (!FieldWidget) {
console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
}
@@ -87,7 +89,9 @@ var BasicView = AbstractView.extend({
// is not specified in the view
FieldWidget = fieldRegistry.get('kanban.many2many_tags');
}
- return FieldWidget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
+ return FieldWidget ||
+ fieldRegistryOwl.getAny([viewType + "." + field.type, field.type, "abstract"]) ||
+ fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
},
/**
* In some cases, we already have a preloaded record
diff --git a/addons/web/static/src/js/views/calendar/calendar_popover.js b/addons/web/static/src/js/views/calendar/calendar_popover.js
index d804f74e53e..9b4af4bb065 100644
--- a/addons/web/static/src/js/views/calendar/calendar_popover.js
+++ b/addons/web/static/src/js/views/calendar/calendar_popover.js
@@ -2,10 +2,13 @@ odoo.define('web.CalendarPopover', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
+const fieldRegistryOwl = require('web.field_registry_owl');
+const FieldWrapper = require('web.FieldWrapper');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var Widget = require('web.Widget');
+const { WidgetAdapterMixin } = require('web.OwlCompatibility');
-var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
+var CalendarPopover = Widget.extend(WidgetAdapterMixin, StandaloneFieldManagerMixin, {
template: 'CalendarView.event.popover',
events: {
'click .o_cw_popover_edit': '_onClickPopoverEdit',
@@ -45,6 +48,25 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
});
return this._super.apply(this, arguments);
},
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ WidgetAdapterMixin.destroy.call(this);
+ },
+ /**
+ * Called each time the widget is attached into the DOM.
+ */
+ on_attach_callback: function () {
+ WidgetAdapterMixin.on_attach_callback.call(this);
+ },
+ /**
+ * Called each time the widget is detached from the DOM.
+ */
+ on_detach_callback: function () {
+ WidgetAdapterMixin.on_detach_callback.call(this);
+ },
//--------------------------------------------------------------------------
// Public
@@ -129,8 +151,20 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
var record = self.model.get(recordID);
_.each(fieldsToGenerate, function (field) {
if (field.invisible) return;
- var FieldClass = fieldRegistry.getAny([field.widget, field.type]);
- var fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
+ let isLegacy = true;
+ let fieldWidget;
+ let FieldClass = fieldRegistryOwl.getAny([field.widget, field.type]);
+ if (FieldClass) {
+ isLegacy = false;
+ fieldWidget = new FieldWrapper(this, FieldClass, {
+ fieldName: field.name,
+ record,
+ options: self.displayFields[field.name],
+ });
+ } else {
+ FieldClass = fieldRegistry.getAny([field.widget, field.type]);
+ fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
+ }
self._registerWidget(recordID, field.name, fieldWidget);
var $field = $('
', {class: 'list-group-item flex-shrink-0 d-flex flex-wrap'});
@@ -139,7 +173,13 @@ var CalendarPopover = Widget.extend(StandaloneFieldManagerMixin, {
var $fieldContainer = $('
', {class: 'flex-grow-1'});
$fieldContainer.appendTo($field);
- defs.push(fieldWidget.appendTo($fieldContainer).then(function () {
+ let def;
+ if (isLegacy) {
+ def = fieldWidget.appendTo($fieldContainer);
+ } else {
+ def = fieldWidget.mount($fieldContainer[0]);
+ }
+ defs.push(def.then(function () {
self.$fieldsList.push($field);
}));
});
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index bcab9808773..a258d16a917 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -1065,6 +1065,13 @@ var FormRenderer = BasicRenderer.extend({
*/
_onNavigationMove: function (ev) {
ev.stopPropagation();
+ // We prevent the default behaviour and stop the propagation of the
+ // originalEvent when the originalEvent is a tab keydown to not let
+ // the browser do it. The action is done by this renderer.
+ if (ev.data.originalEvent && ['next', 'previous'].includes(ev.data.direction)) {
+ ev.data.originalEvent.preventDefault();
+ ev.data.originalEvent.stopPropagation();
+ }
var index;
let target = ev.data.target || ev.target;
if (target.__owl__) {
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 8b4db487ac4..8cd7f5a370d 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -15,6 +15,7 @@ var core = require('web.core');
var dom = require('web.dom');
var ListRenderer = require('web.ListRenderer');
var utils = require('web.utils');
+const { WidgetAdapterMixin } = require('web.OwlCompatibility');
var _t = core._t;
@@ -462,6 +463,9 @@ ListRenderer.include({
}
return Promise.all(defs).then(function () {
+ // mark Owl sub components as mounted
+ WidgetAdapterMixin.on_attach_callback.call(self);
+
// necessary to trigger resize on fieldtexts
core.bus.trigger('DOM_updated');
});
@@ -1553,6 +1557,10 @@ ListRenderer.include({
return;
}
ev.stopPropagation(); // stop the event, the action is done by this renderer
+ if (ev.data.originalEvent && ['next', 'previous'].includes(ev.data.direction)) {
+ ev.data.originalEvent.preventDefault();
+ ev.data.originalEvent.stopPropagation();
+ }
switch (ev.data.direction) {
case 'previous':
if (this.currentFieldIndex > 0) {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 2b0ff6f31dd..db1e893c168 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -93,6 +93,21 @@
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/xml/fields.xml b/addons/web/static/src/xml/fields.xml
new file mode 100644
index 00000000000..ff7939f36a3
--- /dev/null
+++ b/addons/web/static/src/xml/fields.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/tests/components/custom_checkbox_tests.js b/addons/web/static/tests/components/custom_checkbox_tests.js
new file mode 100644
index 00000000000..21d7ae53af2
--- /dev/null
+++ b/addons/web/static/tests/components/custom_checkbox_tests.js
@@ -0,0 +1,56 @@
+odoo.define('web.custom_checkbox_tests', function (require) {
+ "use strict";
+
+ const CustomCheckbox = require('web.CustomCheckbox');
+ const testUtils = require('web.test_utils');
+
+ const { createComponent, dom: testUtilsDom } = testUtils;
+
+ QUnit.module('Components', {}, function () {
+
+ QUnit.module('CustomCheckbox');
+
+ QUnit.test('test checkbox: default values', async function(assert) {
+ assert.expect(6);
+
+ const checkbox = await createComponent(CustomCheckbox, {});
+
+ assert.containsOnce(checkbox.el, 'input');
+ assert.containsNone(checkbox.el, 'input:disabled');
+ assert.containsOnce(checkbox.el, 'label');
+
+ const input = checkbox.el.querySelector('input');
+ assert.notOk(input.checked, 'checkbox should be unchecked');
+ assert.ok(input.id.startsWith('checkbox-comp-'));
+
+ await testUtilsDom.click(checkbox.el.querySelector('label'));
+ assert.ok(input.checked, 'checkbox should be checked');
+
+ checkbox.destroy();
+ });
+
+ QUnit.test('test checkbox: custom values', async function(assert) {
+ assert.expect(6);
+
+ const checkbox = await createComponent(CustomCheckbox, {
+ props: {
+ id: 'my-custom-checkbox',
+ disabled: true,
+ value: true,
+ text: 'checkbox',
+ }
+ });
+
+ assert.containsOnce(checkbox.el, 'input');
+ assert.containsOnce(checkbox.el, 'input:disabled');
+ assert.containsOnce(checkbox.el, 'label');
+
+ const input = checkbox.el.querySelector('input');
+ assert.ok(input.checked, 'checkbox should be checked');
+ assert.strictEqual(input.id, 'my-custom-checkbox');
+ assert.ok(input.checked, 'checkbox should be checked');
+
+ checkbox.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 6bd62b2d25c..710dd66e40e 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -267,12 +267,14 @@ QUnit.module('basic_fields', {
assert.containsOnce(form, '.o_field_boolean input:checked',
"checkbox should now be checked");
// blindly press enter again, it should uncheck the checkbox
- $(document.activeElement).trigger({type: "keydown", which: $.ui.keyCode.ENTER});
+ await testUtils.dom.triggerEvent(document.activeElement, "keydown",
+ {which: $.ui.keyCode.ENTER});
assert.containsNone(form, '.o_field_boolean input:checked',
"checkbox should not be checked");
await testUtils.nextTick();
// blindly press enter again, it should check the checkbox back
- $(document.activeElement).trigger({type: "keydown", which: $.ui.keyCode.ENTER});
+ await testUtils.dom.triggerEvent(document.activeElement, "keydown",
+ {which: $.ui.keyCode.ENTER});
assert.containsOnce(form, '.o_field_boolean input:checked',
"checkbox should still be checked");
@@ -333,6 +335,7 @@ QUnit.module('basic_fields', {
// the checkbox directly in readonly mode !
$cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
await testUtils.dom.click($cell.find('.custom-checkbox .custom-control-label'));
+ await testUtils.nextTick();
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5,
"should still have 5 checkboxes");
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 9b5cad26e75..ee3c68534e3 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -4308,17 +4308,19 @@ QUnit.module('Views', {
// focus first input, trigger tab
form.$('input[name="foo"]').focus();
- form.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ const tabKey = { keyCode: $.ui.keyCode.TAB, which: $.ui.keyCode.TAB };
+ await testUtils.dom.triggerEvent(form.$('input[name="foo"]'), 'keydown', tabKey);
assert.ok($.contains(form.$('div[name="bar"]')[0], document.activeElement),
"bar checkbox should be focused");
- form.$('div[name="bar"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ await testUtils.dom.triggerEvent(document.activeElement, 'keydown', tabKey);
assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
"display_name should be focused");
// simulate shift+tab on active element
- $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
- $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
+ const shiftTabKey = Object.assign({}, tabKey, { shiftKey: true });
+ await testUtils.dom.triggerEvent(document.activeElement, 'keydown', shiftTabKey);
+ await testUtils.dom.triggerEvent(document.activeElement, 'keydown', shiftTabKey);
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"first input should be focused");
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index c5e78394245..20bf8b43205 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -284,14 +284,17 @@
+
+
+
@@ -299,6 +302,7 @@
+
@@ -715,6 +719,7 @@
+
diff --git a/odoo/addons/test_new_api/static/tests/tours/x2many.js b/odoo/addons/test_new_api/static/tests/tours/x2many.js
index a9ad0a7fecc..1eeb10e6977 100644
--- a/odoo/addons/test_new_api/static/tests/tours/x2many.js
+++ b/odoo/addons/test_new_api/static/tests/tours/x2many.js
@@ -223,7 +223,7 @@ odoo.define('web.test.x2many', function (require) {
extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody tr td:contains(aaa)',
}, {
content: "open the many2one to select another user",
- trigger: '.o_input_dropdown > input',
+ trigger: '.o_field_many2one[name="author"] .o_input_dropdown > input',
run: 'text Marc',
}, {
content: "select another user",