本指南是关于为Odoo的Web客户端创建模块的。
一个简单的模块
让我们从一个简单的Odoo模块开始,该模块包含基本的Web组件配置,并让我们测试Web框架。
该示例模块在线可用,可以使用以下命令下载:
$ git clone http://github.com/odoo/petstore
无论您在何处执行命令,都会创建一个petstore
文件夹。
然后,您需要将该文件夹添加到Odoo的addons路径
,创建一个新数据库并安装oepetstore
模块。
如果浏览petstore
文件夹,则应看到以下内容:
oepetstore
|-- images
| |-- alligator.jpg
| |-- ball.jpg
| |-- crazy_circle.jpg
| |-- fish.jpg
| `-- mice.jpg
|-- __init__.py
|-- oepetstore.message_of_the_day.csv
|-- __manifest__.py
|-- petstore_data.xml
|-- petstore.py
|-- petstore.xml
`-- static
`-- src
|-- css
| `-- petstore.css
|-- js
| `-- petstore.js
`-- xml
`-- petstore.xml
该模块已经拥有各种服务器定制。 稍后我们将再次介绍这些内容,现在让我们集中讨论static
文件夹中与Web相关的内容。
Odoo模块的“ Web”端使用的文件必须放置在static
文件夹中,以便Web浏览器可以使用它们,浏览器无法提取该文件夹之外的文件。 src / css
,src / js
和src / xml
子文件夹是常规的,并非绝对必要。
oepetstore/static/css/petstore.css
- 当前为空,将保留CSS用于宠物商店的内容
oepetstore/static/xml/petstore.xml
- 通常为空,将保存QWeb模板
oepetstore/static/js/petstore.js
最重要(也是最有趣)的部分包含应用程序的逻辑(至少是它的Web浏览器端),如javascript。 当前应如下所示:
odoo.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; local.HomePage = instance.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, }); instance.web.client_actions.add( 'petstore.homepage', 'instance.oepetstore.HomePage'); }
仅在浏览器的控制台中显示一条小消息。
必须在模块内定义static
文件夹中的文件,以便正确加载它们。 src / xml
中的所有内容都在__ manifest __。py
中定义,而src / css
和src / js
的内容在petstore.xml
或类似文件中定义。
Odoo JavaScript模块
Javascript没有内置模块。 结果,将不同文件中定义的变量全部混在一起,并且可能会发生冲突。 这引起了用于构建干净名称空间的各种模块模式,并限制了命名冲突的风险。
Odoo框架使用一种这样的模式来定义Web插件中的模块,以便命名空间代码和正确地排序其加载。
oepetstore / static / js / petstore.js
包含模块声明:
odoo.oepetstore = function(instance, local) {
local.xxx = ...;
}
在Odoo网站中,模块被声明为在全局odoo
变量上设置的函数。 该函数的名称必须与插件相同(在本例中为oepetstore
),以便框架可以找到它并自动对其进行初始化。
Web客户端加载模块时,它将调用root函数并提供两个参数:
- 第一个参数是Odoo Web客户端的当前实例,它可以访问Odoo定义的各种功能(翻译,网络服务)以及核心或其他模块定义的对象。
- 第二个参数是Web客户端自动创建的您自己的本地名称空间。 应该从模块外部访问对象和变量(因为Odoo Web客户端需要调用它们,或者因为其他人可能要自定义它们)应该在该命名空间中设置。
Classes
Much as modules, and contrary to most object-oriented languages, javascript does not build in classes1 although it provides roughly equivalent (if lower-level and more verbose) mechanisms.
For simplicity and developer-friendliness Odoo web provides a class system based on John Resig's Simple JavaScript Inheritance.
New classes are defined by calling the extend()
method of odoo.web.Class()
:
var MyClass = instance.web.Class.extend({
say_hello: function() {
console.log("hello");
},
});
The extend()
method takes a dictionary describing
the new class's content (methods and static attributes). In this case, it will
only have a say_hello
method which takes no parameters.
Classes are instantiated using the new
operator:
var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console
And attributes of the instance can be accessed via this
:
var MyClass = instance.web.Class.extend({
say_hello: function() {
console.log("hello", this.name);
},
});
var my_object = new MyClass();
my_object.name = "Bob";
my_object.say_hello();
// print "hello Bob" in the console
Classes can provide an initializer to perform the initial setup of the
instance, by defining an init()
method. The initializer receives the
parameters passed when using the new
operator:
var MyClass = instance.web.Class.extend({
init: function(name) {
this.name = name;
},
say_hello: function() {
console.log("hello", this.name);
},
});
var my_object = new MyClass("Bob");
my_object.say_hello();
// print "hello Bob" in the console
It is also possible to create subclasses from existing (used-defined) classes
by calling extend()
on the parent class, as is done
to subclass Class()
:
var MySpanishClass = MyClass.extend({
say_hello: function() {
console.log("hola", this.name);
},
});
var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hola Bob" in the console
When overriding a method using inheritance, you can use this._super()
to
call the original method:
var MySpanishClass = MyClass.extend({
say_hello: function() {
this._super();
console.log("translation in Spanish: hola", this.name);
},
});
var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hello Bob \n translation in Spanish: hola Bob" in the console
Warning
_super
is not a standard method, it is set on-the-fly to the next
method in the current inheritance chain, if any. It is only defined
during the synchronous part of a method call, for use in asynchronous
handlers (after network calls or in setTimeout
callbacks) a reference
to its value should be retained, it should not be accessed via this
:
// broken, will generate an error
say_hello: function () {
setTimeout(function () {
this._super();
}.bind(this), 0);
}
// correct
say_hello: function () {
// don't forget .bind()
var _super = this._super.bind(this);
setTimeout(function () {
_super();
}.bind(this), 0);
}
Widgets Basics
The Odoo web client bundles jQuery for easy DOM manipulation. It is useful and provides a better API than standard W3C DOM2, but insufficient to structure complex applications leading to difficult maintenance.
Much like object-oriented desktop UI toolkits (e.g. Qt, Cocoa or GTK),
Odoo Web makes specific components responsible for sections of a page. In
Odoo web, the base for such components is the Widget()
class, a component specialized in handling a page section and displaying
information for the user.
Your First Widget
The initial demonstration module already provides a basic widget:
local.HomePage = instance.Widget.extend({
start: function() {
console.log("pet store home page loaded");
},
});
It extends Widget()
and overrides the standard method
start()
, which — much like the previous MyClass
— does little for now.
This line at the end of the file:
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
registers our basic widget as a client action. Client actions will be explained later, for now this is just what allows our widget to be called and displayed when we select the menu.
Warning
because the widget will be called from outside our module, the web client needs its "fully qualified" name, not the local version.
Display Content
Widgets have a number of methods and features, but the basics are simple:
- set up a widget
- format the widget's data
- display the widget
The HomePage
widget already has a start()
method. That method is part of the normal widget lifecycle and automatically
called once the widget is inserted in the page. We can use it to display some
content.
All widgets have a $el
which represents the
section of page they're in charge of (as a jQuery object). Widget content
should be inserted there. By default, $el
is an
empty <div>
element.
A <div>
element is usually invisible to the user if it has no content (or
without specific styles giving it a size) which is why nothing is displayed
on the page when HomePage
is launched.
Let's add some content to the widget's root element, using jQuery:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append("<div>Hello dear Odoo user!</div>");
},
});
That message will now appear when you open
Note
to refresh the javascript code loaded in Odoo Web, you will need to reload the page. There is no need to restart the Odoo server.
The HomePage
widget is used by Odoo Web and managed automatically.
To learn how to use a widget "from scratch" let's create a new one:
local.GreetingsWidget = instance.Widget.extend({
start: function() {
this.$el.append("<div>We are so happy to see you again in this menu!</div>");
},
});
We can now add our GreetingsWidget
to the HomePage
by using the
GreetingsWidget
's appendTo()
method:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append("<div>Hello dear Odoo user!</div>");
var greeting = new local.GreetingsWidget(this);
return greeting.appendTo(this.$el);
},
});
HomePage
first adds its own content to its DOM rootHomePage
then instantiatesGreetingsWidget
- Finally it tells
GreetingsWidget
where to insert itself, delegating part of its$el
to theGreetingsWidget
.
When the appendTo()
method is called, it asks the
widget to insert itself at the specified position and to display its content.
The start()
method will be called during the call
to appendTo()
.
To see what happens under the displayed interface, we will use the browser's
DOM Explorer. But first let's alter our widgets slightly so we can more easily
find where they are, by adding a class to their root elements
:
local.HomePage = instance.Widget.extend({
className: 'oe_petstore_homepage',
...
});
local.GreetingsWidget = instance.Widget.extend({
className: 'oe_petstore_greetings',
...
});
If you can find the relevant section of the DOM (right-click on the text then Inspect Element), it should look like this:
<div class="oe_petstore_homepage">
<div>Hello dear Odoo user!</div>
<div class="oe_petstore_greetings">
<div>We are so happy to see you again in this menu!</div>
</div>
</div>
Which clearly shows the two <div>
elements automatically created by
Widget()
, because we added some classes on them.
We can also see the two message-holding divs we added ourselves
Finally, note the <div class="oe_petstore_greetings">
element which
represents the GreetingsWidget
instance is inside the
<div class="oe_petstore_homepage">
which represents the HomePage
instance, since we appended
Widget Parents and Children
In the previous part, we instantiated a widget using this syntax:
new local.GreetingsWidget(this);
The first argument is this
, which in that case was a HomePage
instance. This tells the widget being created which other widget is its
parent.
As we've seen, widgets are usually inserted in the DOM by another widget and inside that other widget's root element. This means most widgets are "part" of another widget, and exist on behalf of it. We call the container the parent, and the contained widget the child.
Due to multiple technical and conceptual reasons, it is necessary for a widget to know who is its parent and who are its children.
getParent()
can be used to get the parent of a widget:
local.GreetingsWidget = instance.Widget.extend({ start: function() { console.log(this.getParent().$el ); // will print "div.oe_petstore_homepage" in the console }, });
getChildren()
can be used to get a list of its children:
local.HomePage = instance.Widget.extend({ start: function() { var greeting = new local.GreetingsWidget(this); greeting.appendTo(this.$el); console.log(this.getChildren()[0].$el); // will print "div.oe_petstore_greetings" in the console }, });
When overriding the init()
method of a widget it is
of the utmost importance to pass the parent to the this._super()
call,
otherwise the relation will not be set up correctly:
local.GreetingsWidget = instance.Widget.extend({
init: function(parent, name) {
this._super(parent);
this.name = name;
},
});
Finally, if a widget does not have a parent (e.g. because it's the root
widget of the application), null
can be provided as parent:
new local.GreetingsWidget(null);
Destroying Widgets
If you can display content to your users, you should also be able to erase
it. This is done via the destroy()
method:
greeting.destroy();
When a widget is destroyed it will first call
destroy()
on all its children. Then it erases itself
from the DOM. If you have set up permanent structures in
init()
or start()
which
must be explicitly cleaned up (because the garbage collector will not handle
them), you can override destroy()
.
Danger
when overriding destroy()
, _super()
must always be called otherwise the widget and its children are not
correctly cleaned up leaving possible memory leaks and "phantom events",
even if no error is displayed
The QWeb Template Engine
In the previous section we added content to our widgets by directly manipulating (and adding to) their DOM:
this.$el.append("<div>Hello dear Odoo user!</div>");
This allows generating and displaying any type of content, but gets unwieldy when generating significant amounts of DOM (lots of duplication, quoting issues, ...)
As many other environments, Odoo's solution is to use a template engine. Odoo's template engine is called QWeb.
QWeb is an XML-based templating language, similar to Genshi, Thymeleaf or Facelets. It has the following characteristics:
- It's implemented fully in JavaScript and rendered in the browser
- Each template file (XML files) contains multiple templates
- It has special support in Odoo Web's
Widget()
, though it can be used outside of Odoo's web client (and it's possible to useWidget()
without relying on QWeb)
Note
The rationale behind using QWeb instead of existing javascript template engines is the extensibility of pre-existing (third-party) templates, much like Odoo views.
Most javascript template engines are text-based which precludes easy structural extensibility where an XML-based templating engine can be generically altered using e.g. XPath or CSS and a tree-alteration DSL (or even just XSLT). This flexibility and extensibility is a core characteristic of Odoo, and losing it was considered unacceptable.
Using QWeb
First let's define a simple QWeb template in the almost-empty
oepetstore/static/src/xml/petstore.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="HomePageTemplate">
<div style="background-color: red;">This is some simple HTML</div>
</t>
</templates>
Now we can use this template inside of the HomePage
widget. Using the
QWeb
loader variable defined at the top of the page, we can call to the
template defined in the XML file:
local.HomePage = instance.Widget.extend({
start: function() {
this.$el.append(QWeb.render("HomePageTemplate"));
},
});
QWeb.render()
looks for the specified template, renders it to a string
and returns the result.
However, because Widget()
has special integration for QWeb
the template can be set directly on the widget via its
template
attribute:
local.HomePage = instance.Widget.extend({
template: "HomePageTemplate",
start: function() {
...
},
});
Although the result looks similar, there are two differences between these usages:
- with the second version, the template is rendered right before
start()
is called - in the first version the template's content is added to the widget's root element, whereas in the second version the template's root element is directly set as the widget's root element. Which is why the "greetings" sub-widget also gets a red background
Warning
templates should have a single non-t
root element, especially if
they're set as a widget's template
. If there are
multiple "root elements", results are undefined (usually only the first
root element will be used and the others will be ignored)
QWeb Context
QWeb templates can be given data and can contain basic display logic.
For explicit calls to QWeb.render()
, the template data is passed as
second parameter:
QWeb.render("HomePageTemplate", {name: "Klaus"});
with the template modified to:
<t t-name="HomePageTemplate">
<div>Hello <t t-esc="name"/></div>
</t>
will result in:
<div>Hello Klaus</div>
When using Widget()
's integration it is not possible to
provide additional data to the template. The template will be given a single
widget
context variable, referencing the widget being rendered right
before start()
is called (the widget's state will
essentially be that set up by init()
):
<t t-name="HomePageTemplate">
<div>Hello <t t-esc="widget.name"/></div>
</t>
local.HomePage = instance.Widget.extend({
template: "HomePageTemplate",
init: function(parent) {
this._super(parent);
this.name = "Mordecai";
},
start: function() {
},
});
Result:
<div>Hello Mordecai</div>
Template Declaration
We've seen how to render QWeb templates, let's now see the syntax of the templates themselves.
A QWeb template is composed of regular XML mixed with QWeb directives. A
QWeb directive is declared with XML attributes starting with t-
.
The most basic directive is t-name
, used to declare new templates in
a template file:
<templates>
<t t-name="HomePageTemplate">
<div>This is some simple HTML</div>
</t>
</templates>
t-name
takes the name of the template being defined, and declares that
it can be called using QWeb.render()
. It can only be used at the
top-level of a template file.
Escaping
The t-esc
directive can be used to output text:
<div>Hello <t t-esc="name"/></div>
It takes a Javascript expression which is evaluated, the result of the expression is then HTML-escaped and inserted in the document. Since it's an expression it's possible to provide just a variable name as above, or a more complex expression like a computation:
<div><t t-esc="3+5"/></div>
or method calls:
<div><t t-esc="name.toUpperCase()"/></div>
Outputting HTML
To inject HTML in the page being rendered, use t-raw
. Like t-esc
it
takes an arbitrary Javascript expression as parameter, but it does not
perform an HTML-escape step.
<div><t t-raw="name.link(user_account)"/></div>
Danger
t-raw
must not be used on any data which may contain non-escaped
user-provided content as this leads to cross-site scripting
vulnerabilities
Conditionals
QWeb can have conditional blocks using t-if
. The directive takes an
arbitrary expression, if the expression is falsy (false
, null
, 0
or an empty string) the whole block is suppressed, otherwise it is displayed.
<div>
<t t-if="true == true">
true is true
</t>
<t t-if="true == false">
true is not true
</t>
</div>
Note
QWeb doesn't have an "else" structure, use a second t-if
with the
original condition inverted. You may want to store the condition in a
local variable if it's a complex or expensive expression.
Iteration
To iterate on a list, use t-foreach
and t-as
. t-foreach
takes an
expression returning a list to iterate on t-as
takes a variable name to
bind to each item during iteration.
<div>
<t t-foreach="names" t-as="name">
<div>
Hello <t t-esc="name"/>
</div>
</t>
</div>
Note
t-foreach
can also be used with numbers and objects
(dictionaries)
Defining attributes
QWeb provides two related directives to define computed attributes:
t-att-name
and t-attf-name
. In either case, name is the
name of the attribute to create (e.g. t-att-id
defines the attribute
id
after rendering).
t-att-
takes a javascript expression whose result is set as the
attribute's value, it is most useful if all of the attribute's value is
computed:
<div>
Input your name:
<input type="text" t-att-value="defaultName"/>
</div>
t-attf-
takes a format string. A format string is literal text with
interpolation blocks inside, an interpolation block is a javascript
expression between {{
and }}
, which will be replaced by the result
of the expression. It is most useful for attributes which are partially
literal and partially computed such as a class:
<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
insert content here
</div>
Calling other templates
Templates can be split into sub-templates (for simplicity, maintainability, reusability or to avoid excessive markup nesting).
This is done using the t-call
directive, which takes the name of the
template to render:
<t t-name="A">
<div class="i-am-a">
<t t-call="B"/>
</div>
</t>
<t t-name="B">
<div class="i-am-b"/>
</t>
rendering the A
template will result in:
<div class="i-am-a">
<div class="i-am-b"/>
</div>
Sub-templates inherit the rendering context of their caller.
To Learn More About QWeb
For a QWeb reference, see QWeb.
Exercise
Exercise
Usage of QWeb in Widgets
Create a widget whose constructor takes two parameters aside from
parent
: product_names
and color
.
product_names
should an array of strings, each one the name of a productcolor
is a string containing a color in CSS color format (ie:#000000
for black).
The widget should display the given product names one under the other,
each one in a separate box with a background color with the value of
color
and a border. You should use QWeb to render the HTML. Any
necessary CSS should be in oepetstore/static/src/css/petstore.css
.
Use the widget in HomePage
with half a dozen products.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.HomePage = instance.Widget.extend({
start: function() {
var products = new local.ProductsWidget(
this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00");
products.appendTo(this.$el);
},
});
local.ProductsWidget = instance.Widget.extend({
template: "ProductsWidget",
init: function(parent, products, color) {
this._super(parent);
this.products = products;
this.color = color;
},
});
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="ProductsWidget">
<div>
<t t-foreach="widget.products" t-as="product">
<span class="oe_products_item"
t-attf-style="background-color: {{ widget.color }};">
<t t-esc="product"/>
</span>
<br/>
</t>
</div>
</t>
</templates>
.oe_products_item {
display: inline-block;
padding: 3px;
margin: 5px;
border: 1px solid black;
border-radius: 3px;
}
Widget Helpers
Widget
's jQuery Selector
Selecting DOM elements within a widget can be performed by calling the
find()
method on the widget's DOM root:
this.$el.find("input.my_input")...
But because it's a common operation, Widget()
provides an
equivalent shortcut through the $()
method:
local.MyWidget = instance.Widget.extend({
start: function() {
this.$("input.my_input")...
},
});
Warning
The global jQuery function $()
should never be used unless it is
absolutely necessary: selection on a widget's root are scoped to the
widget and local to it, but selections with $()
are global to the
page/application and may match parts of other widgets and views, leading
to odd or dangerous side-effects. Since a widget should generally act
only on the DOM section it owns, there is no cause for global selection.
Easier DOM Events Binding
We have previously bound DOM events using normal jQuery event handlers (e.g.
.click()
or .change()
) on widget elements:
local.MyWidget = instance.Widget.extend({
start: function() {
var self = this;
this.$(".my_button").click(function() {
self.button_clicked();
});
},
button_clicked: function() {
..
},
});
While this works it has a few issues:
- it is rather verbose
- it does not support replacing the widget's root element at runtime as
the binding is only performed when
start()
is run (during widget initialization) - it requires dealing with
this
-binding issues
Widgets thus provide a shortcut to DOM event binding via
events
:
local.MyWidget = instance.Widget.extend({
events: {
"click .my_button": "button_clicked",
},
button_clicked: function() {
..
}
});
events
is an object (mapping) of an event to the
function or method to call when the event is triggered:
- the key is an event name, possibly refined with a CSS selector in which
case only if the event happens on a selected sub-element will the function
or method run:
click
will handle all clicks within the widget, butclick .my_button
will only handle clicks in elements bearing themy_button
class the value is the action to perform when the event is triggered
It can be either a function:
events: { 'click': function (e) { /* code here */ } }
or the name of a method on the object (see example above).
In either case, the
this
is the widget instance and the handler is given a single parameter, the jQuery event object for the event.
Widget Events and Properties
Events
Widgets provide an event system (separate from the DOM/jQuery event system described above): a widget can fire events on itself, and other widgets (or itself) can bind themselves and listen for these events:
local.ConfirmWidget = instance.Widget.extend({
events: {
'click button.ok_button': function () {
this.trigger('user_chose', true);
},
'click button.cancel_button': function () {
this.trigger('user_chose', false);
}
},
start: function() {
this.$el.append("<div>Are you sure you want to perform this action?</div>" +
"<button class='ok_button'>Ok</button>" +
"<button class='cancel_button'>Cancel</button>");
},
});
This widget acts as a facade, transforming user input (through DOM events) into a documentable internal event to which parent widgets can bind themselves.
trigger()
takes the name of the event to trigger as
its first (mandatory) argument, any further arguments are treated as event
data and passed directly to listeners.
We can then set up a parent event instantiating our generic widget and
listening to the user_chose
event using on()
:
local.HomePage = instance.Widget.extend({
start: function() {
var widget = new local.ConfirmWidget(this);
widget.on("user_chose", this, this.user_chose);
widget.appendTo(this.$el);
},
user_chose: function(confirm) {
if (confirm) {
console.log("The user agreed to continue");
} else {
console.log("The user refused to continue");
}
},
});
on()
binds a function to be called when the
event identified by event_name
is. The func
argument is the
function to call and object
is the object to which that function is
related if it is a method. The bound function will be called with the
additional arguments of trigger()
if it has
any. Example:
start: function() {
var widget = ...
widget.on("my_event", this, this.my_event_triggered);
widget.trigger("my_event", 1, 2, 3);
},
my_event_triggered: function(a, b, c) {
console.log(a, b, c);
// will print "1 2 3"
}
Note
Triggering events on an other widget is generally a bad idea. The main
exception to that rule is odoo.web.bus
which exists specifically
to broadcasts evens in which any widget could be interested throughout
the Odoo web application.
Properties
Properties are very similar to normal object attributes in that they allow storing data on a widget instance, however they have the additional feature that they trigger events when set:
start: function() {
this.widget = ...
this.widget.on("change:name", this, this.name_changed);
this.widget.set("name", "Nicolas");
},
name_changed: function() {
console.log("The new value of the property 'name' is", this.widget.get("name"));
}
set()
sets the value of a property and triggerschange:propname
(where propname is the property name passed as first parameter toset()
) andchange
get()
retrieves the value of a property.
Exercise
Exercise
Widget Properties and Events
Create a widget ColorInputWidget
that will display 3 <input
type="text">
. Each of these <input>
is dedicated to type a
hexadecimal number from 00 to FF. When any of these <input>
is
modified by the user the widget must query the content of the three
<input>
, concatenate their values to have a complete CSS color code
(ie: #00FF00
) and put the result in a property named color
. Please
note the jQuery change()
event that you can bind on any HTML
<input>
element and the val()
method that can query the current
value of that <input>
could be useful to you for this exercise.
Then, modify the HomePage
widget to instantiate ColorInputWidget
and display it. The HomePage
widget should also display an empty
rectangle. That rectangle must always, at any moment, have the same
background color as the color in the color
property of the
ColorInputWidget
instance.
Use QWeb to generate all HTML.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.ColorInputWidget = instance.Widget.extend({
template: "ColorInputWidget",
events: {
'change input': 'input_changed'
},
start: function() {
this.input_changed();
return this._super();
},
input_changed: function() {
var color = [
"#",
this.$(".oe_color_red").val(),
this.$(".oe_color_green").val(),
this.$(".oe_color_blue").val()
].join('');
this.set("color", color);
},
});
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function() {
this.colorInput = new local.ColorInputWidget(this);
this.colorInput.on("change:color", this, this.color_changed);
return this.colorInput.appendTo(this.$el);
},
color_changed: function() {
this.$(".oe_color_div").css("background-color", this.colorInput.get("color"));
},
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="ColorInputWidget">
<div>
Red: <input type="text" class="oe_color_red" value="00"></input><br />
Green: <input type="text" class="oe_color_green" value="00"></input><br />
Blue: <input type="text" class="oe_color_blue" value="00"></input><br />
</div>
</t>
<t t-name="HomePage">
<div>
<div class="oe_color_div"></div>
</div>
</t>
</templates>
.oe_color_div {
width: 100px;
height: 100px;
margin: 10px;
}
Modify existing widgets and classes
The class system of the Odoo web framework allows direct modification of
existing classes using the include()
method:
var TestClass = instance.web.Class.extend({
testMethod: function() {
return "hello";
},
});
TestClass.include({
testMethod: function() {
return this._super() + " world";
},
});
console.log(new TestClass().testMethod());
// will print "hello world"
This system is similar to the inheritance mechanism, except it will alter the target class in-place instead of creating a new class.
In that case, this._super()
will call the original implementation of a
method being replaced/redefined. If the class already had sub-classes, all
calls to this._super()
in sub-classes will call the new implementations
defined in the call to include()
. This will also work
if some instances of the class (or of any of its sub-classes) were created
prior to the call to include()
.
Translations
The process to translate text in Python and JavaScript code is very
similar. You could have noticed these lines at the beginning of the
petstore.js
file:
var _t = instance.web._t,
_lt = instance.web._lt;
These lines are simply used to import the translation functions in the current JavaScript module. They are used thus:
this.$el.text(_t("Hello user!"));
In Odoo, translations files are automatically generated by scanning the source
code. All piece of code that calls a certain function are detected and their
content is added to a translation file that will then be sent to the
translators. In Python, the function is _()
. In JavaScript the function is
_t()
(and also _lt()
).
_t()
will return the translation defined for the text it is given. If no
translation is defined for that text, it will return the original text as-is.
Note
To inject user-provided values in translatable strings, it is recommended to use _.str.sprintf with named arguments after the translation:
this.$el.text(_.str.sprintf(
_t("Hello, %(user)s!"), {
user: "Ed"
}));
This makes translatable strings more readable to translators, and gives them more flexibility to reorder or ignore parameters.
_lt()
("lazy translate") is similar but somewhat more
complex: instead of translating its parameter immediately, it returns
an object which, when converted to a string, will perform the translation.
It is used to define translatable terms before the translations system is initialized, for class attributes for instance (as modules are loaded before the user's language is configured and translations are downloaded).
Communication with the Odoo Server
通讯模型
使用Odoo进行的大多数操作都涉及与实现业务问题的模型进行通信,然后这些模型(可能)与某些存储引擎(通常为PostgreSQL)进行交互。
尽管jQuery提供了用于网络交互的$。ajax函数,但是与Odoo进行通信需要其他元数据,这些元数据的设置在每次调用之前都是冗长且容易出错的。 结果,Odoo Web提供了更高级别的通信原语。
为了证明这一点,文件petstore.py
已经包含一个带有示例方法的小型模型:
class message_of_the_day(models.Model):
_name = "oepetstore.message_of_the_day"
@api.model
def my_method(self):
return {"hello": "world"}
message = fields.Text(),
color = fields.Char(size=20),
这将声明一个具有两个字段的模型,以及一个返回文字字典的方法my_method()
。
这是一个示例小部件,它调用my_method()
并显示结果:
local.HomePage = instance.Widget.extend({
start: function() {
var self = this;
var model = new instance.web.Model("oepetstore.message_of_the_day");
model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) {
self.$el.append("<div>Hello " + result["hello"] + "</div>");
// will show "Hello world" to the user
});
},
});
用于调用Odoo模型的类是odoo.Model()
。 以Odoo模型的名称作为第一个参数(在此为oepetstore.message_of_the_day
)进行实例化。
call()
可用于调用Odoo模型的任何(公共)方法。 它采用以下位置参数:
name
- 调用方法的名称,
my_method
此处 args
位置参数的数组以提供给该方法。 由于该示例没有提供位置参数,因此未提供
args
参数。这是另一个带有位置参数的示例:
@api.model def my_method2(self, a, b, c): ...
model.call("my_method", [1, 2, 3], ... // with this a=1, b=2 and c=3
kwargs
要传递的关键字参数的映射。 该示例提供了单个命名参数
context
。@api.model def my_method2(self, a, b, c): ...
model.call("my_method", [], {a: 1, b: 2, c: 3, ... // with this a=1, b=2 and c=3
call()
返回以模型方法返回的值作为第一个参数的延迟解析。
复合上下文
上一节使用了context
参数,该参数未在方法调用中说明:
model.call("my_method", {context: new instance.web.CompoundContext()})
上下文就像Web客户端在调用方法时始终将其赋予服务器的“魔术”参数。 上下文是包含多个键的字典。 最重要的键之一是用户的语言,服务器使用它来翻译应用程序的所有消息。 另一个是用户的时区,如果不同国家的人使用Odoo,则该时间区可正确计算日期和时间。
在所有方法中,argument
都是必需的,否则可能发生不好的事情(例如应用程序未正确翻译)。 因此,当调用模型的方法时,应始终提供该参数。 解决该问题的方法是使用odoo.web.CompoundContext()
。
CompoundContext()
是用于将用户上下文(带有语言,时区等)传递给服务器以及向上下文添加新键的类(某些模型的方法使用任意键添加到上下文中)。 通过为其构造函数提供任意数量的词典或其他CompoundContext()
实例来创建它。 它将合并所有这些上下文,然后再将它们发送到服务器。
model.call("my_method", {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
@api.model
def my_method(self):
print self.env.context
// will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}
您可以在参数context
中看到该词典,其中包含一些与Odoo中当前用户的配置有关的键,以及在实例化时添加的
。new_key
键。 CompoundContext()
查询
虽然call()
足以与Odoo模型进行任何交互,但Odoo Web提供了一个帮助您更简单,更清晰地查询模型(根据各种条件获取记录):query()
是search()
和:read()
的常见组合的快捷方式。 它提供了更清晰的语法来搜索和读取模型:
model.query(['name', 'login', 'user_email', 'signature'])
.filter([['active', '=', true], ['company_id', '=', main_company]])
.limit(15)
.all().then(function (users) {
//处理用户记录
});
与:
model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15})
.then(function (ids) {
return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]);
})
.then(function (users) {
// do work with users records
});
query()
将字段的可选列表作为参数(如果未提供命令行,则将提取模型的所有字段)。 它返回odoo.web.Query()
,可以在执行之前进一步自定义Query()
表示正在构建的查询。 这是不可变的,用于自定义查询的方法实际上会返回修改后的副本,因此可以同时使用原始版本和新版本。 有关自定义选项,请参见Query()
。
根据需要设置查询后,只需调用all()
即可执行查询并返回延期查询的结果。 结果与read()
相同,后者是字典数组,其中每个字典都是请求的记录,每个请求的字典都是键。
练习
Exercise
Message of the Day
Create a MessageOfTheDay
widget displaying the last record of the
oepetstore.message_of_the_day
model. The widget should fetch its
record as soon as it is displayed.
Display the widget in the Pet Store home page.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function() {
return new local.MessageOfTheDay(this).appendTo(this.$el);
},
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
local.MessageOfTheDay = instance.Widget.extend({
template: "MessageOfTheDay",
start: function() {
var self = this;
return new instance.web.Model("oepetstore.message_of_the_day")
.query(["message"])
.order_by('-create_date', '-id')
.first()
.then(function(result) {
self.$(".oe_mywidget_message_of_the_day").text(result.message);
});
},
});
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="HomePage">
<div class="oe_petstore_homepage">
</div>
</t>
<t t-name="MessageOfTheDay">
<div class="oe_petstore_motd">
<p class="oe_mywidget_message_of_the_day"></p>
</div>
</t>
</templates>
.oe_petstore_motd {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
Exercise
Pet Toys List
Create a PetToysList
widget displaying 5 toys (using their name and
their images).
The pet toys are not stored in a new model, instead they're stored in
product.product
using a special category Pet Toys. You can see the
pre-generated toys and add new ones by going to
.You will probably
need to explore product.product
to create the right domain to
select just pet toys.
In Odoo, images are generally stored in regular fields encoded as
base64, HTML supports displaying images straight from base64 with
<img src="data:mime_type;base64,base64_image_data"/>
The PetToysList
widget should be displayed on the home page on the
right of the MessageOfTheDay
widget. You will need to make some layout
with CSS to achieve this.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function () {
return $.when(
new local.PetToysList(this).appendTo(this.$('.oe_petstore_homepage_left')),
new local.MessageOfTheDay(this).appendTo(this.$('.oe_petstore_homepage_right'))
);
}
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
local.MessageOfTheDay = instance.Widget.extend({
template: 'MessageOfTheDay',
start: function () {
var self = this;
return new instance.web.Model('oepetstore.message_of_the_day')
.query(["message"])
.order_by('-create_date', '-id')
.first()
.then(function (result) {
self.$(".oe_mywidget_message_of_the_day").text(result.message);
});
}
});
local.PetToysList = instance.Widget.extend({
template: 'PetToysList',
start: function () {
var self = this;
return new instance.web.Model('product.product')
.query(['name', 'image'])
.filter([['categ_id.name', '=', "Pet Toys"]])
.limit(5)
.all()
.then(function (results) {
_(results).each(function (item) {
self.$el.append(QWeb.render('PetToy', {item: item}));
});
});
}
});
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="HomePage">
<div class="oe_petstore_homepage">
<div class="oe_petstore_homepage_left"></div>
<div class="oe_petstore_homepage_right"></div>
</div>
</t>
<t t-name="MessageOfTheDay">
<div class="oe_petstore_motd">
<p class="oe_mywidget_message_of_the_day"></p>
</div>
</t>
<t t-name="PetToysList">
<div class="oe_petstore_pettoyslist">
</div>
</t>
<t t-name="PetToy">
<div class="oe_petstore_pettoy">
<p><t t-esc="item.name"/></p>
<p><img t-att-src="'data:image/jpg;base64,'+item.image"/></p>
</div>
</t>
</templates>
.oe_petstore_homepage {
display: table;
}
.oe_petstore_homepage_left {
display: table-cell;
width : 300px;
}
.oe_petstore_homepage_right {
display: table-cell;
width : 300px;
}
.oe_petstore_motd {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
.oe_petstore_pettoyslist {
padding: 5px;
}
.oe_petstore_pettoy {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
现有的Web组件
动作管理程序
在Odoo中,许多操作都是从action开始的:打开菜单项(进入视图),打印报告,...
动作是描述客户端应如何响应内容激活的数据。 动作可以存储(并通过模型读取),也可以动态生成(通过javascript代码在客户端本地生成,或通过模型的方法远程生成)。
在Odoo Web中,负责处理和响应这些动作的组件是动作管理器。
使用动作管理器
通过创建描述正确类型的动作的字典,并使用它调用动作管理器实例,可以从javascript代码中显式调用动作管理器。
do_action()
是Widget()
的快捷方式,它查找“当前”动作管理器并执行该动作:
instance.web.TestWidget = instance.Widget.extend({
dispatch_to_new_action: function() {
this.do_action({
type: 'ir.actions.act_window',
res_model: "product.product",
res_id: 1,
views: [[false, 'form']],
target: 'current',
context: {},
});
},
});
最常见的操作类型
是ir.actions.act_window
,它为模型提供视图(以各种方式显示模型),其最常见的属性是:
res_model
- 在视图中显示的模型
res_id
(optional)- 对于表单视图,是
res_model
中的预选记录 views
- 列出通过操作可用的视图。
[view_id,view_type]
,view_id
的列表可以是正确类型的视图的数据库标识符,也可以是false
来使用默认情况下查看指定类型。 视图类型不能多次出现。 默认情况下,该操作将打开列表的第一个视图。 target
current
(默认值)(用操作替换Web客户端的“内容”部分),或new
在对话框中打开操作。context
- 要在操作中使用的其他上下文数据。
Exercise
跳转到产品
修改PetToysList
组件,以便单击玩具可以用玩具的窗体视图替换主页。
local.PetToysList = instance.Widget.extend({
template: 'PetToysList',
events: {
'click .oe_petstore_pettoy': 'selected_item',
},
start: function () {
var self = this;
return new instance.web.Model('product.product')
.query(['name', 'image'])
.filter([['categ_id.name', '=', "Pet Toys"]])
.limit(5)
.all()
.then(function (results) {
_(results).each(function (item) {
self.$el.append(QWeb.render('PetToy', {item: item}));
});
});
},
selected_item: function (event) {
this.do_action({
type: 'ir.actions.act_window',
res_model: 'product.product',
res_id: $(event.currentTarget).data('id'),
views: [[false, 'form']],
});
},
});
<t t-name="PetToy">
<div class="oe_petstore_pettoy" t-att-data-id="item.id">
<p><t t-esc="item.name"/></p>
<p><img t-attf-src="data:image/jpg;base64,{{item.image}}"/></p>
</div>
</t>
客户动作
在本指南中,我们使用了一个简单的Home
小部件,当选择正确的菜单项时,Web客户端会自动启动。 但是Odoo网站如何知道如何启动此小部件? 因为该小部件已注册为客户端操作。
客户端动作(顾名思义)是一种在Odoo Web的javascript中几乎完全在客户端中定义的动作类型。 服务器仅发送一个动作标签(任意名称),并可选地添加一些参数,但除此之外,所有均由自定义客户端代码处理。
我们的小部件通过以下方式注册为客户端操作的处理程序:
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
instance.web.client_actions
是Registry()
,其中动作管理器在需要执行客户端动作处理程序时会查找它。 add()
的第一个参数是客户端操作的名称(标签),第二个参数是从Odoo Web客户端根目录到小部件的路径。
当必须执行客户端操作时,操作管理器会在注册表中查找其标记,遍历指定路径并显示最终找到的小部件。
Note
客户动作处理程序也可以是常规函数,在这种情况下,它将被调用,并且其结果(如果有)将被解释为要执行的下一个动作。
在服务器端,我们仅定义了ir.actions.client
操作:
<record id="action_home_page" model="ir.actions.client">
<field name="tag">petstore.homepage</field>
</record>
和一个打开动作的菜单:
<menuitem id="home_page_petstore_menu" parent="petstore_menu"
name="Home Page" action="action_home_page"/>
视图架构
Odoo Web的许多有用性(和复杂性)都存在于视图中。 每种视图类型都是在客户端中显示模型的一种方式。
视图管理器
当ActionManager
实例收到ir.actions.act_window
类型的动作时,它将视图本身的同步和处理委托给view manager ,然后将根据原始操作的要求设置一个或多个视图:
Views视图
大多数Odoo视图是通过odoo.web.View()
的子类实现的,该子类提供了一些用于处理事件和显示模型信息的通用基本结构。
搜索视图在主要的Odoo框架中被视为视图类型,但是由Web客户端单独处理(因为它是永久性的固定装置,可以与其他视图交互,而常规视图则无法做到) 。
视图负责加载自己的描述XML(使用fields_view_get
)和所需的任何其他数据源。 为此,为视图提供了一个可选的视图标识符,该标识符设置为view_id
属性。
视图还提供了一个DataSet()
实例,该实例包含最必要的模型信息(模型名称以及可能的各种记录ID)。
视图可能还想通过覆盖do_search()
并根据需要更新其DataSet()
来处理搜索查询。
表单视图字段
通常需要扩展Web窗体视图以添加新的字段显示方式。
All built-in fields have a default display implementation, a new
form widget may be necessary to correctly interact with a new field type
(e.g. a GIS field) or to provide new representations and ways to
interact with existing field types (e.g. validate
Char
fields which should contain email addresses
and display them as email links).
To explicitly specify which form widget should be used to display a field,
simply use the widget
attribute in the view's XML description:
<field name="contact_mail" widget="email"/>
Note
- the same widget is used in both "view" (read-only) and "edition" modes of a form view, it's not possible to use a widget in one and an other widget in the other
- and a given field (name) can not be used multiple times in the same form
- a widget may ignore the current mode of the form view and remain the same in both view and edition
Fields are instantiated by the form view after it has read its XML description
and constructed the corresponding HTML representing that description. After
that, the form view will communicate with the field objects using some
methods. These methods are defined by the FieldInterface
interface. Almost all fields inherit the AbstractField
abstract
class. That class defines some default mechanisms that need to be implemented
by most fields.
Here are some of the responsibilities of a field class:
- The field class must display and allow the user to edit the value of the field.
It must correctly implement the 3 field attributes available in all fields of Odoo. The
AbstractField
class already implements an algorithm that dynamically calculates the value of these attributes (they can change at any moment because their value change according to the value of other fields). Their values are stored in Widget Properties (the widget properties were explained earlier in this guide). It is the responsibility of each field class to check these widget properties and dynamically adapt depending of their values. Here is a description of each of these attributes:required
: The field must have a value before saving. Ifrequired
istrue
and the field doesn't have a value, the methodis_valid()
of the field must returnfalse
.invisible
: When this istrue
, the field must be invisible. TheAbstractField
class already has a basic implementation of this behavior that fits most fields.readonly
: Whentrue
, the field must not be editable by the user. Most fields in Odoo have a completely different behavior depending on the value ofreadonly
. As example, theFieldChar
displays an HTML<input>
when it is editable and simply displays the text when it is read-only. This also means it has much more code it would need to implement only one behavior, but this is necessary to ensure a good user experience.
- Fields have two methods,
set_value()
andget_value()
, which are called by the form view to give it the value to display and get back the new value entered by the user. These methods must be able to handle the value as given by the Odoo server when aread()
is performed on a model and give back a valid value for awrite()
. Remember that the JavaScript/Python data types used to represent the values given byread()
and given towrite()
is not necessarily the same in Odoo. As example, when you read a many2one, it is always a tuple whose first value is the id of the pointed record and the second one is the name get (ie:(15, "Agrolait")
). But when you write a many2one it must be a single integer, not a tuple anymore.AbstractField
has a default implementation of these methods that works well for simple data type and set a widget property namedvalue
.
Please note that, to better understand how to implement fields, you are
strongly encouraged to look at the definition of the FieldInterface
interface and the AbstractField
class directly in the code of the Odoo web
client.
Creating a New Type of Field
In this part we will explain how to create a new type of field. The example
here will be to re-implement the FieldChar
class and progressively explain
each part.
Simple Read-Only Field
Here is a first implementation that will only display text. The user will not be able to modify the content of the field.
local.FieldChar2 = instance.web.form.AbstractField.extend({
init: function() {
this._super.apply(this, arguments);
this.set("value", "");
},
render_value: function() {
this.$el.text(this.get("value"));
},
});
instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
In this example, we declare a class named FieldChar2
inheriting from
AbstractField
. We also register this class in the registry
instance.web.form.widgets
under the key char2
. That will allow us to
use this new field in any form view by specifying widget="char2"
in the
<field/>
tag in the XML declaration of the view.
In this example, we define a single method: render_value()
. All it does is
display the widget property value
. Those are two tools defined by the
AbstractField
class. As explained before, the form view will call the
method set_value()
of the field to set the value to display. This method
already has a default implementation in AbstractField
which simply sets
the widget property value
. AbstractField
also watch the
change:value
event on itself and calls the render_value()
when it
occurs. So, render_value()
is a convenience method to implement in child
classes to perform some operation each time the value of the field changes.
In the init()
method, we also define the default value of the field if
none is specified by the form view (here we assume the default value of a
char
field should be an empty string).
Read-Write Field
Read-only fields, which only display content and don't allow the user to modify it can be useful, but most fields in Odoo also allow editing. This makes the field classes more complicated, mostly because fields are supposed to handle both editable and non-editable mode, those modes are often completely different (for design and usability purpose) and the fields must be able to switch between modes at any moment.
To know in which mode the current field should be, the AbstractField
class
sets a widget property named effective_readonly
. The field should watch
for changes in that widget property and display the correct mode
accordingly. Example:
local.FieldChar2 = instance.web.form.AbstractField.extend({
init: function() {
this._super.apply(this, arguments);
this.set("value", "");
},
start: function() {
this.on("change:effective_readonly", this, function() {
this.display_field();
this.render_value();
});
this.display_field();
return this._super();
},
display_field: function() {
var self = this;
this.$el.html(QWeb.render("FieldChar2", {widget: this}));
if (! this.get("effective_readonly")) {
this.$("input").change(function() {
self.internal_set_value(self.$("input").val());
});
}
},
render_value: function() {
if (this.get("effective_readonly")) {
this.$el.text(this.get("value"));
} else {
this.$("input").val(this.get("value"));
}
},
});
instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
<t t-name="FieldChar2">
<div class="oe_field_char2">
<t t-if="! widget.get('effective_readonly')">
<input type="text"></input>
</t>
</div>
</t>
In the start()
method (which is called immediately after a widget has been
appended to the DOM), we bind on the event change:effective_readonly
. That
allows us to redisplay the field each time the widget property
effective_readonly
changes. This event handler will call
display_field()
, which is also called directly in start()
. This
display_field()
was created specifically for this field, it's not a method
defined in AbstractField
or any other class. We can use this method
to display the content of the field depending on the current mode.
From now on the conception of this field is typical, except there is a
lot of verifications to know the state of the effective_readonly
property:
- In the QWeb template used to display the content of the widget, it displays
an
<input type="text" />
if we are in read-write mode and nothing in particular in read-only mode. - In the
display_field()
method, we have to bind on thechange
event of the<input type="text" />
to know when the user has changed the value. When it happens, we call theinternal_set_value()
method with the new value of the field. This is a convenience method provided by theAbstractField
class. That method will set a new value in thevalue
property but will not trigger a call torender_value()
(which is not necessary since the<input type="text" />
already contains the correct value). - In
render_value()
, we use a completely different code to display the value of the field depending if we are in read-only or in read-write mode.
Exercise
Create a Color Field
Create a FieldColor
class. The value of this field should be a string
containing a color code like those used in CSS (example: #FF0000
for
red). In read-only mode, this color field should display a little block
whose color corresponds to the value of the field. In read-write mode, you
should display an <input type="color" />
. That type of <input />
is an HTML5 component that doesn't work in all browsers but works well in
Google Chrome. So it's OK to use as an exercise.
You can use that widget in the form view of the message_of_the_day
model for its field named color
. As a bonus, you can change the
MessageOfTheDay
widget created in the previous part of this guide to
display the message of the day with the background color indicated in the
color
field.
local.FieldColor = instance.web.form.AbstractField.extend({
events: {
'change input': function (e) {
if (!this.get('effective_readonly')) {
this.internal_set_value($(e.currentTarget).val());
}
}
},
init: function() {
this._super.apply(this, arguments);
this.set("value", "");
},
start: function() {
this.on("change:effective_readonly", this, function() {
this.display_field();
this.render_value();
});
this.display_field();
return this._super();
},
display_field: function() {
this.$el.html(QWeb.render("FieldColor", {widget: this}));
},
render_value: function() {
if (this.get("effective_readonly")) {
this.$(".oe_field_color_content").css("background-color", this.get("value") || "#FFFFFF");
} else {
this.$("input").val(this.get("value") || "#FFFFFF");
}
},
});
instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor');
<t t-name="FieldColor">
<div class="oe_field_color">
<t t-if="widget.get('effective_readonly')">
<div class="oe_field_color_content" />
</t>
<t t-if="! widget.get('effective_readonly')">
<input type="color"></input>
</t>
</div>
</t>
.oe_field_color_content {
height: 20px;
width: 50px;
border: 1px solid black;
}
The Form View Custom Widgets
Form fields are used to edit a single field, and are intrinsically linked to a field. Because this may be limiting, it is also possible to create form widgets which are not so restricted and have less ties to a specific lifecycle.
Custom form widgets can be added to a form view through the widget
tag:
<widget type="xxx" />
This type of widget will simply be created by the form view during the
creation of the HTML according to the XML definition. They have properties in
common with the fields (like the effective_readonly
property) but they are
not assigned a precise field. And so they don't have methods like
get_value()
and set_value()
. They must inherit from the FormWidget
abstract class.
Form widgets can interact with form fields by listening for their changes and
fetching or altering their values. They can access form fields through
their field_manager
attribute:
local.WidgetMultiplication = instance.web.form.FormWidget.extend({
start: function() {
this._super();
this.field_manager.on("field_changed:integer_a", this, this.display_result);
this.field_manager.on("field_changed:integer_b", this, this.display_result);
this.display_result();
},
display_result: function() {
var result = this.field_manager.get_field_value("integer_a") *
this.field_manager.get_field_value("integer_b");
this.$el.text("a*b = " + result);
}
});
instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');
FormWidget
is generally the
FormView()
itself, but features used from it should
be limited to those defined by FieldManagerMixin()
,
the most useful being:
get_field_value(field_name)()
which returns the value of a field.set_values(values)()
sets multiple field values, takes a mapping of{field_name: value_to_set}
- An event
field_changed:field_name
is triggered any time the value of the field calledfield_name
is changed
Exercise
Show Coordinates on Google Map
Add two fields to product.product
storing a latitude and a longitude,
then create a new form widget to display the latitude and longitude of
a product's origin on a map
To display the map, use Google Map's embedding:
<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&ll=XXX,YYY&output=embed">
</iframe>
where XXX
should be replaced by the latitude and YYY
by the
longitude.
Display the two position fields and a map widget using them in a new notebook page of the product's form view.
local.WidgetCoordinates = instance.web.form.FormWidget.extend({
start: function() {
this._super();
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
this.display_map();
},
display_map: function() {
this.$el.html(QWeb.render("WidgetCoordinates", {
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
}));
}
});
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
<t t-name="WidgetCoordinates">
<iframe width="400" height="300"
t-attf-src="https://maps.google.com/?ie=UTF8&ll={{latitude}},{{longitude}}&output=embed">
</iframe>
</t>
Exercise
Get the Current Coordinate
Add a button resetting the product's coordinates to the location of the user, you can get these coordinates using the javascript geolocation API.
Now we would like to display an additional button to automatically set the coordinates to the location of the current user.
To get the coordinates of the user, an easy way is to use the geolocation JavaScript API. See the online documentation to know how to use it.
Please also note that the user should not be able to
click on that button when the form view is in read-only mode. So, this
custom widget should handle correctly the effective_readonly
property
just like any field. One way to do this would be to make the button
disappear when effective_readonly
is true.
local.WidgetCoordinates = instance.web.form.FormWidget.extend({
events: {
'click button': function () {
navigator.geolocation.getCurrentPosition(
this.proxy('received_position'));
}
},
start: function() {
var sup = this._super();
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
this.on("change:effective_readonly", this, this.display_map);
this.display_map();
return sup;
},
display_map: function() {
this.$el.html(QWeb.render("WidgetCoordinates", {
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
}));
this.$("button").toggle(! this.get("effective_readonly"));
},
received_position: function(obj) {
this.field_manager.set_values({
"provider_latitude": obj.coords.latitude,
"provider_longitude": obj.coords.longitude,
});
},
});
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
<t t-name="WidgetCoordinates">
<iframe width="400" height="300"
t-attf-src="https://maps.google.com/?ie=UTF8&ll={{latitude}},{{longitude}}&output=embed">
</iframe>
<button>Get My Current Coordinate</button>
</t>