第七章 表单视图
表单视图是odoo中最常见的视图类型之一, 我们在前面了解了它的XML定义, 本章我们将了解它在WC世界中的行为特性.
表单视图的结构
我们来看一个典型的表单视图的结构:
表单视图的初始化
表单视图有两种模式: 只读(Readonly)和编辑(Edit), 分别代表视图的只读和可编辑模式.通过控制栏的编辑按钮,可以对表单视图的两种模式进行切换.
视图在初始化的过程中,对模式进行了初始化:
init: function (viewInfo, params) {
var hasActionMenus = params.hasActionMenus;
this._super.apply(this, arguments);
var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
this.loadParams.type = 'record';
// this is kind of strange, but the param object is modified by
// AbstractView, so we only need to use its hasActionMenus value if it was
// not already present in the beginning of this method
if (hasActionMenus === undefined) {
hasActionMenus = params.hasActionMenus;
}
this.controllerParams.hasActionMenus = hasActionMenus;
this.controllerParams.disableAutofocus = params.disable_autofocus || this.arch.attrs.disable_autofocus;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.footerToButtons = params.footerToButtons;
var defaultButtons = 'default_buttons' in params ? params.default_buttons : true;
this.controllerParams.defaultButtons = defaultButtons;
this.controllerParams.mode = mode;
this.rendererParams.mode = mode;
this.rendererParams.isFromFormViewDialog = params.isFromFormViewDialog;
this.rendererParams.fieldIdsToNames = this.fieldsView.fieldIdsToNames;
}
首先, 表单视图的初始化是先调用了父类(抽象视图)的初始化方法, 抽象视图本身会进行一些参数的初始化过程, 然后表单视图在此基础上又增加了下面的几个属性:
- hasActionMenus: 标识表单视图是否含有动作菜单
- disableAutofocus: 是否取消自动聚焦
- toolbarActions: 是否显示工具栏动作按钮
- footerToButtons: 是否显示footer中的按钮
- defaultButtons: 是否显示默认按钮
- mode: 视图的模式
以上几个参数是Controller的配置参数,由视图在实例化Controller的时候传递.而下面三个则是传递给Renderder的参数.
ActiveActions
所谓ActiveActions是指视图中动作的总称, 常见的ActiveActions有一下几种:
- create
- edit
- delete
- depulicate
这几个动作分别对应视图中的创建和编辑, 更多按钮中的删除和复制.
如果想要控制视图中的创建和编辑按钮的显示,我们通常有以下几种方式来实现.
使用权限控制,将视图当前的对象权限设置为只读,便可以控制创建和编辑按钮的显示. 这种方式优点是简单快速,缺点是颗粒度太粗,并不能实现具体页面具体设置.
第二种方式就是使用表单视图的中create和edit属性, 只需要在特定的页面设置:
<form create="0" edit="0"> ... </form>
就可以实现对创建和编辑按钮的控制. 这种方式的有点是灵活,可以对不同的页面设置不同的权限. 缺点是只能静态控制而不能动态控制
第三种方式是通过动作来控制, 我们可以在窗口动作里的context上下文中传入create/edit来控制FORM表单的显示:
<record model="ir.actions.act_window" id="act_window_demo"> ... <field name="context">{'create':0}</field> </record>
使用动作中的flags属性来控制, 这种方式的适用场景是需要在按钮中返回一个动作, 利用该动作控制视图的显示:
{ "model":"ir.actions.act_window", ... "flags":{ "form":{ "default_buttons":False } } }
这种方式的缺点是, default_buttons的设置会同时隐藏掉创建和编辑按钮,并不能分开设置. 为了实现分开设置的目的,笔者为此专门开发了一个模块来实现此功能, 有需要的同学可以联系笔者进行购买.关于flags的更多内容,参加本部分第十七章.
设置表单视图中的列表视图长度
我们知道,对于X2many类型的字段,在表单视图中默认显示为一个内嵌于表单的列表部件。这个子列表部件与列表视图不同的点在于,子列表部件没有完整的控制按钮,不能进行筛选和分组,而且默认的分页大小为40。
如果需要修改某个特定字段的子列表视图长度,那么可以写成下面这种形式:
<field name="sales">
<tree limit=100>
...
</tee>
</field>
这样我们就可以更改默认的显示长度了。但是这样的问题在于,如果有多个页面要修改或者想要直接实现全局设置,那么这样会非常繁琐。笔者这里提供了一个基础的优化模块提供了修改表单视图中子列表部件的默认长度,可以参考下面的图:
分组
编写过Form表单的同学应该都知道odoo的这样一条行为规范: 使用group标签包裹的字段会自动添加标签描述,而使用div包裹则不会自动添加标签。这条规范就是Group标签在表单的渲染动作中“做了手脚”。
我们从FormRenderer的代码中找到了渲染group标签的代码:
_renderTagGroup: function (node) {
var isOuterGroup = _.some(node.children, function (child) {
return child.tag === 'group';
});
if (!isOuterGroup) {
return this._renderInnerGroup(node);
}
return this._renderOuterGroup(node);
}
从代码中可以看出,渲染group标签使用了_renderInnerGroup方法,按图索骥我们来看一下该方法的具体作用:
_renderInnerGroup: function (node) {
...
var $tds;
if (child.tag === 'field') {
$tds = self._renderInnerGroupField(child);
} else if (child.tag === 'label') {
$tds = self._renderInnerGroupLabel(child);
} else {
var $td = $('<td/>');
var $child = self._renderNode(child);
if ($child.hasClass('o_td_label')) { // transfer classname to outer td for css reasons
$td.addClass('o_td_label');
$child.removeClass('o_td_label');
}
$tds = $td.append($child);
}
if (finalColspan > 1) {
$tds.last().attr('colspan', finalColspan);
}
$currentRow.append($tds);
...
}
改方法的代码比较长,我们这里只选择了部分代码,从上面的这部分代码中我们可以知道,在处理group标签的时候,如果节点(node)是field类型,那么使用_renderInnerGroupField,如果是label类型,则使用_renderInnerGroupLabel方法来处理渲染工作。这里我们先看一下field的处理方法:
_renderInnerGroupField: function (node) {
var $el = this._renderFieldWidget(node, this.state);
var $tds = $('<td/>').append($el);
if (node.attrs.nolabel !== '1') {
var $labelTd = this._renderInnerGroupLabel(node);
$tds = $labelTd.add($tds);
// apply the oe_(edit|read)_only className on the label as well
if (/\boe_edit_only\b/.test(node.attrs.class)) {
$tds.addClass('oe_edit_only');
}
if (/\boe_read_only\b/.test(node.attrs.class)) {
$tds.addClass('oe_read_only');
}
}
return $tds;
}
上面的这段代码是group标签处理field类型的核心逻辑: 先处理field声明的使用的widget或默认widget,然后将其放到表格的td节点中,如果节点使用了nolabel标签,那么不添加标签的渲染。否则使用_renderInnerGroupLabel方法来自动生成对应的label标签。那么_renderInnerGroupLabel方法又是如何自动生成标签的呢?
_renderInnerGroupLabel: function (node) {
return $('<td/>', {class: 'o_td_label'})
.append(this._renderTagLabel(node));
},
在这里我们看到了又一个公共方法:_renderTagLabel。顾名思义,_renderTagLabel应该就是要真正处理Label渲染工作的方法了。
标签
标签的渲染工作实际由方法完成_renderTagLabel:
_renderTagLabel: function (node) {
...
var self = this;
var text;
let fieldName;
if (node.tag === 'label') {
fieldName = this.fieldIdsToNames[node.attrs.for]; // 'for' references a <field> node id
} else {
fieldName = node.attrs.name;
}
if ('string' in node.attrs) { // allow empty string
text = node.attrs.string;
} else if (fieldName) {
text = this.state.fields[fieldName].string;
} else {
return this._renderGenericTag(node);
}
var $result = $('<label>', {
class: 'o_form_label',
for: this._getIDForLabel(node.tag === 'label' ? node.attrs.for : node.attrs.id),
text: text,
});
if (node.tag === 'label') {
this._handleAttributes($result, node);
}
...
}
代码同样比较长,我们这里节选核心部分来解读一下它的工作原理。首先,_renderTagLabel是个公共方法,不仅为分组的标签工作,同样为其他类型的节点服务。我们看到_renderTagLabel方法会判断当前节点的类型,如果是Label,那么会直接使用节点的for属性指定的字段的名字作为标签的显示文字,否则使用节点自己的name属性。但是如果label节点自身标记的string属性,那么string的属性将是优先级最高的。
然后,_renderTagLabel方法会创建一个label标签,使用o_form_label样式,如果节点是Label那么使用for属性,否则使用节点属性中的id作为新创建的Label的for属性的值。如果节点是Label时,还会单独处理它的属性*。
具体地讲,就是处理label标签属性中的样式class、style和placeholder属性。
了解了group和标签的渲染原理,对我们来说是十分有用的,例如笔者就利用这个原理写了一个用来自定义标签的颜色的模块,这里简单提一下该模块用法:
<field name="authors" widget="many2many_tags" color="red"/>
安装此模块以后,我们只要在field的属性中声明一个color属性,并设置相应的颜色值,该字段的标签颜色就会发生相应的改变。上面的代码,最终渲染的效果图如下:
有需要的同学可以到商城选购。
只读显示和只编辑显示
某些情况下,我们可能会希望某些字段(标签)只在编辑模式下显示,某些字段呢又只在只读模式下显示。这种情况下的解决办法就是使用oe_read_only和oe_edit_only样式来完成。
<label for="name" string="主层编码属性值" class="oe_edit_only"/>
<h1>
<field name="name" options="{'no_open':1}" class="oe_edit_only"/>
<field name="display_char" class="oe_read_only"/>
</h1>
上面的代码示例,name字段只在编辑模式下显示,而display字段则只在读模式下显示。